Files
CLI-Anything/docs/hub/index.html
T
yuhao 6f372d36f8 perf(hub): batch the phone deck and skip toolbar-resize rebuilds
Fast scrolling on iPhone could still kill the tab after the GIF/video and
content-visibility work. Two remaining causes only real iOS exhibits:

- iOS Safari fires a resize when its toolbar collapses mid-scroll, and the
  handler rebuilt both masonry grids (teardown + re-append of 101 cards)
  during the fling. Masonry columns depend only on width, so the handler
  now bails when the width is unchanged.

- Phones laid out all 79 harness cards (~28k px of live DOM) up front —
  content-visibility skips paint but the initial masonry measurement still
  forces layout of every card, and a fling still rasterizes the whole run.
  Single-column layouts now render the deck in batches of 20 that grow via
  a sentinel IntersectionObserver (1600px margin) as the user approaches;
  the mobile deck placeholder shrinks to match. Multi-column desktop
  masonry still renders the full set so shortest-column balancing is exact.
  jumpToTool materializes remaining batches before scrolling to a target
  card; search/filter/sort reset to the first batch.

Chromium renderer RSS (390x844@3x fling scenario): load 297MB -> 171MB,
peak 807MB -> 380MB vs the original page. Full cross-engine suite passes
(WebKit + Chromium, mobile + desktop): batch growth while scrolling,
reset-on-search, jumpToTool into late batches, footer reachable at true
bottom, video rings, flip lifecycle, no overflow, zero page errors.
2026-08-13 04:30:13 +00:00

2897 lines
182 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="en" data-theme="dark">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="robots" content="index,follow,max-snippet:-1,max-image-preview:large,max-video-preview:-1">
<title>CLI-Anything Hub - Agent-Friendly CLI Registry</title>
<meta name="description" content="CLI-Anything Hub is an agent-friendly registry and package manager for CLI tools that let AI agents discover, install, and operate GUI applications, developer tools, creative software, web APIs, and public SaaS platforms.">
<meta name="author" content="HKUDS and Yuhao Yang">
<meta name="copyright" content="Copyright: HKUDS. Lead author: Yuhao Yang (yuhao.page).">
<link rel="canonical" href="https://clianything.cc/">
<link rel="alternate" type="text/markdown" href="https://clianything.cc/index.md" title="CLI-Anything Hub Markdown">
<link rel="alternate" type="text/plain" href="https://clianything.cc/llms.txt" title="CLI-Anything LLM Context">
<link rel="alternate" type="text/plain" href="https://clianything.cc/llms-full.txt" title="CLI-Anything Full LLM Context">
<meta property="og:type" content="website">
<meta property="og:site_name" content="CLI-Anything Hub">
<meta property="og:title" content="CLI-Anything Hub - Agent-Friendly CLI Registry">
<meta property="og:description" content="Discover, install, and launch agent-friendly CLI tools for GUI applications, developer tools, creative software, web APIs, and public SaaS platforms.">
<meta property="og:url" content="https://clianything.cc/">
<meta name="twitter:card" content="summary">
<meta name="twitter:title" content="CLI-Anything Hub - Agent-Friendly CLI Registry">
<meta name="twitter:description" content="Agent-friendly CLI registry and package manager for discovering, installing, and launching command-line tools.">
<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=Hanken+Grotesk:ital,wght@0,400;0,500;0,600;0,700;0,800;1,500&family=Instrument+Serif:ital@0;1&family=JetBrains+Mono:wght@400;500&family=Newsreader:ital,opsz,wght@0,6..72,300;0,6..72,400;0,6..72,500;1,6..72,300;1,6..72,400&display=swap" rel="stylesheet">
<style>
/* ===================== Tokens (cool/neutral/tech palette system) =====================
The anchor tokens (--bg, --surface, --ink/2/3, --line, --accent, --accent2) are set
per [data-palette][data-theme] by applyPalette() in JS from the PALETTES object.
Everything below derives glass / chip / shadow / accent-ink from those anchors so the
glass optics survive every palette × theme combination. Steel-dark values are inlined
as a no-JS fallback. */
:root {
color-scheme: dark;
/* --- anchor tokens (Steel · dark fallback; overridden by JS) --- */
--bg: #0d1117;
--surface: #151b24;
--ink: #e8edf4;
--ink-2: #9fb0c4;
--ink-3: #647488;
--line: rgba(255,255,255,0.08);
--accent: #5b9bd6;
--accent2: #7db4e6;
--accent-rgb: 91,155,214;
--surface-rgb: 21,27,36;
--accent-band: 209; /* palette accent hue; per-category tints fan around it */
--ok: #54d196;
--radius: 22px;
--radius-sm: 13px;
/* derived (dark) */
--accent-ink: var(--accent2);
--glass-bg: linear-gradient(158deg, rgba(255,255,255,0.075), rgba(255,255,255,0.024) 46%, rgba(255,255,255,0.010));
--glass-bg-fallback: rgba(var(--surface-rgb), 0.86);
--glass-border: var(--line);
--glass-edge: rgba(255,255,255,0.14);
--glass-shadow: 0 24px 60px -22px rgba(0,0,0,0.62), 0 2px 8px -2px rgba(0,0,0,0.48);
--glass-shadow-hi: 0 38px 90px -28px rgba(0,0,0,0.70), 0 4px 14px -4px rgba(0,0,0,0.52);
--chip-bg: rgba(255,255,255,0.055);
--cmd-bg: rgba(0,0,0,0.30);
--cmd-line: rgba(255,255,255,0.08);
--hue-default: 212;
--tint-s: 52%;
--tint-l: 62%;
--ease: cubic-bezier(.22,.61,.36,1);
--spring: cubic-bezier(.34,1.32,.5,1);
--flip-ease: cubic-bezier(.46,.9,.4,1.05);
--palette-transition: background .45s var(--ease), color .45s var(--ease), border-color .45s var(--ease);
}
[data-theme="light"] {
color-scheme: light;
/* Steel · light fallback (overridden by JS) */
--bg: #f4f6f9;
--surface: #ffffff;
--ink: #14181f;
--ink-2: #4a5568;
--ink-3: #8595a8;
--line: rgba(20,40,80,0.10);
--accent: #2f6fb0;
--accent2: #1f5793;
--accent-rgb: 47,111,176;
--surface-rgb: 255,255,255;
--accent-ink: var(--accent2);
--glass-bg: linear-gradient(158deg, rgba(255,255,255,0.92), rgba(255,255,255,0.62) 48%, rgba(255,255,255,0.42));
--glass-bg-fallback: rgba(var(--surface-rgb), 0.92);
--glass-border: var(--line);
--glass-edge: rgba(255,255,255,0.96);
--glass-shadow: 0 22px 50px -24px rgba(30,44,70,0.22), 0 2px 6px -2px rgba(30,44,70,0.12);
--glass-shadow-hi: 0 34px 80px -28px rgba(30,44,70,0.28), 0 4px 12px -4px rgba(30,44,70,0.16);
--chip-bg: rgba(20,40,80,0.045);
--cmd-bg: rgba(20,40,80,0.05);
--cmd-line: rgba(20,40,80,0.08);
--tint-s: 46%;
--tint-l: 44%;
}
/* every surface eases between palettes (reduced-motion neutralizes below) */
body, .nav.scrolled, .card-face, .mcard, .stat, .empower-card,
.typing-shell, .deck-tab, .filter-btn, .search, .sort-select, .chip-label,
.scope-banner, .palette-swatch { transition: var(--palette-transition); }
/* ===================== Reset / base ===================== */
* { margin: 0; padding: 0; box-sizing: border-box; }
html { scroll-behavior: smooth; scroll-padding-top: 84px; -webkit-text-size-adjust: 100%; }
body {
font-family: 'Hanken Grotesk', sans-serif;
background: var(--bg);
color: var(--ink);
line-height: 1.6;
font-size: 16px;
letter-spacing: -0.004em;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
min-height: 100vh;
overflow-x: hidden;
}
::selection { background: rgba(var(--accent-rgb), 0.30); color: var(--ink); }
a { color: inherit; }
button { font-family: inherit; }
h1,h2,h3 { letter-spacing: -0.022em; line-height: 1.1; font-weight: 700; }
code, .mono { font-family: 'JetBrains Mono', monospace; }
/* ===================== Ambient backdrop ===================== */
.ambient { position: fixed; inset: 0; z-index: -2; overflow: hidden; pointer-events: none; }
/* No filter:blur() here — a 100px blur over three ~700px fixed layers is one of the most
expensive things a page can ask a mobile GPU to hold. The radial gradients below fade
to transparent well before their edge, which reads identically to the blurred version. */
.ambient i { position: absolute; border-radius: 50%; opacity: 0.7; }
.ambient .a { width: 60vw; max-width: 760px; aspect-ratio: 1; top: -18vw; right: -10vw;
background: radial-gradient(circle at 38% 38%, hsl(calc(var(--accent-band) + 28) var(--tint-s) 58% / 0.26), hsl(calc(var(--accent-band) + 28) var(--tint-s) 58% / 0.10) 40%, transparent 68%); }
.ambient .b { width: 54vw; max-width: 680px; aspect-ratio: 1; bottom: -16vw; left: -12vw;
background: radial-gradient(circle at 60% 42%, hsl(var(--accent-band) calc(var(--tint-s) + 12%) 56% / 0.24), hsl(var(--accent-band) calc(var(--tint-s) + 12%) 56% / 0.09) 38%, transparent 66%); }
.ambient .c { width: 46vw; max-width: 560px; aspect-ratio: 1; top: 42%; left: 50%;
background: radial-gradient(circle, hsl(calc(var(--accent-band) - 22) var(--tint-s) 54% / 0.14), hsl(calc(var(--accent-band) - 22) var(--tint-s) 54% / 0.06) 40%, transparent 70%); }
[data-theme="light"] .ambient i { opacity: 0.55; }
.grain { position: fixed; inset: 0; z-index: -1; pointer-events: none; opacity: 0.04; mix-blend-mode: overlay;
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='120' height='120'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='2'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E"); }
/* The full-viewport blend layer forces the whole page through an offscreen compositing
group; on phone GPUs that costs more than the texture is worth. */
@media (hover: none), (pointer: coarse) { .grain { display: none; } }
/* ===================== Glass primitive ===================== */
.glass {
position: relative;
background: var(--glass-bg-fallback);
border: 1px solid var(--glass-border);
border-radius: var(--radius);
box-shadow: var(--glass-shadow);
}
/* Real backdrop blur is desktop-only. Each blurred surface is its own composited layer
that re-reads the backdrop, and this page has dozens at once — enough to blow past
iOS Safari's per-tab GPU budget (the page reloads/white-flashes on iPhone). Touch
devices get the tuned --glass-bg-fallback, which was designed to stand alone. */
@media (hover: hover) and (pointer: fine) {
@supports ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) {
.glass { background: var(--glass-bg);
-webkit-backdrop-filter: blur(22px) saturate(1.5); backdrop-filter: blur(22px) saturate(1.5); }
}
}
.glass::before { content: ''; position: absolute; inset: 0; border-radius: inherit; pointer-events: none;
box-shadow: inset 0 1px 0 var(--glass-edge), inset 0 0 0 1px rgba(255,255,255,0.02); }
/* ===================== Layout ===================== */
.wrap { max-width: 1180px; margin: 0 auto; padding: 0 22px; }
/* ===================== Nav ===================== */
.nav {
position: sticky; top: 0; z-index: 40;
border-bottom: 1px solid transparent;
transition: border-color .3s, background .3s, backdrop-filter .3s;
}
.nav.scrolled {
border-color: var(--line);
background: color-mix(in oklab, var(--bg) 78%, transparent);
-webkit-backdrop-filter: blur(16px) saturate(1.4); backdrop-filter: blur(16px) saturate(1.4);
}
.nav-in { max-width: 1180px; margin: 0 auto; padding: 0 22px; display: flex; align-items: center; justify-content: space-between; gap: 14px; height: 64px; }
.nav-brand { display: inline-flex; align-items: center; gap: 9px; text-decoration: none; font-weight: 800; font-size: 1.08rem; letter-spacing: -0.02em; }
.nav-brand-badge { width: 30px; height: 30px; display: grid; place-items: center; border-radius: 9px; flex-shrink: 0;
border: 1px solid var(--glass-border); background: var(--chip-bg); box-shadow: var(--glass-shadow); }
.nav-brand-badge svg { width: 64%; height: 64%; }
.nav-links { display: flex; align-items: center; gap: 4px; }
.nav-link { padding: 7px 11px; border-radius: 10px; text-decoration: none; color: var(--ink-2); font-size: 0.84rem; font-weight: 500;
transition: color .18s, background .18s; display: inline-flex; align-items: center; gap: 6px; }
.nav-link:hover { color: var(--ink); background: var(--chip-bg); }
.nav-link svg { width: 15px; height: 15px; flex-shrink: 0; }
.nav-link-stars { font-family: 'JetBrains Mono', monospace; font-size: 0.72rem; font-weight: 600; color: var(--accent-ink);
background: var(--chip-bg); border: 1px solid var(--line); border-radius: 7px; padding: 1px 7px; font-variant-numeric: tabular-nums; }
.theme-toggle { display: inline-grid; place-items: center; width: 36px; height: 36px; border-radius: 11px;
border: 1px solid var(--line); background: var(--chip-bg); color: var(--ink-2); cursor: pointer; transition: color .18s, border-color .18s; }
.theme-toggle:hover { color: var(--ink); border-color: var(--ink-3); }
.theme-toggle svg { width: 17px; height: 17px; }
.theme-toggle .icon-moon { display: none; }
[data-theme="light"] .theme-toggle .icon-moon { display: block; }
[data-theme="light"] .theme-toggle .icon-sun { display: none; }
/* Mobile nav drawer */
.nav-menu { display: flex; align-items: center; gap: 4px; }
.nav-toggle { display: none; position: relative; z-index: 70; place-items: center; width: 38px; height: 38px;
border-radius: 11px; border: 1px solid var(--line); background: var(--chip-bg); color: var(--ink); cursor: pointer;
transition: color .18s, border-color .18s; }
.nav-toggle:hover { border-color: var(--ink-3); }
.nav-toggle svg { width: 19px; height: 19px; }
.nav-toggle .icon-x { display: none; }
.nav-toggle.open .icon-bars { display: none; }
.nav-toggle.open .icon-x { display: block; }
.nav-scrim { position: fixed; inset: 0; z-index: 55; background: rgba(2,5,10,0.5);
-webkit-backdrop-filter: blur(2px); backdrop-filter: blur(2px); opacity: 0; visibility: hidden;
transition: opacity .3s var(--ease), visibility .3s; }
.nav-scrim.open { opacity: 1; visibility: visible; }
.nav-menu.open { transform: none; }
@media (max-width: 860px) {
.nav-toggle { display: inline-grid; }
.nav-menu { position: fixed; top: 0; right: 0; height: 100dvh; width: min(82vw, 320px);
flex-direction: column; align-items: stretch; gap: 6px; padding: 80px 16px 28px;
background: var(--glass-bg-fallback); border-left: 1px solid var(--line);
box-shadow: -24px 0 64px -28px rgba(0,0,0,0.6); transform: translateX(106%);
transition: transform .34s var(--ease); overflow-y: auto; z-index: 60; }
.nav-menu .nav-link { padding: 12px 14px; font-size: 0.98rem; border-radius: 11px; color: var(--ink-2); }
.nav-menu .nav-link.active { color: var(--ink); background: var(--chip-bg); }
.nav-menu .nav-link svg { width: 19px; height: 19px; }
.nav-menu .nav-link-stars { margin-left: auto; }
}
@supports ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) {
@media (max-width: 860px) {
.nav-menu { background: var(--glass-bg); -webkit-backdrop-filter: blur(22px) saturate(1.4); backdrop-filter: blur(22px) saturate(1.4); }
}
}
/* ===================== Palette switcher ===================== */
/* Palette switcher hidden from the header (default palette = Steel still applies). */
.palette-switch { display: none; align-items: center; gap: 5px; padding: 4px; border-radius: 13px;
border: 1px solid var(--line); background: var(--chip-bg); }
.palette-switch-label { font-size: 0.62rem; font-weight: 700; letter-spacing: 0.1em; text-transform: uppercase;
color: var(--ink-3); padding: 0 4px 0 6px; }
.palette-swatch { width: 24px; height: 24px; border-radius: 8px; cursor: pointer; padding: 0; position: relative;
border: 1px solid rgba(255,255,255,0.18); overflow: hidden;
background: linear-gradient(135deg, var(--sw-accent) 0 52%, var(--sw-grey) 52% 100%); }
[data-theme="light"] .palette-swatch { border-color: rgba(20,40,80,0.18); }
.palette-swatch:hover { transform: translateY(-1px); }
.palette-swatch:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.palette-swatch[aria-pressed="true"] { box-shadow: 0 0 0 2px var(--bg), 0 0 0 4px var(--accent); }
@media (max-width: 860px) { .palette-switch-label { display: none; } }
@media (max-width: 520px) { .palette-switch { display: none; } }
/* compact popover variant for small screens */
.palette-popwrap { position: relative; display: none; }
.palette-pop-btn { display: inline-grid; place-items: center; width: 36px; height: 36px; border-radius: 11px;
border: 1px solid var(--line); background: var(--chip-bg); color: var(--ink-2); cursor: pointer; }
.palette-pop-btn:hover { color: var(--ink); border-color: var(--ink-3); }
.palette-pop-btn svg { width: 17px; height: 17px; }
.palette-pop-panel { position: absolute; right: 0; top: calc(100% + 8px); display: none; flex-direction: column; gap: 6px;
padding: 10px; border-radius: 13px; border: 1px solid var(--glass-border); background: var(--glass-bg-fallback);
box-shadow: var(--glass-shadow-hi); z-index: 50; min-width: 150px; }
.palette-popwrap.open .palette-pop-panel { display: flex; }
.palette-pop-panel .pp-row { display: flex; align-items: center; gap: 9px; padding: 6px 8px; border-radius: 9px;
border: 1px solid transparent; background: transparent; color: var(--ink-2); cursor: pointer; font-size: 0.84rem; font-weight: 600; font-family: inherit; }
.palette-pop-panel .pp-row:hover { background: var(--chip-bg); color: var(--ink); }
.palette-pop-panel .pp-row[aria-pressed="true"] { color: var(--ink); border-color: var(--accent); }
.palette-pop-panel .pp-dot { width: 18px; height: 18px; border-radius: 6px; flex-shrink: 0;
background: linear-gradient(135deg, var(--sw-accent) 0 52%, var(--sw-grey) 52% 100%); }
/* ===================== Reveal motion ===================== */
.reveal { opacity: 0; transform: translateY(22px); transition: opacity .7s var(--ease), transform .7s var(--ease); }
.reveal.in { opacity: 1; transform: none; }
[style*="--d"] { transition-delay: var(--d); }
/* ===================== Hero ===================== */
.hero { max-width: 1180px; margin: 0 auto; padding: 70px 22px 24px; text-align: center; }
.hero-title { display: flex; align-items: center; justify-content: center; gap: clamp(14px, 2.4vw, 22px); }
/* Badge scales with the serif title so it reads as a peer of the wordmark (≈ title
font-size), not a small chip. The inner glyph tracks the tile size. */
.hero-title-badge { width: clamp(2.6rem, 7.4vw, 5rem); height: clamp(2.6rem, 7.4vw, 5rem);
display: grid; place-items: center; border-radius: clamp(15px, 1.8vw, 22px); flex-shrink: 0;
border: 1px solid var(--glass-border); background: var(--chip-bg); box-shadow: var(--glass-shadow); }
.hero-title-badge svg { width: 64%; height: 64%; }
/* Premium Serif Display title (V1 "Accent on Anything"): Newsreader serif,
"Anything" in serif italic carrying the solid palette accent, while "CLI-" and
"Hub" stay ink. No gradient. The accent uses the palette's accent2 in light and
accent in dark (legibility-tuned), so the word recolors with the palette switcher. */
.hero h1 { font-family: 'Newsreader', serif; font-weight: 400; color: var(--ink);
font-size: clamp(2.7rem, 7.6vw, 5.1rem); letter-spacing: -0.018em; line-height: 1.0; }
.hero-word { display: inline-block; }
.hero-word--cli, .hero-word--hub { color: var(--ink); }
.hero-word--anything { font-style: italic; font-weight: 300; letter-spacing: 0; color: var(--accent2); }
[data-theme="dark"] .hero-word--anything { color: var(--accent); }
.hero-tagline { max-width: 60ch; margin: 20px auto 0; color: var(--ink-2); font-size: clamp(1rem, 2.2vw, 1.18rem); }
.hero-tagline-line--any { color: var(--ink); font-weight: 600; }
.stats { display: flex; justify-content: center; gap: 12px; flex-wrap: wrap; margin-top: 30px; }
.stat { padding: 13px 24px; border-radius: 15px; text-align: center; min-width: 130px;
background: var(--glass-bg-fallback); border: 1px solid var(--glass-border); box-shadow: var(--glass-shadow); position: relative; }
@media (hover: hover) and (pointer: fine) {
@supports ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) {
.stat { background: var(--glass-bg); -webkit-backdrop-filter: blur(18px) saturate(1.4); backdrop-filter: blur(18px) saturate(1.4); }
}
}
/* Stat figures use the SAME two-token accent rule as the italic "Anything" word in the
title (accent2 in light, accent in dark), so they recolor with palette + theme, and
are enlarged to read as large figures like the remote reference. */
.stat-num { font-size: clamp(2.1rem, 4.4vw, 2.7rem); font-weight: 800; letter-spacing: -0.03em;
font-variant-numeric: tabular-nums; line-height: 1.05; color: var(--accent2); }
[data-theme="dark"] .stat-num { color: var(--accent); }
.stat-label { font-size: 0.72rem; color: var(--ink-3); text-transform: uppercase; letter-spacing: 0.1em; font-weight: 600; margin-top: 2px; }
/* ===================== Empower cards ===================== */
/* align-items: stretch makes the two side-by-side cards equal height; each card is a
column flexbox so its inner content distributes evenly down to the matched height. */
.empower-row { display: grid; gap: 18px; grid-template-columns: 1fr 1fr; margin-top: 38px; text-align: left; align-items: stretch; }
/* grid items default to min-width:auto, so the no-wrap install command was forcing the
card — and the whole page — wider than a phone screen (627px on a 390px viewport) */
.empower-row > .empower-card { min-width: 0; }
@media (max-width: 820px) { .empower-row { grid-template-columns: 1fr; } }
.empower-card { padding: 24px 24px 22px; border-radius: var(--radius); border: 1px solid var(--glass-border);
background: var(--glass-bg-fallback); box-shadow: var(--glass-shadow); position: relative;
display: flex; flex-direction: column; }
@media (hover: hover) and (pointer: fine) {
@supports ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) {
.empower-card { background: var(--glass-bg); -webkit-backdrop-filter: blur(20px) saturate(1.4); backdrop-filter: blur(20px) saturate(1.4); }
}
}
.empower-card::before { content: ''; position: absolute; inset: 0; border-radius: inherit; pointer-events: none;
box-shadow: inset 0 1px 0 var(--glass-edge); }
.empower-card h3 { font-size: 1.16rem; font-weight: 700; }
.empower-card h3 span { color: var(--accent2); }
[data-theme="dark"] .empower-card h3 span { color: var(--accent); }
.empower-available { font-size: 0.84rem; color: var(--ink-3); margin-top: 8px; }
.empower-available a { color: var(--accent-ink); text-decoration: none; font-weight: 600; }
.empower-available a:hover { text-decoration: underline; }
.install-row { display: flex; align-items: center; gap: 0; margin-top: 14px; border-radius: 13px; overflow: hidden;
border: 1px solid var(--cmd-line); background: var(--cmd-bg); }
.install-row--wrap { flex-wrap: wrap; }
.install-row .chip-label { font-family: 'JetBrains Mono', monospace; font-size: 0.68rem; font-weight: 600; text-transform: lowercase;
color: var(--ink-3); padding: 0 12px; align-self: stretch; display: inline-flex; align-items: center; border-right: 1px solid var(--cmd-line); white-space: nowrap; }
.install-row code { font-family: 'JetBrains Mono', monospace; font-size: 0.82rem; padding: 12px 14px; color: var(--ink);
flex: 1; min-width: 0; overflow-x: auto; white-space: nowrap; }
.copy-btn { border: 0; border-left: 1px solid var(--cmd-line); background: var(--chip-bg); color: var(--ink-2);
padding: 0 16px; align-self: stretch; cursor: pointer; font-size: 0.78rem; font-weight: 600;
transition: color .18s, background .18s; white-space: nowrap; }
.copy-btn:hover { color: var(--ink); background: color-mix(in oklab, var(--accent) 18%, transparent); }
.empower-note { font-size: 0.84rem; color: var(--ink-2); margin-top: 14px; }
.empower-note strong { color: var(--ink); }
.empower-prompt { font-size: 0.85rem; color: var(--ink-2); margin-top: 10px; padding: 11px 13px; border-radius: 11px;
border: 1px solid var(--line); background: var(--chip-bg); }
.empower-prompt strong { color: var(--ink); }
.empower-prompt em { color: var(--accent-ink); font-style: italic; }
/* Toolkit grid: compressed rows (tighter padding, smaller gaps) so the "Empower yourself"
card fills its share of the matched equal-height row cleanly without looking sparse. */
.cmd-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 6px; margin-top: 14px; flex: 1 1 auto; align-content: stretch; }
@media (max-width: 420px) { .cmd-grid { grid-template-columns: 1fr; } }
.cmd-item { display: flex; flex-direction: column; justify-content: center; gap: 0; padding: 7px 11px; border-radius: 9px;
border: 1px solid var(--line); background: var(--chip-bg); }
.cmd-name { font-family: 'JetBrains Mono', monospace; font-size: 0.75rem; color: var(--accent-ink); font-weight: 500; line-height: 1.35; }
.cmd-desc { font-size: 0.72rem; color: var(--ink-3); line-height: 1.3; }
/* ===================== Hero actions ===================== */
.hero-actions { display: flex; flex-wrap: wrap; gap: 10px; justify-content: center; margin-top: 34px; }
.btn-primary, .btn-secondary { display: inline-flex; align-items: center; gap: 8px; padding: 12px 20px; border-radius: 13px;
font-weight: 600; font-size: 0.9rem; text-decoration: none; cursor: pointer; transition: transform .15s, border-color .18s, background .18s; }
.btn-primary { color: #fff; border: 1px solid transparent;
background: linear-gradient(120deg, var(--accent2), var(--accent)); box-shadow: 0 12px 30px -14px rgba(var(--accent-rgb), 0.7); }
.btn-primary:hover { transform: translateY(-2px); }
.btn-secondary { color: var(--ink); border: 1px solid var(--glass-border); background: var(--chip-bg); }
.btn-secondary:hover { transform: translateY(-2px); border-color: var(--ink-3); }
.btn-secondary svg { width: 15px; height: 15px; }
.hero-hint { margin-top: 18px; font-size: 0.86rem; color: var(--ink-3); }
.hero-hint a { color: var(--accent-ink); font-weight: 600; text-decoration: none; }
.hero-hint a:hover { text-decoration: underline; }
.hero-cta { max-width: 760px; margin: 26px auto 0; font-size: 0.9rem; color: var(--ink-2); text-align: center; }
.hero-cta .nowrap { white-space: nowrap; }
.hero-cta em { color: var(--accent-ink); font-style: normal; font-weight: 700; }
.hero-cta a { color: var(--accent-ink); font-weight: 600; text-decoration: none; }
.hero-cta a:hover { text-decoration: underline; }
/* ===================== Marketplace prelude / typing shell =====================
Wide-but-short, matching the remote design's compact proportions: the shell
spans the column (no narrow max-width), the caption + Copy sit on one short
header row, and the command line is a single slim band — re-themed to glass. */
.marketplace-prelude { max-width: 1180px; margin: 36px auto 0; padding: 0 22px; }
.typing-shell { width: 100%; max-width: 720px; margin: 0 auto; border-radius: var(--radius-sm); overflow: hidden;
padding: 13px 15px 14px; border: 1px solid var(--glass-border); background: var(--glass-bg-fallback);
box-shadow: var(--glass-shadow); position: relative; }
@media (hover: hover) and (pointer: fine) {
@supports ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) {
.typing-shell { background: var(--glass-bg); -webkit-backdrop-filter: blur(20px) saturate(1.4); backdrop-filter: blur(20px) saturate(1.4); }
}
}
.typing-shell-head { display: flex; align-items: center; justify-content: space-between; gap: 12px;
margin-bottom: 9px; }
.typing-shell-label { display: inline-flex; align-items: center; gap: 7px; font-size: 0.7rem; color: var(--ink-3);
letter-spacing: 0.05em; text-transform: uppercase; font-weight: 600; font-style: italic; }
.typing-shell-label::before { content: ''; width: 7px; height: 7px; border-radius: 999px; background: var(--ok);
box-shadow: 0 0 12px color-mix(in oklab, var(--ok) 60%, transparent); flex-shrink: 0; }
.typing-shell-copy { border: 1px solid var(--cmd-line); border-left: 1px solid var(--cmd-line); border-radius: 9px;
padding: 5px 13px; flex-shrink: 0; }
.typing-shell-body { display: flex; align-items: center; gap: 9px; min-height: 0; padding: 9px 12px;
border-radius: 9px; border: 1px solid var(--cmd-line); background: var(--cmd-bg); }
.typing-shell-prompt { color: var(--ok); font-family: 'JetBrains Mono', monospace; font-weight: 600; font-size: 0.84rem; flex-shrink: 0; }
.typing-shell-code { flex: 1; min-width: 0; font-family: 'JetBrains Mono', monospace; font-size: 0.84rem; color: var(--ink);
display: inline-flex; align-items: center; white-space: nowrap; overflow: hidden; }
.typing-shell-cursor { display: inline-block; width: 8px; height: 1.05em; background: var(--accent); margin-left: 2px; animation: blink 1.05s steps(1) infinite; }
@keyframes blink { 0%,50%{opacity:1} 50.01%,100%{opacity:0} }
/* ===================== Deck nav ===================== */
.deck-nav { max-width: 1180px; margin: 56px auto 18px; padding: 0 22px; display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
.deck-tab { display: inline-flex; align-items: center; gap: 9px; padding: 10px 16px; border-radius: 999px; cursor: pointer;
border: 1px solid var(--line); background: var(--chip-bg); color: var(--ink-2); font-size: 0.9rem; font-weight: 600;
transition: color .18s, border-color .18s, background .18s; }
.deck-tab:hover { color: var(--ink); }
.deck-tab.active { color: var(--bg); background: var(--ink); border-color: var(--ink); }
[data-theme="light"] .deck-tab.active { color: #fff; }
.deck-count { font-family: 'JetBrains Mono', monospace; font-size: 0.72rem; font-weight: 600; opacity: 0.7; font-variant-numeric: tabular-nums; }
.deck-separator { flex: 1; }
.deck-arrow-btn { display: inline-flex; align-items: center; gap: 7px; padding: 9px 14px; border-radius: 11px; cursor: pointer;
border: 1px solid var(--glass-border); background: var(--chip-bg); color: var(--ink-2); font-size: 0.84rem; font-weight: 600;
transition: color .18s, border-color .18s; }
.deck-arrow-btn:hover { color: var(--ink); border-color: var(--ink-3); }
.deck-arrow-btn svg { width: 16px; height: 16px; }
.deck-arrow-btn--back { flex-direction: row-reverse; }
@media (max-width: 560px) { .deck-arrow-btn { display: none; } .deck-separator { display: none; } }
/* ===================== Deck stage ===================== */
/* ── Render-skip for offscreen page regions ──
content-visibility:auto lets the engine skip layout+paint for sections far
from the viewport, so a fast fling doesn't rasterize ~30k px of cards at
once (the iOS Safari tab-kill scenario). JS measurement inside a skipped
subtree (masonry offsetHeight) still works: layout APIs force layout, only
paint stays skipped. contain-intrinsic-size uses the `auto` keyword so the
placeholder snaps to the real size after first render; the px values are
only the pre-render estimate. Engines without support just ignore this. */
/* Each rule sets a plain-length fallback first: Safari 15.416 supports
content-visibility but not the `auto` keyword here, and would otherwise
drop the whole declaration and give skipped sections no height. */
.demos-teaser, .marketplace-prelude, .matrices, .deck-stage, .footer { content-visibility: auto; }
.demos-teaser { contain-intrinsic-size: 1000px; contain-intrinsic-size: auto 1000px; }
.marketplace-prelude { contain-intrinsic-size: 220px; contain-intrinsic-size: auto 220px; }
.matrices { contain-intrinsic-size: 900px; contain-intrinsic-size: auto 900px; }
.deck-stage { contain-intrinsic-size: 9500px; contain-intrinsic-size: auto 9500px; }
@media (max-width: 640px) { .deck-stage { contain-intrinsic-size: 7200px; contain-intrinsic-size: auto 7200px; } }
.footer { contain-intrinsic-size: 320px; contain-intrinsic-size: auto 320px; }
.deck-stage { max-width: 1180px; margin: 0 auto; padding: 0 22px; }
.deck-viewport { overflow: hidden; touch-action: pan-y; cursor: grab; }
.deck-viewport.dragging { cursor: grabbing; }
/* will-change only while the deck actually moves (deck-sliding is armed on every drag or
animated switch) — a permanent will-change would pin the full-height two-panel track
as a composited layer, a large standing GPU allocation on phones. */
.deck-track { display: flex; transition: transform .5s var(--spring); }
html.deck-sliding .deck-track { will-change: transform; }
.deck-track.is-dragging { transition: none; }
/* Each panel is exactly one viewport wide; the track slides by one full panel width
between decks. (Replaces the old over-wide "peek" carousel that mis-measured its
width and clipped the active panel on the right while bleeding the other on the left.) */
.deck-panel { flex: 0 0 100%; min-width: 0; position: relative; }
.deck-sentinel { height: 1px; }
/* Perf — Safari especially. While the deck moves we collapse every effect that forces a
per-frame re-rasterization of the large card subtree, so each panel caches as a flat
layer the GPU can translate cheaply. All effects return the moment motion stops.
• per-card glass backdrop-blur (cheap on Chrome, costly on Safari)
• the ~150 3D flip contexts (perspective + preserve-3d + backface-visibility) — the
real Safari cliff: Safari cannot flatten them and re-renders each context per frame.
Flattening to transform-style:flat keeps the correct face visible via
backface-visibility, with no flip glitch on settle.
• the sticky-nav backdrop-blur and the blend-mode grain overlay. */
html.deck-sliding .card-face,
html.deck-sliding .nav.scrolled { -webkit-backdrop-filter: none !important; backdrop-filter: none !important; }
html.deck-sliding .card { perspective: none !important; }
html.deck-sliding .card-inner { transform-style: flat !important; }
html.deck-sliding .grain { visibility: hidden; }
/* Pager dots + swipe hint — signal the deck is a swipeable two-panel carousel. */
.deck-dots { display: inline-flex; align-items: center; gap: 7px; margin-left: 2px; }
.deck-dot { width: 8px; height: 8px; padding: 0; border: 0; border-radius: 999px; cursor: pointer;
background: var(--ink-3); opacity: 0.4; transition: width .25s var(--ease), opacity .2s, background .2s; }
.deck-dot:hover { opacity: 0.7; }
.deck-dot.active { width: 22px; opacity: 1; background: var(--accent); }
.deck-hint { display: inline-flex; align-items: center; gap: 6px; font-size: 0.74rem; color: var(--ink-3); white-space: nowrap; }
.deck-hint svg { width: 16px; height: 16px; opacity: 0.8; }
@media (max-width: 600px) { .deck-hint span { display: none; } }
/* ===================== Controls (search + sort) ===================== */
.controls { display: flex; gap: 12px; align-items: center; margin-bottom: 16px; flex-wrap: wrap; }
.search { flex: 1; min-width: 220px; padding: 13px 16px; border-radius: 13px; border: 1px solid var(--glass-border);
background: var(--chip-bg); color: var(--ink); font-size: 0.92rem; font-family: inherit; transition: border-color .18s, background .18s; }
.search::placeholder { color: var(--ink-3); }
.search:focus { outline: none; border-color: var(--accent); background: color-mix(in oklab, var(--accent) 8%, var(--chip-bg)); }
.sort-select { padding: 12px 14px; border-radius: 13px; border: 1px solid var(--glass-border); background: var(--chip-bg);
color: var(--ink); font-size: 0.86rem; font-family: inherit; font-weight: 500; cursor: pointer; transition: border-color .18s; }
.sort-select:focus { outline: none; border-color: var(--accent); }
.sort-select option { background: var(--bg-2); color: var(--ink); }
.public-panel-note { font-size: 0.82rem; color: var(--ink-3); margin-bottom: 14px; padding: 12px 15px; border-radius: 12px;
border: 1px solid var(--line); background: var(--chip-bg); max-width: 80ch; }
/* ===================== Filter chips ===================== */
.filter-row { display: flex; gap: 7px; flex-wrap: wrap; margin-bottom: 22px; }
.filter-btn { padding: 7px 14px; border-radius: 999px; border: 1px solid var(--line); background: var(--chip-bg);
color: var(--ink-2); font-size: 0.8rem; font-weight: 600; cursor: pointer; transition: color .15s, border-color .15s, background .15s; }
.filter-btn:hover { color: var(--ink); }
.filter-btn.active { color: var(--bg); background: var(--ink); border-color: var(--ink); }
[data-theme="light"] .filter-btn.active { color: #fff; }
/* ===================== Card grid + 3D flip cards ===================== */
/* Masonry: cards interlock into balanced columns so uneven heights fill the vertical
gaps — an abundant, staggered "琳琅满目" wall of CLIs rather than a ragged row grid.
Engine-independent JS masonry (layoutMasonry): a flex row of .grid-col wrappers,
each a vertical flex column; cards are distributed into the SHORTEST column as we go.
(CSS multicol balancing was buggy — Safari left empty tops in columns 23.)
max-width caps usable width because the deck-panel is intentionally over-wide for
the swipe carousel; columns land at ~3 across desktop / 2 tablet / 1 phone. */
/* Cap to the visible viewport (not the over-wide carousel panel) so the grid's own width
tracks the real screen — this lets masonryColumnCount pick 3/2/1 columns responsively. */
.grid { display: flex; align-items: flex-start; gap: 18px; max-width: min(1044px, calc(100vw - 108px)); padding-bottom: 24px; }
/* phones: the panel is exactly viewport-wide now, so let cards use the full column
instead of the desktop's defensive 100vw-108px cap (which left ~60px dead space) */
@media (max-width: 640px) { .grid { max-width: 100%; } }
.grid-col { flex: 1 1 0; min-width: 0; display: flex; flex-direction: column; gap: 18px; }
/* the .empty / error state is a direct child of .grid (not wrapped in a column) */
.grid > .empty { flex: 1 1 100%; }
/* Cards are FLAT by default. Safari cannot flatten a perspective + preserve-3d +
backface-visibility context and re-renders each one per frame — with ~150 of them this
is the biggest static cost on the page (the deck-slide suspender below already proved
the flat state renders the correct face via backface-visibility with no glitch).
The real 3D context is enabled per-card only while it is hovered or mid-flip (.is-3d,
set by setCardFlipped for the duration of the flip transition). */
.card { position: relative; }
.card-inner { position: absolute; inset: 0; width: 100%; height: 100%;
transform-style: flat; transition: transform .72s var(--flip-ease); }
.card.flipped .card-inner { transform: rotateY(180deg); }
.card.is-3d { perspective: 1600px; }
.card.is-3d .card-inner { transform-style: preserve-3d; }
/* subtle hover tilt hint (does not auto-flip) */
@media (hover: hover) {
.card:hover { perspective: 1600px; }
.card:hover .card-inner { transform-style: preserve-3d; }
.card:not(.flipped):hover .card-inner { transform: rotateY(-5deg) rotateX(2.5deg); }
}
.card-face {
position: absolute; inset: 0; display: flex; flex-direction: column;
-webkit-backface-visibility: hidden; backface-visibility: hidden;
border-radius: var(--radius); border: 1px solid var(--glass-border);
background:
radial-gradient(120% 90% at 0% 0%, hsl(var(--ch, var(--hue-default)) var(--tint-s) var(--tint-l) / 0.16), transparent 56%),
var(--glass-bg-fallback);
box-shadow: var(--glass-shadow); overflow: hidden;
}
/* ~110 live card faces at once — the single biggest backdrop-filter population on the
page. Desktop pointers only (see the .glass note above). */
@media (hover: hover) and (pointer: fine) {
@supports ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) {
.card-face { background:
radial-gradient(120% 90% at 0% 0%, hsl(var(--ch, var(--hue-default)) var(--tint-s) var(--tint-l) / 0.18), transparent 56%),
var(--glass-bg);
-webkit-backdrop-filter: blur(18px) saturate(1.4); backdrop-filter: blur(18px) saturate(1.4); }
}
}
.card-face::before { content: ''; position: absolute; inset: 0; border-radius: inherit; pointer-events: none;
box-shadow: inset 0 1px 0 var(--glass-edge); z-index: 3; }
.card-front { padding: 18px 19px; }
.card-back { transform: rotateY(180deg); padding: 16px 17px; }
/* content-sized: .card-sizer is in normal flow and its height follows the FRONT face
only (the back is taken out of flow), so the card fits its compact front content
with no empty bottom gap. .card-inner overlays absolutely on top for the real 3D
flip; when flipped, the back fills that same height and scrolls internally. */
.card-sizer { position: relative; width: 100%; }
.card-sizer .card-face { position: relative; box-shadow: none; border: 1px solid transparent; background: none; visibility: hidden;
-webkit-backdrop-filter: none; backdrop-filter: none; }
.card-sizer .card-back { position: absolute; inset: 0; transform: none; }
/* FRONT face content */
.card-head { display: flex; align-items: flex-start; gap: 10px; }
.card-title { font-size: 1.04rem; font-weight: 700; flex: 1; min-width: 0; line-height: 1.25; }
.card-title a { text-decoration: none; color: var(--ink); display: inline-flex; align-items: center; gap: 5px; }
.card-title a:hover { color: var(--accent-ink); }
.card-title svg { width: 12px; height: 12px; flex-shrink: 0; opacity: 0.65; }
.card-meta { display: flex; flex-direction: column; align-items: flex-end; gap: 5px; flex-shrink: 0; }
.category-tag { font-size: 0.62rem; font-weight: 700; letter-spacing: 0.05em; text-transform: uppercase; padding: 3px 8px; border-radius: 7px;
color: hsl(var(--ch, var(--hue-default)) 80% 76%); background: hsl(var(--ch, var(--hue-default)) var(--tint-s) var(--tint-l) / 0.16);
border: 1px solid hsl(var(--ch, var(--hue-default)) 70% 64% / 0.32); white-space: nowrap; }
[data-theme="light"] .category-tag { color: hsl(var(--ch, var(--hue-default)) 72% 38%); }
.card-version { font-family: 'JetBrains Mono', monospace; font-size: 0.66rem; color: var(--ink-3); font-weight: 500; }
.public-badge { font-size: 0.6rem; font-weight: 700; letter-spacing: 0.05em; text-transform: uppercase; padding: 3px 7px; border-radius: 6px;
border: 1px solid var(--line); background: var(--chip-bg); color: var(--ink-2); white-space: nowrap; }
.public-badge--npm { color: #d98a4a; border-color: #d98a4a55; }
.public-badge--brew { color: #c79cff; border-color: #c79cff55; }
.public-badge--bundled { color: var(--ink-3); }
.card-desc { color: var(--ink-2); font-size: 0.86rem; margin-top: 11px;
display: -webkit-box; -webkit-line-clamp: 3; -webkit-box-orient: vertical; overflow: hidden; }
.card-date { font-size: 0.72rem; color: var(--ink-3); margin-top: 10px; }
.card-requires { font-size: 0.76rem; color: var(--ink-2); margin-top: 8px; }
.card-requires strong { color: var(--ink); font-weight: 700; }
/* install block (used on both faces where applicable) */
.card-install { display: flex; align-items: stretch; gap: 0; margin-top: 8px; border-radius: 11px; overflow: hidden;
border: 1px solid var(--cmd-line); background: var(--cmd-bg); }
.card-install code { font-family: 'JetBrains Mono', monospace; font-size: 0.74rem; color: var(--ink); padding: 9px 11px;
flex: 1; min-width: 0; overflow: hidden; display: flex; flex-direction: column; gap: 3px; }
/* Each command renders on exactly ONE line; long lines truncate with an ellipsis.
The full, untruncated text is preserved in the Copy payload (display only). */
.card-install .install-command { color: var(--accent-ink); display: block; min-width: 0; line-height: 1.5;
white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.card-install--note code { color: var(--ink-3); display: block; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
/* two-step install: a short plain-language label above the clean command box */
.install-steps { margin-top: 14px; }
.install-steps-label { display: flex; align-items: center; gap: 7px; font-size: 0.7rem; font-weight: 600; color: var(--ink-3);
letter-spacing: 0.01em; margin-bottom: 5px; }
.install-steps-label .isl-num { display: inline-grid; place-items: center; width: 15px; height: 15px; border-radius: 5px; flex-shrink: 0;
font-family: 'JetBrains Mono', monospace; font-size: 0.6rem; font-weight: 700; color: var(--accent-ink);
background: hsl(var(--ch, var(--hue-default)) var(--tint-s) var(--tint-l) / 0.16); border: 1px solid hsl(var(--ch, var(--hue-default)) var(--tint-s) 60% / 0.3); }
.install-steps-label .isl-sep { color: var(--ink-3); opacity: 0.55; }
.card-copy-btn { border: 0; border-left: 1px solid var(--cmd-line); background: var(--chip-bg); color: var(--ink-2);
padding: 0 13px; cursor: pointer; font-size: 0.72rem; font-weight: 600; white-space: nowrap; transition: color .18s, background .18s; align-self: stretch; }
.card-copy-btn:hover { color: var(--ink); background: color-mix(in oklab, var(--accent) 18%, transparent); }
.card-public-notes { margin-top: 10px; display: flex; flex-direction: column; gap: 8px; }
.card-npx { font-size: 0.76rem; color: var(--ink-2); }
.card-npx--direct strong { color: var(--ink); font-weight: 700; display: block; margin-bottom: 2px; }
.card-npx--direct span { font-family: 'JetBrains Mono', monospace; font-size: 0.72rem; color: var(--accent-ink); word-break: break-all; }
.card-npx--comment { padding-left: 11px; border-left: 2px solid var(--line); color: var(--ink-3); }
.card-footer { margin-top: auto; padding-top: 14px; display: flex; align-items: center; justify-content: space-between; gap: 10px; flex-wrap: wrap; }
.card-links { display: flex; align-items: center; gap: 4px; font-size: 0.78rem; color: var(--ink-3); }
.card-links a { color: var(--accent-ink); text-decoration: none; font-weight: 600; }
.card-links a:hover { text-decoration: underline; }
.card-contributor { font-size: 0.76rem; color: var(--ink-3); display: inline-flex; align-items: center; gap: 6px; flex-wrap: wrap; }
.card-contributor a { color: var(--ink-2); text-decoration: none; font-weight: 500; }
.card-contributor a:hover { color: var(--ink); text-decoration: underline; }
.community-badge { font-size: 0.58rem; font-weight: 700; letter-spacing: 0.04em; text-transform: uppercase; padding: 2px 6px; border-radius: 5px;
color: var(--ok); border: 1px solid color-mix(in oklab, var(--ok) 45%, transparent); background: color-mix(in oklab, var(--ok) 14%, transparent); }
/* flip affordance — a clear "Details" / "Back" pill (NOT a refresh glyph) */
.card-front-foot { margin-top: 14px; padding-top: 13px; display: flex; align-items: center; justify-content: space-between; gap: 10px; }
.flip-btn { display: inline-flex; align-items: center; gap: 6px; padding: 6px 12px; border-radius: 10px; cursor: pointer;
border: 1px solid var(--glass-border); background: var(--chip-bg); color: var(--ink-2); font-size: 0.76rem; font-weight: 600;
font-family: inherit; transition: color .18s, border-color .18s, background .18s, transform .15s; flex-shrink: 0; }
.flip-btn:hover { color: var(--ink); border-color: hsl(var(--ch, var(--hue-default)) 70% 64% / 0.6);
background: hsl(var(--ch, var(--hue-default)) var(--tint-s) var(--tint-l) / 0.14); transform: translateY(-1px); }
.flip-btn:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.flip-btn svg { width: 14px; height: 14px; flex-shrink: 0; }
.flip-btn--back svg { transform: scaleX(-1); }
.flip-hint { font-size: 0.7rem; color: var(--ink-3); font-weight: 600; }
/* ===================== Software icons (Simple Icons) ===================== */
/* Unified rounded tile; the <img> is the single-color Simple Icons SVG tinted to the
palette ink; the .icon-mono fallback is a tasteful single-letter monogram. */
.sw-icon { display: inline-grid; place-items: center; width: 26px; height: 26px; border-radius: 8px; flex-shrink: 0;
background: hsl(var(--ch, var(--hue-default)) var(--tint-s) var(--tint-l) / 0.14);
border: 1px solid hsl(var(--ch, var(--hue-default)) var(--tint-s) 60% / 0.28); overflow: hidden; }
.sw-icon img { width: 15px; height: 15px; object-fit: contain; display: block;
/* Simple Icons are single-color SVGs; flatten them to a cohesive monochrome glyph that
stays legible in both themes (near-white on dark, near-ink on light). */
filter: var(--icon-tint); }
/* Favicon-sourced logos (apps Simple Icons lacks) keep their real colour — no monochrome
flatten — and sit a touch larger since they're already a rounded glyph. */
.sw-icon img.raw { filter: none; width: 18px; height: 18px; border-radius: 4px; }
.sw-icon .icon-mono { font-family: 'Hanken Grotesk', sans-serif; font-size: 0.8rem; font-weight: 700; line-height: 1;
color: hsl(var(--ch, var(--hue-default)) 70% 70%); text-transform: uppercase; }
[data-theme="light"] .sw-icon .icon-mono { color: hsl(var(--ch, var(--hue-default)) calc(var(--tint-s) + 14%) 40%); }
/* dark theme: glyph → light ink; light theme: glyph → dark ink. brightness(0) flattens any
source color to black, then invert/opacity lifts it to the right tone for the theme. */
:root { --icon-tint: brightness(0) invert(1) opacity(0.86); }
[data-theme="light"] { --icon-tint: brightness(0) invert(0.18) opacity(0.92); }
/* a row of stacked member icons on matrix cards */
.icon-stack { display: flex; margin-top: 14px; align-items: center; }
.icon-stack .sw-icon { margin-left: -8px; box-shadow: 0 2px 6px rgba(0,0,0,0.25); background:
radial-gradient(120% 120% at 30% 25%, hsl(var(--ch, var(--hue-default)) var(--tint-s) var(--tint-l) / 0.2), var(--surface)); }
.icon-stack .sw-icon:first-child { margin-left: 0; }
.icon-stack .sw-icon.miss { opacity: 0.6; }
.icon-stack .more { font-size: 0.72rem; color: var(--ink-3); margin-left: 9px; }
/* small icon beside a card / matrix title */
.title-icon { width: 22px; height: 22px; border-radius: 7px; }
.title-icon img { width: 13px; height: 13px; }
.title-icon .icon-mono { font-size: 0.66rem; }
.card-title-row { display: inline-flex; align-items: center; gap: 8px; min-width: 0; }
/* back face */
.card-back-head { display: flex; align-items: center; justify-content: space-between; gap: 8px; margin-bottom: 10px; }
.card-back-title { font-size: 0.7rem; font-weight: 700; letter-spacing: 0.12em; text-transform: uppercase; color: var(--ink-3); }
.card-back-scroll { flex: 1; min-height: 0; overflow-y: auto; padding-right: 4px; }
.card-back-scroll::-webkit-scrollbar { width: 7px; }
.card-back-scroll::-webkit-scrollbar-track { background: transparent; }
.card-back-scroll::-webkit-scrollbar-thumb { background: var(--line); border-radius: 6px; }
.card-back-scroll::-webkit-scrollbar-thumb:hover { background: var(--ink-3); }
.card-back-scroll { scrollbar-width: thin; scrollbar-color: var(--line) transparent; }
.card-back-field { margin-top: 12px; }
.card-back-field:first-child { margin-top: 0; }
.card-back-label { font-size: 0.64rem; font-weight: 700; letter-spacing: 0.08em; text-transform: uppercase; color: var(--ink-3); margin-bottom: 5px; }
/* a face hidden from a11y/tab when not active */
.card:not(.flipped) .card-back :is(a,button,input,select,[tabindex]),
.card.flipped .card-front :is(a,button,input,select,[tabindex]) { /* aria-hidden handled via attr; this is style fallback */ }
.empty { grid-column: 1 / -1; text-align: center; padding: 56px 20px; color: var(--ink-3); }
.empty a { color: var(--accent); }
.skeleton { border-radius: var(--radius); min-height: 300px; background: var(--chip-bg); animation: shimmerSk 1.6s var(--ease) infinite; }
@keyframes shimmerSk { 0%,100%{opacity:.5} 50%{opacity:1} }
/* skill popover (public cards) */
.skill-popover { position: relative; display: inline-block; }
.card-link-btn { border: 0; background: transparent; color: var(--accent-ink); font-size: 0.78rem; font-weight: 600;
font-family: inherit; cursor: pointer; padding: 0; }
.card-link-btn:hover { text-decoration: underline; }
.skill-popover-panel { position: absolute; bottom: calc(100% + 8px); left: 50%; transform: translateX(-50%) translateY(6px);
min-width: 240px; max-width: 320px; padding: 12px; border-radius: 12px; border: 1px solid var(--glass-border);
background: var(--glass-bg-fallback); box-shadow: var(--glass-shadow-hi); z-index: 20;
opacity: 0; pointer-events: none; transition: opacity .2s, transform .2s; display: flex; flex-direction: column; gap: 8px; }
@media (hover: hover) and (pointer: fine) {
@supports ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) {
.skill-popover-panel { background: var(--glass-bg); -webkit-backdrop-filter: blur(20px) saturate(1.5); backdrop-filter: blur(20px) saturate(1.5); }
}
}
.skill-popover.is-open .skill-popover-panel { opacity: 1; pointer-events: auto; transform: translateX(-50%); }
.skill-popover-label { font-size: 0.62rem; font-weight: 700; letter-spacing: 0.1em; text-transform: uppercase; color: var(--ink-3); }
.skill-popover-panel code { font-family: 'JetBrains Mono', monospace; font-size: 0.7rem; color: var(--accent-ink); word-break: break-all;
padding: 8px 10px; border-radius: 8px; background: var(--cmd-bg); border: 1px solid var(--cmd-line); display: block; }
.skill-popover-copy { align-self: flex-start; border: 1px solid var(--cmd-line); border-radius: 8px; padding: 6px 12px; }
/* front-face Skill control: compact icon+label, sized like the flip pill (not oversized) */
.card-skill-front { display: inline-flex; align-items: center; gap: 6px; padding: 6px 11px; border-radius: 10px; cursor: pointer;
border: 1px solid var(--glass-border); background: var(--chip-bg); color: var(--accent-ink); font-size: 0.76rem; font-weight: 600;
font-family: inherit; text-decoration: none; transition: color .18s, border-color .18s, background .18s, transform .15s; flex-shrink: 0; }
.card-skill-front:hover { color: var(--ink); border-color: hsl(var(--ch, var(--hue-default)) 70% 64% / 0.6);
background: hsl(var(--ch, var(--hue-default)) var(--tint-s) var(--tint-l) / 0.14); transform: translateY(-1px); text-decoration: none; }
.card-skill-front:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.card-skill-front svg { width: 14px; height: 14px; flex-shrink: 0; }
/* ===================== Matrices band ===================== */
.matrices { max-width: 1180px; margin: 0 auto; padding: 64px 22px 8px; }
.matrices-head { display: flex; align-items: flex-end; justify-content: space-between; gap: 16px; margin: 0 0 24px; flex-wrap: wrap; }
.matrices-head .klabel { font-size: 0.72rem; font-weight: 700; letter-spacing: 0.16em; text-transform: uppercase; color: var(--accent-ink); }
.matrices-head h2 { font-size: clamp(1.6rem, 4vw, 2.2rem); margin-top: 8px; }
.matrices-head p { color: var(--ink-2); max-width: 54ch; margin-top: 8px; font-size: 0.96rem; }
.matrices-head .mat-stat { font-family: 'JetBrains Mono', monospace; font-size: 0.78rem; color: var(--ink-3); white-space: nowrap; }
.shelf { display: grid; gap: 18px; grid-template-columns: repeat(auto-fill, minmax(330px, 1fr)); grid-auto-rows: 1fr; align-items: stretch; }
@media (min-width: 980px) { .shelf { grid-template-columns: repeat(3, 1fr); } }
@media (max-width: 760px) {
.shelf { display: flex; grid-auto-flow: column; overflow-x: auto; scroll-snap-type: x mandatory;
margin: 0 -22px; padding: 0 22px 12px; gap: 16px; }
.shelf > .mcard { flex: 0 0 84%; scroll-snap-align: center; }
}
/* matrix flip card — reuses .card / .card-inner / .card-face machinery, taller min-height */
/* flat-by-default like .card above; 3D only on hover / mid-flip */
.mcard { min-height: 372px; position: relative; }
.mcard .card-inner { position: absolute; inset: 0; width: 100%; height: 100%;
transform-style: flat; transition: transform .72s var(--flip-ease); }
.mcard.flipped .card-inner { transform: rotateY(180deg); }
.mcard.is-3d { perspective: 1600px; }
.mcard.is-3d .card-inner { transform-style: preserve-3d; }
@media (hover: hover) {
.mcard:hover { perspective: 1600px; }
.mcard:hover .card-inner { transform-style: preserve-3d; }
.mcard:not(.flipped):hover .card-inner { transform: rotateY(-4deg) rotateX(2deg); }
}
.mcard .card-sizer { position: relative; width: 100%; min-height: inherit; }
.mcard .card-sizer .card-face { position: relative; box-shadow: none; border: 1px solid transparent; background: none; visibility: hidden;
-webkit-backdrop-filter: none; backdrop-filter: none; }
.mcard .card-sizer .card-back { position: absolute; inset: 0; transform: none; }
.mcard-top { display: flex; align-items: center; justify-content: space-between; gap: 10px; }
.mcard-glyph { width: 42px; height: 42px; border-radius: 13px; display: grid; place-items: center;
background: hsl(var(--ch, var(--hue-default)) var(--tint-s) var(--tint-l) / 0.18); border: 1px solid hsl(var(--ch, var(--hue-default)) var(--tint-s) 60% / 0.34); color: hsl(var(--ch, var(--hue-default)) var(--tint-s) 74%); }
[data-theme="light"] .mcard-glyph { color: hsl(var(--ch, var(--hue-default)) calc(var(--tint-s) + 14%) 38%); }
.mcard-glyph svg { width: 21px; height: 21px; }
.mcard h3 { font-size: 1.2rem; margin-top: 16px; }
.mcard .mdesc { color: var(--ink-2); font-size: 0.88rem; margin-top: 8px;
display: -webkit-box; -webkit-line-clamp: 3; -webkit-box-orient: vertical; overflow: hidden; }
.mcard .meta-row { display: flex; align-items: center; gap: 14px; margin-top: 14px; font-size: 0.8rem; color: var(--ink-3); }
.mcard .meta-row b { color: var(--ink); font-weight: 700; }
.avatars { display: flex; margin-top: 14px; align-items: center; }
.avatars .av { width: 30px; height: 30px; border-radius: 9px; margin-left: -7px; display: grid; place-items: center;
font-size: 0.6rem; font-weight: 700; background: var(--chip-bg); border: 1px solid var(--glass-border);
box-shadow: 0 2px 6px rgba(0,0,0,0.25); text-transform: uppercase; color: var(--ink-2); }
.avatars .av:first-child { margin-left: 0; }
.avatars .av.miss { opacity: 0.55; }
.avatars .more { font-size: 0.72rem; color: var(--ink-3); margin-left: 9px; }
/* matrix front: primary command + flip foot */
.mcard-front-foot { margin-top: auto; padding-top: 13px; display: flex; align-items: center; justify-content: space-between; gap: 10px; }
/* matrix back: scrolling detail */
.mback-scroll { flex: 1; min-height: 0; overflow-y: auto; padding-right: 5px; scrollbar-width: thin; scrollbar-color: var(--line) transparent; }
.mback-scroll::-webkit-scrollbar { width: 7px; }
.mback-scroll::-webkit-scrollbar-thumb { background: var(--line); border-radius: 6px; }
.msub { font-size: 0.62rem; font-weight: 700; letter-spacing: 0.1em; text-transform: uppercase; color: var(--ink-3); margin: 16px 0 8px; }
.msub:first-child { margin-top: 2px; }
.mcmd { display: flex; align-items: center; gap: 8px; background: var(--cmd-bg); border: 1px solid var(--cmd-line);
border-radius: 10px; padding: 8px 10px; margin-bottom: 7px; }
/* matrix commands stay on a single line; truncate with an ellipsis if they overflow
(Copy still copies the full command). */
.mcmd code { font-family: 'JetBrains Mono', monospace; font-size: 0.74rem; color: var(--ink); flex: 1; min-width: 0;
white-space: nowrap; overflow: hidden; text-overflow: ellipsis; line-height: 1.5; }
.mcmd code .pfx { color: var(--ink-3); }
.mcmd .card-copy-btn { border: 0; border-left: 1px solid var(--cmd-line); }
/* capability accordions */
.cap { border: 1px solid var(--line); border-radius: 11px; margin-bottom: 7px; overflow: hidden; background: var(--chip-bg); }
.cap > summary { list-style: none; cursor: pointer; padding: 10px 12px; display: flex; align-items: center; gap: 9px; }
.cap > summary::-webkit-details-marker { display: none; }
.cap > summary:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; border-radius: 10px; }
.cap .cap-id { font-family: 'JetBrains Mono', monospace; font-size: 0.66rem; color: hsl(var(--ch, var(--hue-default)) var(--tint-s) 74%); flex-shrink: 0;
background: hsl(var(--ch, var(--hue-default)) 50% 50% / 0.14); padding: 3px 6px; border-radius: 6px; }
[data-theme="light"] .cap .cap-id { color: hsl(var(--ch, var(--hue-default)) calc(var(--tint-s) + 14%) 38%); }
.cap .cap-title { font-weight: 600; font-size: 0.82rem; flex: 1; min-width: 0; }
.cap .chev { width: 14px; height: 14px; color: var(--ink-3); transition: transform .3s var(--ease); flex-shrink: 0; }
.cap[open] .chev { transform: rotate(90deg); }
.cap-detail { padding: 0 12px 12px; }
.cap-intent { color: var(--ink-2); font-size: 0.8rem; margin-bottom: 10px; }
.io { display: flex; flex-wrap: wrap; gap: 5px; margin-bottom: 10px; }
.io span { font-family: 'JetBrains Mono', monospace; font-size: 0.64rem; padding: 3px 7px; border-radius: 6px; background: var(--cmd-bg); color: var(--ink-2); border: 1px solid var(--cmd-line); }
.io span.out { color: hsl(var(--ch, var(--hue-default)) var(--tint-s) 70%); }
[data-theme="light"] .io span.out { color: hsl(var(--ch, var(--hue-default)) calc(var(--tint-s) + 12%) 40%); }
.prov { display: flex; gap: 9px; padding: 8px 0; border-top: 1px dashed var(--line); }
.prov:first-of-type { border-top: 0; }
.prov .pk { font-size: 0.58rem; font-weight: 700; letter-spacing: 0.04em; text-transform: uppercase; padding: 3px 6px; border-radius: 6px;
white-space: nowrap; height: fit-content; border: 1px solid; flex-shrink: 0; margin-top: 2px; }
.prov .pbody b { font-weight: 600; font-size: 0.8rem; }
.prov .pbody .pmeta { font-size: 0.7rem; color: var(--ink-3); margin-top: 1px; }
.prov .pbody .pnotes { font-size: 0.74rem; color: var(--ink-2); margin-top: 3px; }
/* provider-kind badges (cool/neutral) */
.pk-agentnative { color: #54d196; border-color: #54d19655; background: #54d19614; }
.pk-publiccli { color: var(--accent); border-color: rgba(var(--accent-rgb),.4); background: rgba(var(--accent-rgb),.10); }
.pk-harnesscli { color: #9aa6ff; border-color: #9aa6ff55; background: #9aa6ff14; }
.pk-agentskill { color: #5ad1d1; border-color: #5ad1d155; background: #5ad1d114; }
.pk-bundledscript{ color: #79c0e8; border-color: #79c0e855; background: #79c0e814; }
.pk-api { color: #5ad1d1; border-color: #5ad1d155; background: #5ad1d114; }
.pk-native, .pk-python, .pk-websearch, .pk-fallback { color: var(--ink-2); border-color: var(--line); background: var(--chip-bg); }
[data-theme="light"] .pk-agentnative{color:#0f8a52} [data-theme="light"] .pk-harnesscli{color:#4350c0}
[data-theme="light"] .pk-agentskill{color:#127d7d} [data-theme="light"] .pk-bundledscript{color:#1f6fa0} [data-theme="light"] .pk-api{color:#127d7d}
.member { display: flex; flex-wrap: wrap; gap: 7px; }
.member button { display: inline-flex; align-items: center; gap: 6px; padding: 6px 10px; border-radius: 9px; cursor: pointer;
border: 1px solid var(--glass-border); background: var(--chip-bg); color: var(--ink); font-size: 0.78rem; font-weight: 500;
font-family: inherit; transition: border-color .18s, background .18s, transform .18s; }
.member button:hover { border-color: hsl(var(--ch, var(--hue-default)) var(--tint-s) 60% / 0.6); transform: translateY(-1px); }
.member button .badge-mini { font-size: 0.56rem; padding: 1px 5px; border-radius: 5px; background: var(--cmd-bg); color: var(--ink-3); text-transform: uppercase; letter-spacing: 0.04em; }
.member button.miss { opacity: 0.6; cursor: default; }
.mback-cta { display: flex; gap: 8px; flex-wrap: wrap; margin-top: 14px; }
.mbtn { display: inline-flex; align-items: center; gap: 7px; padding: 9px 13px; border-radius: 10px; font-weight: 600; font-size: 0.8rem;
text-decoration: none; cursor: pointer; border: 1px solid var(--glass-border); background: var(--chip-bg); color: var(--ink); font-family: inherit;
transition: border-color .18s, background .18s, transform .15s; }
.mbtn:hover { transform: translateY(-1px); border-color: var(--ink-3); }
.mbtn.primary { background: hsl(var(--ch, var(--hue-default)) var(--tint-s) var(--tint-l) / 0.2); border-color: hsl(var(--ch, var(--hue-default)) var(--tint-s) 60% / 0.5); }
.mbtn.primary:hover { background: hsl(var(--ch, var(--hue-default)) var(--tint-s) var(--tint-l) / 0.3); }
.mbtn svg { width: 14px; height: 14px; }
/* scope banner above the decks */
.scope-banner { display: none; align-items: center; gap: 12px; flex-wrap: wrap; padding: 12px 16px; margin: 0 22px 16px;
max-width: 1180px; margin-left: auto; margin-right: auto;
border-radius: 14px; border: 1px solid hsl(var(--ch, var(--hue-default)) var(--tint-s) 60% / 0.4); background: hsl(var(--ch, var(--hue-default)) var(--tint-s) var(--tint-l) / 0.12); }
.scope-banner.on { display: flex; }
.scope-banner .lbl { font-size: 0.86rem; }
.scope-banner b { font-weight: 700; }
.scope-banner .clear { margin-left: auto; border: 0; background: transparent; color: var(--ink-2); cursor: pointer; font-weight: 600;
font-size: 0.82rem; display: inline-flex; gap: 6px; align-items: center; padding: 5px 9px; border-radius: 9px; font-family: inherit; }
.scope-banner .clear:hover { background: var(--chip-bg); color: var(--ink); }
/* membership chip on catalog cards */
.in-matrix { display: inline-flex; align-items: center; gap: 5px; font-size: 0.68rem; font-weight: 600; padding: 4px 8px; border-radius: 8px;
border: 1px solid var(--line); background: var(--chip-bg); color: var(--ink-2); cursor: pointer; font-family: inherit; transition: color .15s, border-color .15s; }
.in-matrix:hover { color: var(--ink); border-color: var(--accent); }
.in-matrix .mid { font-family: 'JetBrains Mono', monospace; color: var(--accent-ink); }
.card.flash .card-face { animation: cardFlash 1.5s var(--ease); }
@keyframes cardFlash { 0%,12% { box-shadow: 0 0 0 2px var(--accent), var(--glass-shadow-hi); } 100% { box-shadow: var(--glass-shadow); } }
/* ===================== Footer ===================== */
.footer { border-top: 1px solid var(--line); margin-top: 56px; padding: 34px 22px 50px; max-width: 1180px; margin-left: auto; margin-right: auto;
display: flex; flex-wrap: wrap; gap: 18px 32px; align-items: center; justify-content: space-between; }
.footer-left, .footer-right { font-size: 0.84rem; color: var(--ink-3); }
.footer a { color: var(--accent-ink); text-decoration: none; font-weight: 600; }
.footer a:hover { text-decoration: underline; }
.footer-stats { width: 100%; margin-top: 6px; }
.analytics-report { display: inline-flex; align-items: center; gap: 14px; flex-wrap: wrap; padding: 10px 16px; border-radius: 13px;
border: 1px solid var(--line); background: var(--chip-bg); }
.analytics-kicker { font-size: 0.64rem; font-weight: 700; letter-spacing: 0.14em; text-transform: uppercase; color: var(--accent-ink); }
.analytics-row { display: inline-flex; align-items: baseline; gap: 8px; }
.analytics-title { font-size: 0.74rem; color: var(--ink-3); text-transform: uppercase; letter-spacing: 0.06em; font-weight: 600; }
.analytics-total { font-size: 1.02rem; font-weight: 800; font-variant-numeric: tabular-nums; }
.analytics-pair { font-size: 0.78rem; color: var(--ink-3); font-variant-numeric: tabular-nums; }
.analytics-pair .human-value { color: var(--ink-2); }
.analytics-pair .agent-value { color: var(--accent-ink); }
.analytics-divider { width: 1px; height: 18px; background: var(--line); }
@media (prefers-reduced-motion: reduce) {
* { animation-duration: 0.001ms !important; animation-iteration-count: 1 !important; scroll-behavior: auto !important; }
.reveal { opacity: 1; transform: none; transition: none; }
.card-inner { transition: opacity .2s ease !important; transform: none !important; }
/* Reduced-motion flip becomes an instant crossfade */
.card .card-front { opacity: 1; transition: opacity .2s ease; }
.card .card-back { opacity: 0; transition: opacity .2s ease; }
.card.flipped .card-front { opacity: 0; }
.card.flipped .card-back { opacity: 1; }
.card-face { -webkit-backface-visibility: visible; backface-visibility: visible; }
.card-back { transform: none; }
.card:hover .card-inner { transform: none !important; }
/* matrix flip cards: crossfade too */
.mcard .card-inner { transition: opacity .2s ease !important; transform: none !important; }
.mcard .card-front { opacity: 1; transition: opacity .2s ease; }
.mcard .card-back { opacity: 0; transition: opacity .2s ease; }
.mcard.flipped .card-front { opacity: 0; }
.mcard.flipped .card-back { opacity: 1; }
.mcard:hover .card-inner { transform: none !important; }
}
/* ===================== Real-World Demos — homepage teaser (full gallery on demos.html) ===================== */
.demos-teaser { max-width: 1180px; margin: 56px auto 0; padding: 0 22px; }
.dt-head { display: flex; align-items: flex-end; justify-content: space-between; gap: 16px; margin-bottom: 22px; flex-wrap: wrap; }
.dt-head .kl { font-size: 0.72rem; font-weight: 700; letter-spacing: 0.16em; text-transform: uppercase; color: var(--accent-ink); }
.dt-head h2 { font-size: clamp(1.6rem, 4vw, 2.2rem); margin-top: 8px; }
.dt-head p { color: var(--ink-2); max-width: 56ch; margin-top: 8px; font-size: 0.96rem; }
.dt-seeall { display: inline-flex; align-items: center; gap: 7px; font-weight: 700; font-size: 0.88rem; color: var(--accent-ink); text-decoration: none;
white-space: nowrap; padding: 9px 14px; border-radius: 11px; border: 1px solid var(--glass-border); background: var(--chip-bg); transition: border-color .18s, transform .18s; }
.dt-seeall:hover { border-color: var(--accent); transform: translateY(-1px); }
.dt-seeall svg { width: 16px; height: 16px; }
.demos-strip { display: grid; grid-template-columns: repeat(3, 1fr) auto; gap: 18px; align-items: stretch; }
@media (max-width: 900px) {
.demos-strip { grid-auto-flow: column; grid-template-columns: none; grid-auto-columns: 80%; overflow-x: auto;
scroll-snap-type: x mandatory; margin: 0 -22px; padding: 0 22px 12px; }
.demos-strip > * { scroll-snap-align: start; }
}
.dposter { display: flex; flex-direction: column; padding: 14px; border-radius: var(--radius); text-decoration: none; color: inherit;
background: var(--glass-bg-fallback); border: 1px solid var(--glass-border); box-shadow: var(--glass-shadow); position: relative; transition: transform .3s var(--ease), box-shadow .3s var(--ease); }
@media (hover: hover) and (pointer: fine) {
@supports ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) {
.dposter { background: var(--glass-bg); -webkit-backdrop-filter: blur(22px) saturate(1.5); backdrop-filter: blur(22px) saturate(1.5); } }
}
.dposter:hover { transform: translateY(-4px); box-shadow: var(--glass-shadow-hi); }
.dt-media { position: relative; border-radius: 13px; overflow: hidden; border: 1px solid var(--line); background: #05070b; }
[data-theme="light"] .dt-media { background: #e9edf3; }
.dt-chrome { display: flex; align-items: center; gap: 6px; padding: 7px 11px; background: var(--cmd-bg); border-bottom: 1px solid var(--cmd-line); }
.dt-chrome .dts { display: inline-flex; gap: 4px; }
.dt-chrome .dts i { width: 8px; height: 8px; border-radius: 50%; }
.dt-chrome .dts i:nth-child(1){ background: hsl(8 60% 60%); } .dt-chrome .dts i:nth-child(2){ background: hsl(45 70% 58%); } .dt-chrome .dts i:nth-child(3){ background: hsl(140 45% 55%); }
.dt-chrome .u { font-family: 'JetBrains Mono', monospace; font-size: 0.62rem; color: var(--ink-3); flex: 1; min-width: 0; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.dt-chrome .lv { display: inline-flex; align-items: center; gap: 4px; font-size: 0.56rem; font-weight: 700; letter-spacing: 0.08em; text-transform: uppercase; color: var(--accent-ink); }
.dt-chrome .lv i { width: 5px; height: 5px; border-radius: 50%; background: var(--accent); box-shadow: 0 0 7px var(--accent); animation: dtpulse 1.6s infinite; }
@keyframes dtpulse { 0%,100%{opacity:1} 50%{opacity:.35} }
.dt-media img, .dt-media video { display: block; width: 100%; height: 170px; object-fit: cover; }
.dposter .dt-cat { display: inline-flex; align-items: center; gap: 6px; font-size: 0.64rem; font-weight: 700; letter-spacing: 0.04em; text-transform: uppercase; margin-top: 13px;
color: hsl(var(--ch) var(--tint-s) 74%); }
[data-theme="light"] .dposter .dt-cat { color: hsl(var(--ch) calc(var(--tint-s) + 12%) 36%); }
.dposter .dt-cat svg { width: 12px; height: 12px; }
.dposter h3 { font-family: 'Newsreader', serif; font-weight: 400; font-size: 1.3rem; margin-top: 6px; letter-spacing: -0.01em; }
.dposter h3 .ap { font-style: italic; color: var(--accent-ink); }
.dposter .dt-blurb { color: var(--ink-2); font-size: 0.83rem; margin-top: 7px; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; }
.dposter .dt-by { margin-top: auto; padding-top: 12px; font-size: 0.74rem; color: var(--ink-3); display: flex; align-items: center; gap: 7px; }
.dposter .dt-by b { color: var(--ink-2); font-weight: 600; }
.dt-av { width: 22px; height: 22px; border-radius: 7px; display: grid; place-items: center; font-size: 0.55rem; font-weight: 700; text-transform: uppercase;
background: hsl(var(--ch) var(--tint-s) 52% / 0.2); color: hsl(var(--ch) var(--tint-s) 74%); border: 1px solid hsl(var(--ch) var(--tint-s) 55% / 0.35); }
[data-theme="light"] .dt-av { color: hsl(var(--ch) calc(var(--tint-s) + 12%) 36%); }
.dposter-end { display: grid; place-content: center; gap: 8px; text-align: center; min-width: 184px; padding: 22px; text-decoration: none; color: inherit;
border-radius: var(--radius); border: 1px dashed hsl(var(--accent-band) var(--tint-s) 55% / 0.5); background: hsl(var(--accent-band) var(--tint-s) 52% / 0.07); transition: background .25s, transform .25s; }
.dposter-end:hover { background: hsl(var(--accent-band) var(--tint-s) 52% / 0.13); transform: translateY(-2px); }
.dposter-end .ico { width: 52px; height: 52px; border-radius: 50%; display: grid; place-items: center; margin: 0 auto; color: var(--accent-ink);
border: 1px solid hsl(var(--accent-band) var(--tint-s) 55% / 0.5); background: hsl(var(--accent-band) var(--tint-s) 52% / 0.14); }
.dposter-end .ico svg { width: 22px; height: 22px; }
.dposter-end .go { font-weight: 700; color: var(--ink); font-size: 0.95rem; }
.dposter-end .tx { color: var(--ink-2); font-size: 0.82rem; max-width: 16ch; margin: 0 auto; }
@media (max-width: 900px) { .dposter-end { min-width: 70%; } }
@media (prefers-reduced-motion: reduce) { .dt-chrome .lv i { animation: none; } }
</style>
<script>document.documentElement.classList.add('js');</script>
</head>
<body>
<div class="ambient" aria-hidden="true"><i class="a"></i><i class="b"></i><i class="c"></i></div>
<div class="grain" aria-hidden="true"></div>
<!-- Nav -->
<nav class="nav" id="nav">
<div class="nav-in">
<a class="nav-brand" href="#"><span class="nav-brand-badge" aria-hidden="true"><svg viewBox="0 0 48 48" fill="none"><path d="M8 16.5 18.5 24 8 31.5" stroke="currentColor" stroke-width="3.9" stroke-linecap="square" stroke-linejoin="miter" style="color: var(--accent);"/><path d="M24 29.2h16" stroke="currentColor" stroke-width="3.9" stroke-linecap="square" style="color: var(--ink);"/></svg></span>CLI-Hub</a>
<div class="nav-links">
<div class="nav-menu" id="navMenu">
<a class="nav-link" href="#demos">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.85" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="3" y="4" width="18" height="14" rx="2"/><path d="m10 9 4 2.5L10 14z" fill="currentColor" stroke="none"/></svg>
Demos
</a>
<a class="nav-link" href="#matrices">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.85" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="3.5" y="3.5" width="7" height="7" rx="1.6"/><rect x="13.5" y="3.5" width="7" height="7" rx="1.6"/><rect x="3.5" y="13.5" width="7" height="7" rx="1.6"/><rect x="13.5" y="13.5" width="7" height="7" rx="1.6"/></svg>
Matrices
</a>
<a class="nav-link" href="docs.html">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.85" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M4 4.8A1.8 1.8 0 0 1 5.8 3H12v17H5.8A1.8 1.8 0 0 0 4 21.8z"/><path d="M20 4.8A1.8 1.8 0 0 0 18.2 3H12v17h6.2A1.8 1.8 0 0 1 20 21.8z"/></svg>
Docs
</a>
<a class="nav-link" href="https://reeceyang.sgp1.cdn.digitaloceanspaces.com/SKILL.md" target="_blank">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.85" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m5.5 18.5 9.8-9.8"/><path d="m13.7 6.9 3.4 3.4"/><path d="M18 4v2.4"/><path d="M16.8 5.2h2.4"/><path d="M7.5 4.8v2"/><path d="M6.5 5.8h2"/><path d="M18.2 15.8v2"/><path d="M17.2 16.8h2"/></svg>
SKILL
</a>
<a class="nav-link" href="https://github.com/HKUDS/CLI-Anything" target="_blank">
<svg viewBox="0 0 98 96" fill="currentColor" aria-hidden="true"><path fill-rule="evenodd" clip-rule="evenodd" d="M48.854 0C21.839 0 0 22 0 49.217c0 21.756 13.993 40.172 33.405 46.69 2.427.49 3.316-1.059 3.316-2.362 0-1.141-.08-5.052-.08-9.127-13.59 2.934-16.42-5.867-16.42-5.867-2.184-5.704-5.42-7.17-5.42-7.17-4.448-3.015.324-3.015.324-3.015 4.934.326 7.523 5.052 7.523 5.052 4.367 7.496 11.404 5.378 14.235 4.074.404-3.178 1.699-5.378 3.074-6.6C29.304 70.25 17.9 66.013 17.9 47.02c0-5.52 1.94-10.046 5.127-13.58-.486-1.302-2.264-6.437.486-13.34 0 0 4.206-1.302 13.59 5.216 3.963-1.14 8.17-1.628 12.34-1.628 4.17 0 8.376.568 12.34 1.628 9.384-6.518 13.59-5.216 13.59-5.216 2.75 6.903.972 12.038.486 13.34 3.268 3.534 5.127 8.06 5.127 13.58 0 19.074-11.485 23.15-22.328 24.29 1.78 1.548 3.316 4.481 3.316 9.126 0 6.6-.08 11.897-.08 13.526 0 1.304.89 2.853 3.316 2.364 19.412-6.52 33.405-24.935 33.405-46.691C97.707 22 75.788 0 48.854 0z"/></svg>
GitHub
<span class="nav-link-stars" data-github-stars aria-label="GitHub star count">--</span>
</a>
<a class="nav-link" href="https://arxiv.org/abs/2606.03854" target="_blank" rel="noopener noreferrer">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.85" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M7 3.5h8.2L19 7.3v12A1.7 1.7 0 0 1 17.3 21H7a2 2 0 0 1-2-2V5.5a2 2 0 0 1 2-2z"/><path d="M15 3.5v4h4"/><path d="M8.5 11.2h7"/><path d="M8.5 14.5h7"/><path d="M8.5 17.8h4.5"/><path d="M10 3.5v5l1.7-1.1L13.4 8.5v-5"/></svg>
Tech Report
</a>
<a class="nav-link" href="https://github.com/HKUDS/CLI-Anything/blob/main/CONTRIBUTING.md" target="_blank">
Contribute
</a>
</div>
<div class="palette-switch" id="palette-switch" role="group" aria-label="Color palette">
<span class="palette-switch-label">Palette</span>
</div>
<div class="palette-popwrap" id="palette-popwrap">
<button class="palette-pop-btn" id="palette-pop-btn" aria-haspopup="true" aria-expanded="false" aria-label="Choose color palette">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round"><circle cx="13.5" cy="6.5" r=".5" fill="currentColor"/><circle cx="17.5" cy="10.5" r=".5" fill="currentColor"/><circle cx="8.5" cy="7.5" r=".5" fill="currentColor"/><circle cx="6.5" cy="12.5" r=".5" fill="currentColor"/><path d="M12 2C6.5 2 2 6.5 2 12s4.5 10 10 10c.926 0 1.648-.746 1.648-1.688 0-.437-.18-.835-.437-1.125-.29-.289-.438-.652-.438-1.125 0-.926.746-1.688 1.688-1.688H16c3.314 0 6-2.686 6-6 0-4.97-4.5-8.375-10-8.375Z"/></svg>
</button>
<div class="palette-pop-panel" id="palette-pop-panel" role="group" aria-label="Color palette"></div>
</div>
<button class="theme-toggle" onclick="toggleTheme()" aria-label="Toggle theme">
<svg class="icon-sun" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="5"/><line x1="12" y1="1" x2="12" y2="3"/><line x1="12" y1="21" x2="12" y2="23"/><line x1="4.22" y1="4.22" x2="5.64" y2="5.64"/><line x1="18.36" y1="18.36" x2="19.78" y2="19.78"/><line x1="1" y1="12" x2="3" y2="12"/><line x1="21" y1="12" x2="23" y2="12"/><line x1="4.22" y1="19.78" x2="5.64" y2="18.36"/><line x1="18.36" y1="5.64" x2="19.78" y2="4.22"/></svg>
<svg class="icon-moon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"/></svg>
</button>
<button class="nav-toggle" id="navToggle" aria-label="Menu" aria-expanded="false" aria-controls="navMenu">
<svg class="icon-bars" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><line x1="3" y1="6" x2="21" y2="6"/><line x1="3" y1="12" x2="21" y2="12"/><line x1="3" y1="18" x2="21" y2="18"/></svg>
<svg class="icon-x" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M18 6 6 18M6 6l12 12"/></svg>
</button>
</div>
</div>
<div class="nav-scrim" id="navScrim"></div>
</nav>
<!-- Hero -->
<div class="hero">
<div class="hero-title">
<span class="hero-title-badge" aria-hidden="true">
<svg viewBox="0 0 48 48" fill="none">
<path d="M8 16.5 18.5 24 8 31.5" stroke="currentColor" stroke-width="3.9" stroke-linecap="square" stroke-linejoin="miter" style="color: var(--accent);" />
<path d="M24 29.2h16" stroke="currentColor" stroke-width="3.9" stroke-linecap="square" style="color: var(--ink);" />
<path d="M26 12h14" stroke="url(#heroBadgeBeam)" stroke-width="2.6" stroke-linecap="square" opacity="0.78" />
<path d="M7 10.5h9" stroke="currentColor" stroke-width="1.8" stroke-linecap="square" style="color: var(--ink-3);" />
<path d="M7 37.5h11" stroke="currentColor" stroke-width="1.8" stroke-linecap="square" style="color: rgba(96,165,250,0.3);" />
<defs>
<linearGradient id="heroBadgeBeam" x1="26" y1="12" x2="40" y2="12" gradientUnits="userSpaceOnUse">
<stop stop-color="#60A5FA" stop-opacity="0" />
<stop offset="0.52" stop-color="#93C5FD" />
<stop offset="1" stop-color="#60A5FA" stop-opacity="0" />
</linearGradient>
</defs>
</svg>
</span>
<h1>
<span class="hero-word hero-word--cli">CLI-</span><span class="hero-word hero-word--anything">Anything</span> <span class="hero-word hero-word--hub">Hub</span>
</h1>
</div>
<p class="hero-tagline"><span class="hero-tagline-line hero-tagline-line--any">Any software. Any codebase. Any Web API.</span> <span class="hero-tagline-line hero-tagline-line--rest">Generate an agent-native CLI and let AI agents operate it &mdash; install with a single command.</span></p>
<div class="stats">
<div class="stat">
<div class="stat-num" id="cli-count">&mdash;</div>
<div class="stat-label">CLIs Available</div>
</div>
<div class="stat">
<div class="stat-num" id="category-count">&mdash;</div>
<div class="stat-label">Categories</div>
</div>
</div>
<div class="empower-row">
<div class="empower-card">
<h3>Empower your agents &mdash; <span>install in one command</span></h3>
<p class="empower-available">Also Available on: <a href="https://clawhub.ai/yuh-yang/cli-anything-hub" target="_blank">ClawHub</a>, <a href="https://www.skillhub.club/web/skills/itsyuhao-cli-anything-hub" target="_blank">SkillHub</a>, <a href="https://skillhub.cn/skills/cli-hub-meta-skill" target="_blank">SkillHub.cn</a></p>
<div class="install-row install-row--wrap">
<span class="chip-label">npx skills</span>
<code>npx skills add HKUDS/CLI-Anything --skill cli-hub-meta-skill -g -y</code>
<button class="copy-btn" onclick="copyCmd(this, 'npx skills add HKUDS/CLI-Anything --skill cli-hub-meta-skill -g -y')">Copy</button>
</div>
<p class="empower-note"><strong>Works with:</strong> OpenClaw, Nanobot, Claude Code, Codex, Antigravity, and other SKILL-compatible agents.</p>
<div class="empower-prompt">
<strong>Then prompt:</strong> <em>"Find appropriate CLI software in CLI-Hub and complete the task: ..."</em>
</div>
</div>
<div class="empower-card empower-self">
<h3>Empower yourself &mdash; <span>your CLI toolkit</span></h3>
<div class="install-row install-row--wrap">
<span class="chip-label">pip</span>
<code>pip install cli-anything-hub</code>
<button class="copy-btn" onclick="copyCmd(this, 'pip install cli-anything-hub')">Copy</button>
</div>
<div class="cmd-grid">
<div class="cmd-item"><span class="cmd-name">cli-hub list</span><span class="cmd-desc">Browse the registry</span></div>
<div class="cmd-item"><span class="cmd-name">cli-hub search</span><span class="cmd-desc">Search by keyword</span></div>
<div class="cmd-item"><span class="cmd-name">cli-hub install</span><span class="cmd-desc">Install from CLI-Hub</span></div>
<div class="cmd-item"><span class="cmd-name">cli-hub info</span><span class="cmd-desc">Inspect one CLI</span></div>
<div class="cmd-item"><span class="cmd-name">cli-hub update</span><span class="cmd-desc">Update an install</span></div>
<div class="cmd-item"><span class="cmd-name">cli-hub uninstall</span><span class="cmd-desc">Remove an install</span></div>
</div>
</div>
</div>
<div class="hero-actions">
<a class="btn-primary" href="https://reeceyang.sgp1.cdn.digitaloceanspaces.com/SKILL.md" target="_blank">Agent SKILL (SKILL.md)</a>
<a class="btn-secondary" href="docs.html">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M4 4.8A1.8 1.8 0 0 1 5.8 3H12v17H5.8A1.8 1.8 0 0 0 4 21.8z"/><path d="M20 4.8A1.8 1.8 0 0 0 18.2 3H12v17h6.2A1.8 1.8 0 0 1 20 21.8z"/></svg>
Documentation
</a>
<a class="btn-secondary" href="https://github.com/HKUDS/CLI-Anything" target="_blank">
<svg viewBox="0 0 98 96" fill="currentColor" aria-hidden="true"><path fill-rule="evenodd" clip-rule="evenodd" d="M48.854 0C21.839 0 0 22 0 49.217c0 21.756 13.993 40.172 33.405 46.69 2.427.49 3.316-1.059 3.316-2.362 0-1.141-.08-5.052-.08-9.127-13.59 2.934-16.42-5.867-16.42-5.867-2.184-5.704-5.42-7.17-5.42-7.17-4.448-3.015.324-3.015.324-3.015 4.934.326 7.523 5.052 7.523 5.052 4.367 7.496 11.404 5.378 14.235 4.074.404-3.178 1.699-5.378 3.074-6.6C29.304 70.25 17.9 66.013 17.9 47.02c0-5.52 1.94-10.046 5.127-13.58-.486-1.302-2.264-6.437.486-13.34 0 0 4.206-1.302 13.59 5.216 3.963-1.14 8.17-1.628 12.34-1.628 4.17 0 8.376.568 12.34 1.628 9.384-6.518 13.59-5.216 13.59-5.216 2.75 6.903.972 12.038.486 13.34 3.268 3.534 5.127 8.06 5.127 13.58 0 19.074-11.485 23.15-22.328 24.29 1.78 1.548 3.316 4.481 3.316 9.126 0 6.6-.08 11.897-.08 13.526 0 1.304.89 2.853 3.316 2.364 19.412-6.52 33.405-24.935 33.405-46.691C97.707 22 75.788 0 48.854 0z"/></svg>
Star on GitHub
</a>
<a class="btn-secondary" href="https://github.com/HKUDS/CLI-Anything/blob/main/CONTRIBUTING.md" target="_blank">
Contributing Guide
</a>
<a class="btn-secondary" href="https://github.com/HKUDS/CLI-Anything/blob/main/.github/PULL_REQUEST_TEMPLATE.md" target="_blank">
PR Template
</a>
</div>
<p class="hero-hint">Feed <a href="https://reeceyang.sgp1.cdn.digitaloceanspaces.com/SKILL.md" target="_blank">SKILL.md</a> to your AI agent for autonomous CLI discovery &amp; installation</p>
<p class="hero-cta">We welcome contributions for <em>any</em> application &mdash; desktop apps, dev tools, cloud services, SaaS APIs, creative suites, and beyond. If it has a GUI or an API, it deserves an agent-native CLI. Read the <a href="https://github.com/HKUDS/CLI-Anything/blob/main/CONTRIBUTING.md" target="_blank">Contributing Guide</a>, use the <a href="https://github.com/HKUDS/CLI-Anything/blob/main/.github/PULL_REQUEST_TEMPLATE.md" target="_blank">PR Template</a>, and bring it <span class="nowrap">to the hub!</span></p>
</div>
<!-- Real-World Demos — homepage teaser (full Editorial gallery lives on demos.html) -->
<section class="demos-teaser" id="demos">
<div class="dt-head">
<div>
<div class="kl">See it work</div>
<h2>Real-world demos</h2>
<p>Agents driving generated CLIs to finished artifacts &mdash; CAD models, 3D scenes, diagrams, gameplay, captioned video, and live GIS. No GUI needed.</p>
</div>
<a class="dt-seeall" href="demos.html">See all demos
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M5 12h14"/><path d="m12 5 7 7-7 7"/></svg>
</a>
</div>
<div class="demos-strip">
<a class="dposter" href="demos.html#freecad" style="--ch:210">
<div class="dt-media">
<div class="dt-chrome"><span class="dts"><i></i><i></i><i></i></span><span class="u">cli-anything-freecad · preview live</span><span class="lv"><i></i>live</span></div>
<video class="lazy-vid" muted loop playsinline preload="none" poster="assets/demos/freecad-curiosity-preview-trajectory-poster.jpg" src="assets/demos/freecad-curiosity-preview-trajectory.mp4" aria-label="FreeCAD Curiosity rover demo"></video>
</div>
<span class="dt-cat"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M12 2 3 7v10l9 5 9-5V7z"/><path d="M3 7l9 5 9-5"/><path d="M12 12v10"/></svg>3D / CAD</span>
<h3><span class="ap">FreeCAD</span> — Curiosity Rover</h3>
<p class="dt-blurb">An agent assembles a Curiosity-inspired rover while publishing real preview bundles and recording a replayable trajectory.</p>
<div class="dt-by"><span class="dt-av">CA</span><span><b>Core team</b> · CLI-Anything</span></div>
</a>
<a class="dposter" href="demos.html#blender" style="--ch:28">
<div class="dt-media">
<div class="dt-chrome"><span class="dts"><i></i><i></i><i></i></span><span class="u">cli-anything-blender · preview live</span><span class="lv"><i></i>live</span></div>
<video class="lazy-vid" muted loop playsinline preload="none" poster="assets/demos/blender-orbital-relay-drone-preview-trajectory-poster.jpg" src="assets/demos/blender-orbital-relay-drone-preview-trajectory.mp4" aria-label="Blender orbital relay drone demo"></video>
</div>
<span class="dt-cat"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M12 2 3 7v10l9 5 9-5V7z"/><path d="M3 7l9 5 9-5"/><path d="M12 12v10"/></svg>3D · Hard-surface</span>
<h3><span class="ap">Blender</span> — Orbital Relay Drone</h3>
<p class="dt-blurb">A hard-surface drone grows under a real preview loop, each stage pushing render-backed bundles tied to the trajectory.</p>
<div class="dt-by"><span class="dt-av">CA</span><span><b>Core team</b> · CLI-Anything</span></div>
</a>
<a class="dposter" href="demos.html#drawio" style="--ch:250">
<div class="dt-media">
<div class="dt-chrome"><span class="dts"><i></i><i></i><i></i></span><span class="u">cli-anything-drawio · build</span><span class="lv"><i></i>live</span></div>
<video class="lazy-vid" muted loop playsinline preload="none" poster="assets/demos/drawio-demo-poster.jpg" src="assets/demos/drawio-demo.mp4" aria-label="Draw.io HTTPS handshake diagram demo"></video>
</div>
<span class="dt-cat"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="7" height="5" rx="1"/><rect x="14" y="16" width="7" height="5" rx="1"/><path d="M6.5 8v4a2 2 0 0 0 2 2h9"/></svg>Diagramming</span>
<h3><span class="ap">Draw.io</span> — HTTPS Handshake</h3>
<p class="dt-blurb">A full HTTPS lifecycle diagram built from scratch — TCP handshake, TLS negotiation, and termination — entirely via CLI.</p>
<div class="dt-by"><span class="dt-av">ZH</span><span>Contributed by <b>@zhangxilong-43</b></span></div>
</a>
<a class="dposter-end" href="demos.html">
<span class="ico" aria-hidden="true"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M5 12h14"/><path d="m12 5 7 7-7 7"/></svg></span>
<span class="go">See all demos</span>
<span class="tx">Explore the full gallery</span>
</a>
</div>
</section>
<div class="marketplace-prelude">
<div class="typing-shell" data-typing-text="pip install cli-anything-hub">
<div class="typing-shell-head">
<span class="typing-shell-label">install first, then browse the marketplace</span>
<button class="copy-btn typing-shell-copy" onclick="copyCmd(this, 'pip install cli-anything-hub')">Copy</button>
</div>
<div class="typing-shell-body" aria-label="pip install cli-anything-hub">
<span class="typing-shell-prompt">$</span>
<code class="typing-shell-code"><span class="typing-shell-text" data-typing-target></span><span class="typing-shell-cursor" aria-hidden="true"></span></code>
</div>
</div>
</div>
<!-- Matrices -->
<section class="matrices" id="matrices">
<div class="matrices-head">
<div>
<div class="klabel">The collections</div>
<h2>Matrices</h2>
<p>Each matrix is a capability bundle &mdash; a curated set of intents mapped to provider CLIs from the catalog below. Flip a card for its capabilities, then scope the catalog to just its tools.</p>
</div>
<div class="mat-stat" id="mat-stat"></div>
</div>
<div class="shelf" id="shelf">
<div class="skeleton"></div><div class="skeleton"></div><div class="skeleton"></div>
</div>
</section>
<!-- Catalog scope banner (set by matrix "show only these tools") -->
<div class="scope-banner" id="scopeBanner" role="status">
<span class="lbl">Scoped to <b id="scopeName">a matrix</b> &mdash; showing <b id="scopeCount">0</b> tools.</span>
<button class="clear" id="scopeClear" type="button">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" width="14" height="14"><path d="M18 6 6 18M6 6l12 12"/></svg>
Show all tools
</button>
</div>
<!-- Deck nav -->
<div class="deck-nav">
<button class="deck-tab active" data-deck="harness" onclick="switchDeck('harness')">
CLI-Anything CLIs
<span class="deck-count" id="deck-count-harness">&mdash;</span>
</button>
<button class="deck-tab deck-tab--public" data-deck="public" onclick="switchDeck('public')">
Public CLIs
<span class="deck-count" id="deck-count-public">&mdash;</span>
</button>
<div class="deck-dots" role="tablist" aria-label="Switch deck">
<button class="deck-dot active" data-deck="harness" onclick="switchDeck('harness')" aria-label="Show CLI-Anything CLIs" title="CLI-Anything CLIs"></button>
<button class="deck-dot" data-deck="public" onclick="switchDeck('public')" aria-label="Show Public CLIs" title="Public CLIs"></button>
</div>
<span class="deck-separator"></span>
<span class="deck-hint" aria-hidden="true">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round"><path d="M8 9 4 12l4 3"/><path d="M16 9l4 3-4 3"/><path d="M4 12h16"/></svg>
<span>drag, scroll, or tap to switch</span>
</span>
<button class="deck-arrow-btn" id="deck-toggle" onclick="toggleDeck()">
<span>Public CLIs</span>
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="9 18 15 12 9 6"/></svg>
</button>
</div>
<!-- Deck viewport -->
<div class="deck-stage">
<div class="deck-viewport">
<div class="deck-track" id="deck-track" data-active="harness">
<!-- Harness deck -->
<div class="deck-panel deck-panel--harness" data-peek-label="CLI-Anything">
<div class="controls">
<input class="search" type="text" placeholder="Search CLIs..." id="search-harness">
<select class="sort-select" id="sort-harness">
<option value="updated">Last updated</option>
<option value="name">Name</option>
<option value="category">Category</option>
</select>
</div>
<div class="filter-row" id="filters-harness"></div>
<div class="grid" id="grid-harness">
<div class="skeleton"></div><div class="skeleton"></div><div class="skeleton"></div>
</div>
<div class="deck-sentinel" id="sentinel-harness" data-deck="harness" hidden aria-hidden="true"></div>
</div>
<!-- Public CLI deck -->
<div class="deck-panel deck-panel--public" data-peek-label="Public CLIs">
<div class="controls">
<input class="search" type="text" placeholder="Search public CLIs..." id="search-public">
<select class="sort-select" id="sort-public">
<option value="updated">Last updated</option>
<option value="name">Name</option>
<option value="category">Category</option>
</select>
</div>
<div class="public-panel-note">
Public CLIs remain the property of their respective software owners and maintainers. All rights stay with those owners; CLI-Hub only helps you discover and install them.
</div>
<div class="filter-row" id="filters-public"></div>
<div class="grid" id="grid-public"></div>
<div class="deck-sentinel" id="sentinel-public" data-deck="public" hidden aria-hidden="true"></div>
</div>
</div>
</div>
</div>
<!-- Footer -->
<footer class="footer">
<div class="footer-left">
Want to add or update a CLI? Read the <a href="https://github.com/HKUDS/CLI-Anything/blob/main/CONTRIBUTING.md" target="_blank">Contributing Guide</a> and use the <a href="https://github.com/HKUDS/CLI-Anything/blob/main/.github/PULL_REQUEST_TEMPLATE.md" target="_blank">PR Template</a>
</div>
<div class="footer-right">
Powered by <a href="https://github.com/HKUDS/CLI-Anything" target="_blank">CLI-Anything</a>
</div>
<div class="footer-stats">
<section class="analytics-report" aria-label="Traffic snapshot">
<span class="analytics-kicker">Traffic</span>
<span class="analytics-row">
<span class="analytics-title">Web</span>
<strong id="stat-web-unique" class="analytics-total">...</strong>
</span>
<span class="analytics-divider" aria-hidden="true"></span>
<span class="analytics-row">
<span class="analytics-title">CLI</span>
<strong id="stat-cli-total" class="analytics-total">...</strong>
<span class="analytics-pair"><strong id="stat-cli-human" class="human-value">...</strong>/<strong id="stat-cli-agent" class="agent-value">...</strong></span>
</span>
</section>
</div>
</footer>
<script>
// Forward declaration so palette init (which runs before the matrix block) can read it.
var STATE = {
byName: new Map(), // cli name -> catalog entry (with ._deck)
matrices: [],
membership: new Map(), // cli name -> [matrix, ...]
scope: null, // matrix name the catalog is scoped to
};
var MATRIX_URLS = [
'../../matrix_registry.json',
'https://raw.githubusercontent.com/HKUDS/CLI-Anything/main/matrix_registry.json'
];
// ── Palette system (tech · humanistic · aesthetic) ──
// Anchor tokens per palette × theme; glass/chip/shadow derive in CSS from these.
// accentBand = the palette's ACCENT hue; per-category tints spread ±~25° around it
// (catHue), so each palette keeps its own colour family (warm palettes → warm tints).
const PALETTES = {
steel: {
label: 'Steel', accentBand: 209,
sw: { accent: '#2f6fb0', grey: '#8595a8' },
light: { bg:'#f4f6f9', surface:'#ffffff', ink:'#14181f', ink2:'#4a5568', ink3:'#8595a8', line:'rgba(20,40,80,.10)', accent:'#2f6fb0', accent2:'#1f5793', accentRgb:'47,111,176', surfaceRgb:'255,255,255' },
dark: { bg:'#0d1117', surface:'#151b24', ink:'#e8edf4', ink2:'#9fb0c4', ink3:'#647488', line:'rgba(255,255,255,.08)', accent:'#5b9bd6', accent2:'#7db4e6', accentRgb:'91,155,214', surfaceRgb:'21,27,36' },
},
steelsky: {
label: 'Steel Sky', accentBand: 203,
sw: { accent: '#1c8ee0', grey: '#8595a8' },
light: { bg:'#f4f6f9', surface:'#ffffff', ink:'#14181f', ink2:'#4a5568', ink3:'#8595a8', line:'rgba(20,40,80,.10)', accent:'#1c8ee0', accent2:'#1573bd', accentRgb:'28,142,224', surfaceRgb:'255,255,255' },
dark: { bg:'#0d1117', surface:'#151b24', ink:'#e8edf4', ink2:'#9fb0c4', ink3:'#647488', line:'rgba(255,255,255,.08)', accent:'#46b2f4', accent2:'#6cc6f8', accentRgb:'70,178,244', surfaceRgb:'21,27,36' },
},
sky: {
label: 'Sky', accentBand: 203,
sw: { accent: '#1c8ee0', grey: '#8299ac' },
light: { bg:'#f4f9fe', surface:'#ffffff', ink:'#0f1f2d', ink2:'#465a6b', ink3:'#8299ac', line:'rgba(20,70,115,.10)', accent:'#1c8ee0', accent2:'#1573bd', accentRgb:'28,142,224', surfaceRgb:'255,255,255' },
dark: { bg:'#0a1320', surface:'#121e2d', ink:'#e9f3fb', ink2:'#9bb4c9', ink3:'#5f7689', line:'rgba(255,255,255,.08)', accent:'#46b2f4', accent2:'#6cc6f8', accentRgb:'70,178,244', surfaceRgb:'18,30,45' },
},
sage: {
label: 'Sage', accentBand: 160,
sw: { accent: '#2c9b78', grey: '#7f968a' },
light: { bg:'#f3f8f4', surface:'#ffffff', ink:'#13211b', ink2:'#475a51', ink3:'#7f968a', line:'rgba(20,65,48,.10)', accent:'#2c9b78', accent2:'#1d8062', accentRgb:'44,155,120', surfaceRgb:'255,255,255' },
dark: { bg:'#0b1410', surface:'#121d18', ink:'#e7f2eb', ink2:'#9ab4a7', ink3:'#5e7568', line:'rgba(255,255,255,.07)', accent:'#48c29c', accent2:'#67d0b1', accentRgb:'72,194,156', surfaceRgb:'18,29,24' },
},
sand: {
label: 'Sand', accentBand: 175,
sw: { accent: '#1d7a71', grey: '#948979' },
light: { bg:'#f8f5ef', surface:'#fffdf8', ink:'#221d15', ink2:'#585044', ink3:'#948979', line:'rgba(75,58,30,.12)', accent:'#1d7a71', accent2:'#145d57', accentRgb:'29,122,113', surfaceRgb:'255,253,248' },
dark: { bg:'#13110b', surface:'#1b1811', ink:'#f1ece1', ink2:'#b2a797', ink3:'#796e5e', line:'rgba(255,255,255,.07)', accent:'#3eb2a4', accent2:'#5cc3b6', accentRgb:'62,178,164', surfaceRgb:'27,24,17' },
},
blush: {
label: 'Blush', accentBand: 3,
sw: { accent: '#c25e5a', grey: '#9b8a87' },
light: { bg:'#faf6f4', surface:'#fffefd', ink:'#271d1c', ink2:'#5e504e', ink3:'#9b8a87', line:'rgba(90,50,45,.11)', accent:'#c25e5a', accent2:'#a4453f', accentRgb:'194,94,90', surfaceRgb:'255,254,253' },
dark: { bg:'#140f0e', surface:'#1c1615', ink:'#f3eae8', ink2:'#b6a4a1', ink3:'#7d6b68', line:'rgba(255,255,255,.07)', accent:'#df8480', accent2:'#e89f9b', accentRgb:'223,132,128', surfaceRgb:'28,22,21' },
},
mist: {
label: 'Mist', accentBand: 222,
sw: { accent: '#3b6ef0', grey: '#8893a6' },
light: { bg:'#f6f8fb', surface:'#ffffff', ink:'#171b22', ink2:'#4e586a', ink3:'#8893a6', line:'rgba(25,35,60,.09)', accent:'#3b6ef0', accent2:'#2f57cf', accentRgb:'59,110,240', surfaceRgb:'255,255,255' },
dark: { bg:'#0a0d14', surface:'#12161f', ink:'#eef1f7', ink2:'#9aa6bd', ink3:'#606b82', line:'rgba(255,255,255,.07)', accent:'#5b8bff', accent2:'#789fff', accentRgb:'91,139,255', surfaceRgb:'18,22,31' },
},
};
const PALETTE_ORDER = ['steel', 'steelsky', 'sky', 'sage', 'sand', 'blush', 'mist'];
const DEFAULT_PALETTE = 'steelsky';
function getPalette() {
const stored = localStorage.getItem('palette');
return (stored && PALETTES[stored]) ? stored : DEFAULT_PALETTE;
}
function applyPalette(name) {
const p = PALETTES[name] || PALETTES[DEFAULT_PALETTE];
const theme = document.documentElement.getAttribute('data-theme') || 'dark';
const t = (theme === 'light') ? p.light : p.dark;
const root = document.documentElement.style;
root.setProperty('--bg', t.bg);
root.setProperty('--surface', t.surface);
root.setProperty('--ink', t.ink);
root.setProperty('--ink-2', t.ink2);
root.setProperty('--ink-3', t.ink3);
root.setProperty('--line', t.line);
root.setProperty('--accent', t.accent);
root.setProperty('--accent2', t.accent2);
root.setProperty('--accent-rgb', t.accentRgb);
root.setProperty('--surface-rgb', t.surfaceRgb);
root.setProperty('--accent-band', String(p.accentBand));
root.setProperty('--hue-default', String(p.accentBand));
document.documentElement.setAttribute('data-palette', name);
syncPaletteUI(name);
if (typeof rerenderTints === 'function') rerenderTints();
}
function setPalette(name) {
if (!PALETTES[name]) return;
applyPalette(name);
localStorage.setItem('palette', name);
}
function syncPaletteUI(active) {
document.querySelectorAll('.palette-swatch, .pp-row').forEach(b => {
b.setAttribute('aria-pressed', String(b.dataset.palette === active));
});
}
function buildPaletteSwitcher() {
const row = document.getElementById('palette-switch');
const panel = document.getElementById('palette-pop-panel');
if (!row || !panel) return;
PALETTE_ORDER.forEach(key => {
const p = PALETTES[key];
const sw = document.createElement('button');
sw.type = 'button';
sw.className = 'palette-swatch';
sw.dataset.palette = key;
sw.style.setProperty('--sw-accent', p.sw.accent);
sw.style.setProperty('--sw-grey', p.sw.grey);
sw.setAttribute('aria-label', p.label + ' palette');
sw.title = p.label;
sw.addEventListener('click', () => setPalette(key));
row.appendChild(sw);
const pr = document.createElement('button');
pr.type = 'button';
pr.className = 'pp-row';
pr.dataset.palette = key;
pr.setAttribute('aria-label', p.label + ' palette');
const dot = document.createElement('span');
dot.className = 'pp-dot';
dot.style.setProperty('--sw-accent', p.sw.accent);
dot.style.setProperty('--sw-grey', p.sw.grey);
pr.appendChild(dot);
pr.appendChild(document.createTextNode(p.label));
pr.addEventListener('click', () => { setPalette(key); closePalettePop(); });
panel.appendChild(pr);
});
// popover toggle (small screens)
const popwrap = document.getElementById('palette-popwrap');
const popBtn = document.getElementById('palette-pop-btn');
if (popBtn && popwrap) {
popBtn.addEventListener('click', (e) => {
e.stopPropagation();
const open = popwrap.classList.toggle('open');
popBtn.setAttribute('aria-expanded', String(open));
});
document.addEventListener('click', (e) => { if (!popwrap.contains(e.target)) closePalettePop(); });
document.addEventListener('keydown', (e) => { if (e.key === 'Escape') closePalettePop(); });
}
syncPaletteUI(getPalette());
}
function closePalettePop() {
const popwrap = document.getElementById('palette-popwrap');
const popBtn = document.getElementById('palette-pop-btn');
if (popwrap) popwrap.classList.remove('open');
if (popBtn) popBtn.setAttribute('aria-expanded', 'false');
}
// ── Theme: system preference + localStorage override ──
function getPreferredTheme() {
const stored = localStorage.getItem('theme');
if (stored) return stored;
return window.matchMedia('(prefers-color-scheme: light)').matches ? 'light' : 'dark';
}
function applyTheme(theme) {
document.documentElement.setAttribute('data-theme', theme);
applyPalette(getPalette()); // re-resolve anchor tokens for the new theme
}
function toggleTheme() {
const current = document.documentElement.getAttribute('data-theme') || 'dark';
const next = current === 'dark' ? 'light' : 'dark';
applyTheme(next);
localStorage.setItem('theme', next);
}
applyTheme(getPreferredTheme());
buildPaletteSwitcher();
window.matchMedia('(prefers-color-scheme: light)').addEventListener('change', (e) => {
if (!localStorage.getItem('theme')) applyTheme(e.matches ? 'light' : 'dark');
});
// ── Nav scrolled state ──
(function () {
const nav = document.getElementById('nav');
const onScroll = () => nav.classList.toggle('scrolled', window.scrollY > 8);
window.addEventListener('scroll', onScroll, { passive: true }); onScroll();
})();
// ── Mobile nav drawer ──
(function () {
const toggle = document.getElementById('navToggle');
const menu = document.getElementById('navMenu');
const scrim = document.getElementById('navScrim');
if (!toggle || !menu) return;
function setOpen(open) {
menu.classList.toggle('open', open);
toggle.classList.toggle('open', open);
if (scrim) scrim.classList.toggle('open', open);
toggle.setAttribute('aria-expanded', String(open));
document.body.style.overflow = open ? 'hidden' : '';
}
toggle.addEventListener('click', () => setOpen(!menu.classList.contains('open')));
if (scrim) scrim.addEventListener('click', () => setOpen(false));
menu.addEventListener('click', (e) => { if (e.target.closest('a')) setOpen(false); });
document.addEventListener('keydown', (e) => { if (e.key === 'Escape') setOpen(false); });
window.addEventListener('resize', () => { if (window.innerWidth > 860) setOpen(false); });
})();
// ── Demo videos: play near the viewport, tear down far from it ──
// Demos ship as H.264 <video> (hardware-decoded, a few MB) instead of animated GIF
// (hundreds of MB of decoded frames — the iOS Safari tab-kill culprit). preload="none"
// keeps them free until scrolled near. Two rings: within 200px play/pause; beyond
// 1200px drop the src entirely so the media pipeline is fully released (poster stays).
(function () {
const vids = document.querySelectorAll('video.lazy-vid');
if (!vids.length) return;
const start = (v) => {
if (!v.getAttribute('src') && v.dataset.src) { v.setAttribute('src', v.dataset.src); v.load(); }
v.muted = true; const p = v.play(); if (p) p.catch(() => {});
};
if (!('IntersectionObserver' in window)) { vids.forEach(start); return; }
const near = new IntersectionObserver((entries) => {
entries.forEach((en) => {
if (en.isIntersecting) start(en.target);
else if (!en.target.paused) en.target.pause();
});
}, { rootMargin: '200px 0px' });
const far = new IntersectionObserver((entries) => {
entries.forEach((en) => {
const v = en.target;
if (!en.isIntersecting && v.getAttribute('src')) {
v.dataset.src = v.getAttribute('src');
if (!v.paused) v.pause();
v.removeAttribute('src'); v.load();
}
});
}, { rootMargin: '1200px 0px' });
vids.forEach(v => { near.observe(v); far.observe(v); });
})();
const REPO = 'https://github.com/HKUDS/CLI-Anything';
const GITHUB_REPO_API = 'https://api.github.com/repos/HKUDS/CLI-Anything';
const GITHUB_STARS_BADGE_API = 'https://img.shields.io/github/stars/HKUDS/CLI-Anything.json';
const REPO_SKILLS_SOURCE = 'HKUDS/CLI-Anything';
const REGISTRY_URLS = [
'../../registry.json',
'https://raw.githubusercontent.com/HKUDS/CLI-Anything/main/registry.json'
];
const PUBLIC_REGISTRY_URLS = [
'../../public_registry.json',
'https://raw.githubusercontent.com/HKUDS/CLI-Anything/main/public_registry.json'
];
const DATES_URLS = [
'https://hkuds.github.io/CLI-Anything/registry-dates.json',
'../../registry-dates.json'
];
const EXTERNAL_LINK_SVG = '<svg viewBox="0 0 16 16" fill="currentColor"><path d="M3.75 2h3.5a.75.75 0 0 1 0 1.5H4.56l6.72 6.72a.75.75 0 1 1-1.06 1.06L3.5 4.56v2.69a.75.75 0 0 1-1.5 0v-3.5A1.75 1.75 0 0 1 3.75 2Zm7.5 0h1A1.75 1.75 0 0 1 14 3.75v8.5A1.75 1.75 0 0 1 12.25 14h-8.5A1.75 1.75 0 0 1 2 12.25v-1a.75.75 0 0 1 1.5 0v1c0 .138.112.25.25.25h8.5a.25.25 0 0 0 .25-.25v-8.5a.25.25 0 0 0-.25-.25h-1a.75.75 0 0 1 0-1.5Z"/></svg>';
// Skill glyph — identical to the header nav SKILL link icon, for a unified design language.
const SKILL_ICON_SVG = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.85" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m5.5 18.5 9.8-9.8"/><path d="m13.7 6.9 3.4 3.4"/><path d="M18 4v2.4"/><path d="M16.8 5.2h2.4"/><path d="M7.5 4.8v2"/><path d="M6.5 5.8h2"/><path d="M18.2 15.8v2"/><path d="M17.2 16.8h2"/></svg>';
// ── Flip affordance glyphs ──
// FRONT → "Details" (chevron-right circle), BACK → "Back" (arrow-left). These deliberately
// avoid the old circular two-arrow icon, which read as "refresh/reload" rather than "flip".
const DETAILS_GLYPH = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="9"/><path d="m10.5 8.5 3.5 3.5-3.5 3.5"/></svg>';
const BACK_GLYPH = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M19 12H5"/><path d="m12 5-7 7 7 7"/></svg>';
// ===================== Software icon system (Simple Icons) =====================
// ICON_REGISTRY maps a CLI/software *name* (lowercased) → a Simple Icons slug.
// HOW TO ADD AN ICON:
// 1. Find the slug at https://simpleicons.org (e.g. "Krita" → "krita").
// 2. Add a lowercased-name → slug entry below (the catalog `name`, not display_name).
// 3. That's it — iconFor() renders https://cdn.simpleicons.org/<slug> and tints it.
// Resolver order in iconFor(): (1) entry.icon field if present (future-proofing),
// (2) ICON_REGISTRY[name], (3) a single-letter monogram fallback. A wrong/missing slug
// that 404s also degrades to the monogram via <img onerror>.
const ICON_REGISTRY = {
gimp: 'gimp', blender: 'blender', obs: 'obsstudio', 'obs-studio': 'obsstudio', obsstudio: 'obsstudio',
audacity: 'audacity', krita: 'krita', inkscape: 'inkscape', shotcut: 'shotcut', ffmpeg: 'ffmpeg',
imagemagick: 'imagemagick', darktable: 'darktable', kdenlive: 'kdenlive', davinci: 'davinciresolve',
'davinci-resolve': 'davinciresolve', handbrake: 'handbrake', vlc: 'vlcmediaplayer', mpv: 'mpv',
godot: 'godotengine', 'godot-engine': 'godotengine', godotengine: 'godotengine', unity: 'unity',
unrealengine: 'unrealengine', 'unreal-engine': 'unrealengine', aseprite: 'aseprite', musescore: 'musescore',
lmms: 'lmms', ardour: 'ardour', reaper: 'reaper',
github: 'github', gitlab: 'gitlab', git: 'git', slack: 'slack', discord: 'discord', telegram: 'telegram',
notion: 'notion', obsidian: 'obsidian', zoom: 'zoom', calibre: 'calibre', libreoffice: 'libreoffice',
figma: 'figma', miro: 'miro', trello: 'trello', asana: 'asana', jira: 'jira', linear: 'linear',
airtable: 'airtable', todoist: 'todoist', evernote: 'evernote', spotify: 'spotify', youtube: 'youtube',
vimeo: 'vimeo', twitch: 'twitch', canva: 'canva', adobe: 'adobe', photoshop: 'adobephotoshop',
premiere: 'adobepremierepro', illustrator: 'adobeillustrator', aftereffects: 'adobeaftereffects',
docker: 'docker', kubernetes: 'kubernetes', postman: 'postman', insomnia: 'insomnia', vscode: 'visualstudiocode',
'vs-code': 'visualstudiocode', vim: 'vim', neovim: 'neovim', emacs: 'gnuemacs', sublime: 'sublimetext',
jupyter: 'jupyter', anaconda: 'anaconda', python: 'python', nodejs: 'nodedotjs', npm: 'npm',
ghostscript: 'ghostscript', pandoc: 'pandoc', latex: 'latex', tableau: 'tableau', grafana: 'grafana',
wordpress: 'wordpress', shopify: 'shopify', stripe: 'stripe', mailchimp: 'mailchimp', hubspot: 'hubspot',
salesforce: 'salesforce', googledrive: 'googledrive', dropbox: 'dropbox', onedrive: 'microsoftonedrive',
gmail: 'gmail', googlecalendar: 'googlecalendar', googlesheets: 'googlesheets', googledocs: 'googledocs',
excalidraw: 'excalidraw', drawio: 'diagramsdotnet', openai: 'openai', anthropic: 'anthropic',
huggingface: 'huggingface', replicate: 'replicate', elevenlabs: 'elevenlabs',
// Chinese / APAC brands Simple Icons carries (others resolve via the favicon chain below)
wechat: 'wechat', weixin: 'wechat', qq: 'tencentqq', tencentqq: 'tencentqq',
bilibili: 'bilibili', weibo: 'sinaweibo', sinaweibo: 'sinaweibo', zhihu: 'zhihu',
baidu: 'baidu', alipay: 'alipay', dingtalk: 'dingtalk', tiktok: 'tiktok', douyin: 'tiktok',
xiaohongshu: 'xiaohongshu', taobao: 'taobao', tmall: 'tmall', gitee: 'gitee',
alibabacloud: 'alibabacloud', huawei: 'huawei', xiaomi: 'xiaomi',
};
// Synonyms & non-English names → a canonical key (used for BOTH the Simple Icons slug and
// the favicon-domain lookup). Powers 近义查询 (synonyms); paired with the containment scan
// in resolveIconKey() for 模糊查询 (fuzzy). Add an alias to widen what a CLI name can match.
const ICON_ALIASES = {
feishu: 'lark', lark: 'lark', larksuite: 'lark', '飞书': 'lark',
wecom: 'wecom', wework: 'wecom', wechatwork: 'wecom', weixinwork: 'wecom', '企业微信': 'wecom',
minimax: 'minimax', minimaxi: 'minimax',
wechat: 'wechat', weixin: 'wechat', '微信': 'wechat',
dingtalk: 'dingtalk', dingding: 'dingtalk', '钉钉': 'dingtalk',
alipay: 'alipay', '支付宝': 'alipay',
bilibili: 'bilibili', bili: 'bilibili', '哔哩哔哩': 'bilibili',
weibo: 'weibo', '微博': 'weibo', sinaweibo: 'weibo',
zhihu: 'zhihu', '知乎': 'zhihu', baidu: 'baidu', '百度': 'baidu',
taobao: 'taobao', '淘宝': 'taobao', tmall: 'tmall', '天猫': 'tmall',
douyin: 'douyin', '抖音': 'douyin', tiktok: 'tiktok',
kuaishou: 'kuaishou', '快手': 'kuaishou', meituan: 'meituan', '美团': 'meituan',
amap: 'amap', gaode: 'amap', '高德': 'amap', netease: 'netease', '网易': 'netease',
neteasemusic: 'neteasemusic', qqmusic: 'qqmusic', yuque: 'yuque', '语雀': 'yuque',
xiaohongshu: 'xiaohongshu', rednote: 'xiaohongshu', '小红书': 'xiaohongshu',
qq: 'qq', tencent: 'tencent',
};
// Curated real-logo favicon domains (authoritative — beats a generic homepage such as a
// github.com repo URL). Apps Simple Icons lacks still get a real colour logo from here.
const ICON_DOMAINS = {
lark: 'larksuite.com', wecom: 'work.weixin.qq.com', minimax: 'minimaxi.com',
wechat: 'weixin.qq.com', dingtalk: 'dingtalk.com', alipay: 'alipay.com',
bilibili: 'bilibili.com', weibo: 'weibo.com', zhihu: 'zhihu.com', baidu: 'baidu.com',
taobao: 'taobao.com', tmall: 'tmall.com', douyin: 'douyin.com', tiktok: 'tiktok.com',
kuaishou: 'kuaishou.com', meituan: 'meituan.com', amap: 'amap.com', netease: '163.com',
neteasemusic: 'music.163.com', qqmusic: 'y.qq.com', yuque: 'yuque.com',
xiaohongshu: 'xiaohongshu.com', qq: 'qq.com', tencent: 'tencent.com',
};
// Generic code/registry hosts we never use as a favicon source (they'd brand every app the
// same). When an entry's homepage is one of these, we skip straight to the monogram.
const ICON_GENERIC_HOSTS = new Set(['github.com', 'gitlab.com', 'bitbucket.org', 'npmjs.com', 'pypi.org', 'gitee.com', 'sourceforge.net', 'readthedocs.io', 'readthedocs.org', 'codeberg.org']);
function iconNormName(s) {
return String(s || '').toLowerCase().trim()
.replace(/[\s_]*cli$/, '').replace(/^cli[\s_-]*/, '').replace(/[\s_\-./]+/g, '');
}
function iconDomainFromUrl(u) { try { return new URL(u).hostname.replace(/^www\./, ''); } catch (_) { return ''; } }
// Multi-source <img> fallback: walk data-fb (pipe-separated URLs) before degrading to a monogram.
window.iconFallback = function (img) {
const fbs = (img.getAttribute('data-fb') || '').split('|').filter(Boolean);
if (fbs.length) { const next = fbs.shift(); img.setAttribute('data-fb', fbs.join('|')); if (next.indexOf('simpleicons') === -1) img.classList.add('raw'); img.src = next; return; }
const span = document.createElement('span'); span.className = 'icon-mono';
span.textContent = img.getAttribute('data-letter') || '?';
img.replaceWith(span);
};
// Cross-reference resolver: alias (synonym) → exact key → containment (fuzzy) scan.
function resolveIconKey(name) {
const n = iconNormName(name);
if (!n) return '';
if (ICON_ALIASES[n]) return ICON_ALIASES[n];
if (ICON_REGISTRY[n] || ICON_DOMAINS[n]) return n;
const keys = Object.keys(ICON_ALIASES).concat(Object.keys(ICON_REGISTRY), Object.keys(ICON_DOMAINS));
for (const k of keys) { if (k.length >= 3 && (n.includes(k) || k.includes(n))) return ICON_ALIASES[k] || k; }
return n;
}
// Tasteful single-letter monogram fallback (one clean letter, palette-tinted tile).
function iconMono(label) {
const ch = (String(label || '?').trim()[0] || '?').toUpperCase();
return `<span class="icon-mono">${esc(ch)}</span>`;
}
// Resolve a CLI/catalog entry (or bare name string) to a tinted Simple Icons <img>, or
// the monogram fallback. extraClass lets callers reuse the tile at different sizes.
function iconFor(cli, extraClass) {
const obj = (cli && typeof cli === 'object') ? cli : null;
const label = obj ? (obj.display_name || obj.name) : String(cli || '');
const rawName = String(obj ? obj.name : cli || '');
const cls = 'sw-icon' + (extraClass ? ' ' + extraClass : '');
const letter = (String(label || '?').trim()[0] || '?').toUpperCase();
const key = resolveIconKey(rawName);
// (1) explicit slug on the entry, then (2) Simple Icons by resolved key or raw name.
let slug = obj && typeof obj.icon === 'string' && obj.icon.trim() ? obj.icon.trim().toLowerCase() : '';
if (!slug) slug = ICON_REGISTRY[key] || ICON_REGISTRY[iconNormName(rawName)] || '';
// (3) real-logo favicon domain: curated first, else the entry's own homepage (skip code hosts).
let domain = ICON_DOMAINS[key] || '';
if (!domain && obj) { const hp = iconDomainFromUrl(obj.homepage || obj.source_url || ''); if (hp && !ICON_GENERIC_HOSTS.has(hp)) domain = hp; }
// Ordered source chain → monogram. Simple Icons (clean monochrome) first; then live
// favicons widen coverage to apps it lacks (esp. Chinese: Lark, WeCom, MiniMax, DingTalk…).
const srcs = [];
if (slug) srcs.push(`https://cdn.simpleicons.org/${esc(slug)}`);
if (domain) {
srcs.push(`https://icons.duckduckgo.com/ip3/${esc(domain)}.ico`);
srcs.push(`https://www.google.com/s2/favicons?domain=${esc(domain)}&sz=64`);
}
if (!srcs.length) return `<span class="${cls}" title="${escAttr(label)}">${iconMono(label)}</span>`;
const first = srcs.shift();
const rawCls = first.indexOf('simpleicons') === -1 ? ' class="raw"' : '';
return `<span class="${cls}" title="${escAttr(label)}"><img${rawCls} loading="lazy" decoding="async" ` +
`src="${first}" data-fb="${escAttr(srcs.join('|'))}" data-letter="${escAttr(letter)}" ` +
`alt="${escAttr(label)} icon" onerror="iconFallback(this)"></span>`;
}
// Per-category tints follow the ACTIVE palette's accent hue (catHue centres on the
// palette accent band and spreads each category a small signed offset around it), so the
// whole page reads as one colour family that recolours with the palette: warm palettes
// (Sand, Blush) get warm tints, cool palettes (Steel, Sky, Mist) stay cool, Sage stays
// green. Saturation/lightness are fixed in CSS (legible in both themes); we only vary hue.
const TINT_SPREAD = 25; // categories fan ±~25° around the palette accent hue
// ordered offsets in [-1, 1]; scaled by TINT_SPREAD so the spread tracks the band
const CATEGORY_OFFSET = {
video: -0.30, knowledge: 0, '3d': 0.58, game: -0.74, image: 1.0,
ai: 0.30, audio: -0.52, dev: 0.15, design: 0.86, productivity: -0.15,
creative: 0.72, web: 0, data: -0.38, science: 0.44, business: 0.22, other: 0
};
// hues wrap on the 0360 circle (Blush's ~3° accent fans into both reds and oranges)
function wrapHue(h) { return ((h % 360) + 360) % 360; }
function catHue(cat) {
const band = parseFloat(getComputedStyle(document.documentElement).getPropertyValue('--accent-band')) || 209;
const key = String(cat || 'other');
let norm;
if (Object.prototype.hasOwnProperty.call(CATEGORY_OFFSET, key)) {
norm = CATEGORY_OFFSET[key];
} else {
// deterministic offset in [-1, 1] for any unlisted category
let h = 0; for (let i = 0; i < key.length; i++) h = (h * 31 + key.charCodeAt(i)) % 101;
norm = (h - 50) / 50;
}
return wrapHue(Math.round(band + norm * TINT_SPREAD));
}
// Recompute per-category --ch in place whenever the palette (accent band) changes,
// so tints re-bias toward the new accent without losing card/scroll/flip state.
function rerenderTints() {
document.querySelectorAll('[data-cat]').forEach(el => {
el.style.setProperty('--ch', String(catHue(el.dataset.cat)));
});
// scope banner follows the scoped matrix's hue
const sb = document.getElementById('scopeBanner');
if (sb && STATE.scope) {
const m = STATE.matrices.find(x => x.name === STATE.scope);
if (m) sb.style.setProperty('--ch', String(catHue(m.category)));
}
}
let harnessClis = [];
let publicClis = [];
let activeDeck = 'harness';
let deckSuspendTimer = null;
// Collapse heavy effects (glass blur, the 3D flip contexts, sticky-nav blur, grain) for the
// duration of ANY deck movement — drag or animated switch — so panels translate as flat,
// GPU-cached layers. This is what makes the slide smooth on Safari. Auto-clears 560ms after
// the last movement (covers the .5s settle transition); each move resets the timer.
function armDeckSuspend() {
document.documentElement.classList.add('deck-sliding');
clearTimeout(deckSuspendTimer);
deckSuspendTimer = setTimeout(() => document.documentElement.classList.remove('deck-sliding'), 560);
}
let wheelSwitchLockUntil = 0;
const deckGesture = { pointerId: null, startX: 0, deltaX: 0, dragging: false };
const deckState = {
harness: { filter: 'all', sort: 'updated', query: '' },
public: { filter: 'all', sort: 'updated', query: '' },
};
async function fetchJson(urls) {
for (const url of urls) {
try { const resp = await fetch(url); if (resp.ok) return await resp.json(); }
catch (_) { continue; }
}
return null;
}
function formatCompactCount(value) {
return new Intl.NumberFormat('en', {
notation: 'compact', maximumFractionDigits: value >= 10000 ? 0 : 1,
}).format(value);
}
function formatBadgeCount(value) {
if (typeof value !== 'string') return null;
const label = value.trim();
return label ? label.toUpperCase() : null;
}
function renderGitHubStars(label, title) {
document.querySelectorAll('[data-github-stars]').forEach(el => {
el.textContent = label; el.setAttribute('aria-label', title); el.title = title;
});
}
async function loadGitHubStars() {
try {
try {
const badgeResp = await fetch(GITHUB_STARS_BADGE_API);
if (badgeResp.ok) {
const badge = await badgeResp.json();
const badgeLabel = formatBadgeCount(badge.message || badge.value);
if (badgeLabel) { renderGitHubStars(badgeLabel, `${badgeLabel} GitHub stars`); return; }
}
} catch (_) {}
const resp = await fetch(GITHUB_REPO_API, { headers: { 'Accept': 'application/vnd.github+json' } });
if (!resp.ok) return;
const repo = await resp.json();
const stars = repo.stargazers_count;
if (typeof stars !== 'number') return;
renderGitHubStars(formatCompactCount(stars), `${stars.toLocaleString('en-US')} GitHub stars`);
} catch (_) {}
}
async function loadRegistries() {
try {
const [harnessData, publicData, dates, matrixData] = await Promise.all([
fetchJson(REGISTRY_URLS), fetchJson(PUBLIC_REGISTRY_URLS), fetchJson(DATES_URLS), fetchJson(MATRIX_URLS),
]);
if (!harnessData) throw new Error('Registry load failed');
harnessClis = harnessData.clis;
if (dates) harnessClis.forEach(c => c.last_modified = dates[c.name]);
if (publicData && publicData.clis) {
publicClis = publicData.clis;
if (dates) publicClis.forEach(c => c.last_modified = dates[c.name]);
}
const totalClis = harnessClis.length + publicClis.length;
const allCategories = [...new Set([...harnessClis, ...publicClis].map(c => c.category))];
document.getElementById('deck-count-harness').textContent = harnessClis.length;
document.getElementById('deck-count-public').textContent = publicClis.length;
const harnessCats = [...new Set(harnessClis.map(c => c.category))];
buildFilters('harness', harnessCats);
const publicCats = [...new Set(publicClis.map(c => c.category))];
buildFilters('public', publicCats);
// Build name -> entry map for the unification join (harness wins on name clash)
STATE.byName.clear();
publicClis.forEach(c => { c._deck = 'public'; STATE.byName.set(c.name, c); });
harnessClis.forEach(c => { c._deck = 'harness'; STATE.byName.set(c.name, c); });
// Ingest matrices BEFORE the first deck render so membership chips land in the one
// and only render. (The old flow rendered both decks, then re-rendered BOTH again
// when the matrix registry arrived — doubling the heaviest work on the page, right
// under the hero counter animation.)
ingestMatrices(matrixData);
renderDeck('harness');
// Start the hero counters only after the visible deck has rendered AND painted
// (double rAF) — started before, their animation frames were starved by the big
// render and the count visibly stuttered.
requestAnimationFrame(() => requestAnimationFrame(() => {
animateCount(document.getElementById('cli-count'), totalClis, 980);
animateCount(document.getElementById('category-count'), allCategories.length, 860);
}));
// The public deck sits on the offscreen carousel panel and the matrix shelf is far
// below the fold — render them after the counters finish rather than competing with
// the animation for the main thread. switchDeck() renders on demand if the user
// swipes over first.
setTimeout(renderDeferredSections, 1100);
} catch (e) {
document.getElementById('grid-harness').innerHTML =
'<div class="empty">Failed to load registry. <a href="' + REPO + '/blob/main/registry.json" style="color:var(--accent)">View source</a></div>';
}
}
// ── Matrices (the unification): build membership from the already-fetched registry ──
function ingestMatrices(data) {
if (!data || !Array.isArray(data.matrices) || !data.matrices.length) {
STATE.matrices = [];
return;
}
STATE.matrices = data.matrices;
STATE.membership.clear();
STATE.matrices.forEach(m => {
(m.clis || []).forEach(n => {
if (!STATE.membership.has(n)) STATE.membership.set(n, []);
STATE.membership.get(n).push(m);
});
});
const capCount = STATE.matrices.reduce((s, m) => s + ((m.capabilities && m.capabilities.length) || 0), 0);
const ms = document.getElementById('mat-stat');
if (ms) ms.textContent = STATE.matrices.length + ' matrices · ' + capCount + ' capabilities';
}
// Deferred second wave: the offscreen public deck + the below-the-fold matrix shelf.
let deferredSectionsDone = false;
function renderDeferredSections() {
if (deferredSectionsDone) return;
deferredSectionsDone = true;
renderDeck('public');
const shelf = document.getElementById('shelf');
if (!STATE.matrices.length) {
if (shelf) shelf.innerHTML = '<div class="empty">Could not load the matrix registry right now &mdash; the catalog below is unaffected. Try refreshing, or browse on <a href="' + REPO + '" target="_blank" rel="noopener">GitHub</a>.</div>';
const ms = document.getElementById('mat-stat'); if (ms) ms.textContent = '';
} else {
renderShelf();
}
syncFlipA11y();
}
const CATEGORY_LABELS = { 'ai': 'AI', '3d': '3D' };
function catLabel(cat) {
return CATEGORY_LABELS[cat] || cat.charAt(0).toUpperCase() + cat.slice(1);
}
function buildFilters(deck, categories) {
const container = document.getElementById('filters-' + deck);
container.innerHTML = '';
const allBtn = document.createElement('button');
allBtn.className = 'filter-btn active';
allBtn.textContent = 'All';
allBtn.dataset.cat = 'all';
allBtn.dataset.deck = deck;
allBtn.onclick = () => setFilter(deck, 'all');
container.appendChild(allBtn);
categories.sort().forEach(cat => {
const btn = document.createElement('button');
btn.className = 'filter-btn';
btn.textContent = catLabel(cat);
btn.dataset.cat = cat;
btn.dataset.deck = deck;
btn.onclick = () => setFilter(deck, cat);
container.appendChild(btn);
});
}
function setFilter(deck, cat) {
deckState[deck].filter = cat;
document.querySelectorAll(`#filters-${deck} .filter-btn`).forEach(b => {
b.classList.toggle('active', b.dataset.cat === cat);
});
renderDeck(deck);
}
function switchDeck(deck) {
// the public deck's first render is deferred; materialize it before sliding to it
if (deck === 'public') renderDeferredSections();
activeDeck = deck;
const track = document.getElementById('deck-track');
track.dataset.active = deck;
armDeckSuspend();
updateDeckPosition();
document.querySelectorAll('.deck-tab').forEach(t => {
t.classList.toggle('active', t.dataset.deck === deck);
});
document.querySelectorAll('.deck-dot').forEach(d => {
d.classList.toggle('active', d.dataset.deck === deck);
});
const toggle = document.getElementById('deck-toggle');
if (deck === 'harness') {
toggle.querySelector('span').textContent = 'Public CLIs';
toggle.querySelector('svg').innerHTML = '<polyline points="9 18 15 12 9 6"/>';
toggle.classList.remove('deck-arrow-btn--back');
} else {
toggle.querySelector('span').textContent = 'CLI-Anything CLIs';
toggle.querySelector('svg').innerHTML = '<polyline points="15 18 9 12 15 6"/>';
toggle.classList.add('deck-arrow-btn--back');
}
}
function toggleDeck() { switchDeck(activeDeck === 'harness' ? 'public' : 'harness'); }
function getDeckMetrics() {
const viewport = document.querySelector('.deck-viewport');
const track = document.getElementById('deck-track');
if (!viewport || !track) return null;
// Each panel is exactly one viewport wide, so the slide distance is the viewport width.
const panelWidth = viewport.clientWidth;
return { viewport, track, panelWidth, maxShift: panelWidth };
}
function setDeckTransform(offset = 0) {
const metrics = getDeckMetrics();
if (!metrics) return;
const { track, maxShift } = metrics;
const baseShift = activeDeck === 'public' ? -maxShift : 0;
const clampedShift = Math.min(0, Math.max(-maxShift, baseShift + offset));
track.style.transform = `translateX(${clampedShift}px)`;
}
function updateDeckPosition() { setDeckTransform(); }
function finishDeckDrag() {
const viewport = document.querySelector('.deck-viewport');
const track = document.getElementById('deck-track');
if (viewport) viewport.classList.remove('dragging');
if (track) track.classList.remove('is-dragging');
deckGesture.pointerId = null; deckGesture.startX = 0; deckGesture.deltaX = 0; deckGesture.dragging = false;
}
function initDeckGestures() {
const viewport = document.querySelector('.deck-viewport');
const track = document.getElementById('deck-track');
if (!viewport || !track) return;
const isInteractiveTarget = (target) => !!target.closest('a, button, input, select, textarea, label');
viewport.addEventListener('pointerdown', (e) => {
if (e.pointerType === 'mouse' && e.button !== 0) return;
if (isInteractiveTarget(e.target)) return;
deckGesture.pointerId = e.pointerId; deckGesture.startX = e.clientX; deckGesture.deltaX = 0; deckGesture.dragging = true;
viewport.classList.add('dragging'); track.classList.add('is-dragging');
if (viewport.setPointerCapture) viewport.setPointerCapture(e.pointerId);
});
viewport.addEventListener('pointermove', (e) => {
if (!deckGesture.dragging || e.pointerId !== deckGesture.pointerId) return;
deckGesture.deltaX = e.clientX - deckGesture.startX;
armDeckSuspend();
setDeckTransform(deckGesture.deltaX);
});
const endDrag = (e) => {
if (!deckGesture.dragging || e.pointerId !== deckGesture.pointerId) return;
const deltaX = deckGesture.deltaX;
finishDeckDrag();
if (activeDeck === 'harness' && deltaX < -60) { switchDeck('public'); return; }
if (activeDeck === 'public' && deltaX > 60) { switchDeck('harness'); return; }
updateDeckPosition();
};
viewport.addEventListener('pointerup', endDrag);
viewport.addEventListener('pointercancel', endDrag);
viewport.addEventListener('lostpointercapture', () => {
if (!deckGesture.dragging) return;
finishDeckDrag(); updateDeckPosition();
});
viewport.addEventListener('wheel', (e) => {
const now = Date.now();
if (now < wheelSwitchLockUntil) return;
if (Math.abs(e.deltaX) < 24 || Math.abs(e.deltaX) <= Math.abs(e.deltaY)) return;
e.preventDefault();
if (e.deltaX > 0 && activeDeck !== 'public') { switchDeck('public'); wheelSwitchLockUntil = now + 450; }
else if (e.deltaX < 0 && activeDeck !== 'harness') { switchDeck('harness'); wheelSwitchLockUntil = now + 450; }
}, { passive: false });
}
function renderDeck(deck) {
let clis = deck === 'harness' ? harnessClis : publicClis;
const state = deckState[deck];
const query = state.query.toLowerCase();
// matrix scoping: restrict to the scoped matrix's member CLIs
if (STATE.scope) {
const m = STATE.matrices.find(x => x.name === STATE.scope);
if (m) { const set = new Set(m.clis || []); clis = clis.filter(c => set.has(c.name)); }
}
const filtered = clis.filter(c => {
const matchFilter = state.filter === 'all' || c.category === state.filter;
const matchSearch = !query ||
c.display_name.toLowerCase().includes(query) ||
c.description.toLowerCase().includes(query) ||
c.name.toLowerCase().includes(query);
return matchFilter && matchSearch;
});
filtered.sort((a, b) => {
if (state.sort === 'updated') return (b.last_modified || '').localeCompare(a.last_modified || '');
if (state.sort === 'name') return a.display_name.localeCompare(b.display_name);
if (state.sort === 'category') return a.category.localeCompare(b.category) || a.display_name.localeCompare(b.display_name);
return 0;
});
const grid = document.getElementById('grid-' + deck);
if (filtered.length === 0) {
grid.innerHTML = '<div class="empty">No CLIs match your search.</div>';
state.fullList = []; state.shownCount = 0;
updateDeckSentinel(deck);
return;
}
// Phones (single-column) render in growing batches: a full 79-card render
// means ~28k px of live DOM, and laying out + rasterizing it during a fast
// fling is what killed the tab on iPhones. Desktop masonry keeps the full
// set so its shortest-column balancing stays exact.
state.fullList = filtered;
state.shownCount = Math.min(deckBatchLimit(grid), filtered.length);
const shown = filtered.slice(0, state.shownCount);
if (deck === 'harness') grid.innerHTML = shown.map(renderHarnessCard).join('');
else grid.innerHTML = shown.map(renderNpmCard).join('');
layoutMasonry(grid);
updateDeckSentinel(deck);
}
// ── Progressive deck batches (phones) ──
const DECK_BATCH_SIZE = 20;
function deckBatchLimit(grid) {
// Batch only in single-column layout and only when IO can grow the list.
return (deckSentinelIO && masonryColumnCount(grid) === 1) ? DECK_BATCH_SIZE : Infinity;
}
function appendDeckBatch(deck) {
const state = deckState[deck];
if (!state.fullList || state.shownCount >= state.fullList.length) return;
const grid = document.getElementById('grid-' + deck);
const next = state.fullList.slice(state.shownCount, state.shownCount + DECK_BATCH_SIZE);
state.shownCount += next.length;
grid.insertAdjacentHTML('beforeend', next.map(deck === 'harness' ? renderHarnessCard : renderNpmCard).join(''));
layoutMasonry(grid);
updateDeckSentinel(deck);
}
function updateDeckSentinel(deck) {
const s = document.getElementById('sentinel-' + deck);
if (!s || !deckSentinelIO) return;
const state = deckState[deck];
const more = !!(state.fullList && state.shownCount < state.fullList.length);
s.hidden = !more;
// Re-observe so IO re-evaluates even when the sentinel never left the
// margin band (otherwise a still-intersecting sentinel fires no new event).
deckSentinelIO.unobserve(s);
if (more) deckSentinelIO.observe(s);
}
const deckSentinelIO = ('IntersectionObserver' in window)
? new IntersectionObserver((entries) => {
entries.forEach((en) => { if (en.isIntersecting) appendDeckBatch(en.target.dataset.deck); });
}, { rootMargin: '1600px 0px' })
: null;
// ── Engine-independent JS masonry: shortest-column distribution ──
// Rebuilds the grid's .grid-col wrappers from the current set of .card elements and
// drops each next card into the currently shortest column. Idempotent / re-runnable.
function masonryColumnCount(grid) {
// Measure the grid's OWN usable width (capped by max-width), not the over-wide
// swipe-carousel panel it lives in. clientWidth already reflects the max-width cap.
const w = grid.clientWidth || grid.getBoundingClientRect().width || 0;
if (w >= 900) return 3;
if (w >= 600) return 2;
return 1;
}
function layoutMasonry(grid) {
if (!grid) return;
// Collect the current card set (whether loose or already inside columns).
const cards = Array.from(grid.querySelectorAll(':scope > .card, :scope > .grid-col > .card'));
if (!cards.length) return; // empty/error states render a bare .empty div — leave it
const cols = masonryColumnCount(grid);
// Rebuild column wrappers from scratch so the layout is fully re-runnable. The
// wrappers must be live in the DOM so offsetHeight reflects real laid-out height as
// we go, which is what makes the shortest-column placement correct.
grid.innerHTML = '';
const wrappers = [];
for (let i = 0; i < cols; i++) {
const col = document.createElement('div');
col.className = 'grid-col';
grid.appendChild(col);
wrappers.push(col);
}
// Place each card into the currently shortest column, preserving overall card order.
cards.forEach(card => {
let target = wrappers[0];
let min = wrappers[0].offsetHeight;
for (let i = 1; i < wrappers.length; i++) {
const h = wrappers[i].offsetHeight;
if (h < min) { min = h; target = wrappers[i]; }
}
target.appendChild(card);
});
}
function relayoutAllGrids() {
layoutMasonry(document.getElementById('grid-harness'));
layoutMasonry(document.getElementById('grid-public'));
}
function repoSkillId(skillPath) {
if (!skillPath || skillPath.startsWith('http')) return '';
const match = skillPath.match(/^skills\/([^/]+)\/SKILL\.md$/);
return match ? match[1] : '';
}
function isWebUrl(value) { return /^https?:\/\//i.test(value || ''); }
function isRepoSkillPath(value) { return !/\s/.test(value || '') && /(^\.{0,2}\/|\/|\.md$|\.txt$)/i.test(value || ''); }
function normalizeNpxSkillsCmd(cmd) {
if (!cmd) return '';
const trimmed = cmd.trim();
if (!trimmed.startsWith('npx skills add ')) return '';
return trimmed.replace(/\s+-g\b/g, '').replace(/\s+-y\b/g, '').trim() + ' -g -y';
}
function harnessSkillCmd(c) {
const skillId = repoSkillId(c.skill_md);
return skillId ? `npx skills add ${REPO_SKILLS_SOURCE} --skill ${skillId} -g -y` : '';
}
function publicSkillCmd(c) { return normalizeNpxSkillsCmd(c.skill_md); }
function renderSkillAction(value) {
if (!value) return '';
if (isWebUrl(value)) return `<a href="${esc(value)}" target="_blank">Skill</a>`;
if (isRepoSkillPath(value)) return `<a href="${REPO}/blob/main/${esc(value)}" target="_blank">Skill</a>`;
return `
<span class="skill-popover">
<button class="card-link-btn skill-trigger" type="button" aria-expanded="false" data-skill-value="${escAttr(value)}">Skill</button>
<span class="skill-popover-panel">
<span class="skill-popover-label">Skill install</span>
<code>${esc(value)}</code>
<button class="card-copy-btn skill-popover-copy" type="button" data-copy-value="${escAttr(value)}">Copy</button>
</span>
</span>`;
}
// FRONT-face Skill control: small icon+label consistent with the card visual language.
// Same three cases as renderSkillAction (web URL → link, repo skill path → link,
// npx popover → trigger + popover), with the header SKILL icon prepended.
function renderSkillFront(value) {
if (!value) return '';
if (isWebUrl(value)) return `<a class="card-skill-front" href="${esc(value)}" target="_blank">${SKILL_ICON_SVG}<span>Skill</span></a>`;
if (isRepoSkillPath(value)) return `<a class="card-skill-front" href="${REPO}/blob/main/${esc(value)}" target="_blank">${SKILL_ICON_SVG}<span>Skill</span></a>`;
return `
<span class="skill-popover">
<button class="card-skill-front skill-trigger" type="button" aria-expanded="false" data-skill-value="${escAttr(value)}">${SKILL_ICON_SVG}<span>Skill</span></button>
<span class="skill-popover-panel">
<span class="skill-popover-label">Skill install</span>
<code>${esc(value)}</code>
<button class="card-copy-btn skill-popover-copy" type="button" data-copy-value="${escAttr(value)}">Copy</button>
</span>
</span>`;
}
// Clean install box: just the command lines (NO inline "# step …" comments) + Copy-all.
// `label` is a short plain-language hint rendered ABOVE the box; `numbered` adds tiny
// 1·2 step markers in that label when there are two commands.
function renderInstallStack(steps, label, numbered) {
const cmds = steps.map(s => s.cmd);
const display = cmds.map(cmd => `<span class="install-command">${esc(cmd)}</span>`).join('');
const copyText = cmds.join('\n');
let head = '';
if (label) {
head = `<div class="install-steps-label">${esc(label)}</div>`;
} else if (numbered && cmds.length === 2) {
head = `<div class="install-steps-label"><span class="isl-num">1</span>Install the CLI<span class="isl-sep">·</span><span class="isl-num">2</span>add the skill</div>`;
}
return `<div class="install-steps">
${head}
<div class="card-install">
<code>${display}</code>
<button class="card-copy-btn" onclick='copyCmd(this, ${JSON.stringify(copyText)})'>Copy</button>
</div>
</div>`;
}
// ── 3D flip card shell ──
// front = important info (title, desc, category, version, primary install copy)
// back = full install command, requires, entry_point/direct cmd, source/skill links, contributors
function flipCardShell(c, hue, frontHTML, backHTML) {
const titleText = esc(c.display_name || c.name);
return `
<div class="card" id="tool-${cssId(c.name)}" style="--ch:${hue}" data-cat="${escAttr(c.category || 'other')}">
<div class="card-sizer" aria-hidden="true">
<div class="card-face card-front">${frontHTML}</div>
<div class="card-face card-back">${backHTML}</div>
</div>
<div class="card-inner">
<div class="card-face card-front">${frontHTML}</div>
<div class="card-face card-back" aria-hidden="true">${backHTML}</div>
</div>
</div>`;
}
function flipAffordance() {
return `<button class="flip-btn flip-toggle" type="button" aria-label="Show details, requirements and links" aria-pressed="false"><span>Details</span>${DETAILS_GLYPH}</button>`;
}
function flipBackBtn() {
return `<button class="flip-btn flip-btn--back flip-toggle" type="button" aria-label="Back to overview" aria-pressed="true">${BACK_GLYPH}<span>Back</span></button>`;
}
// ── Matrix card rendering (FLIP metaphor, lifted from Gallery D, re-themed) ──
const MATRIX_GLYPH = {
video: '<path d="m22 8-6 4 6 4V8z"/><rect x="2" y="6" width="14" height="12" rx="2"/>',
knowledge: '<path d="M4 19.5A2.5 2.5 0 0 1 6.5 17H20"/><path d="M6.5 2H20v20H6.5A2.5 2.5 0 0 1 4 19.5v-15A2.5 2.5 0 0 1 6.5 2z"/>',
'3d': '<path d="M12 2 2 7l10 5 10-5-10-5z"/><path d="m2 17 10 5 10-5M2 12l10 5 10-5"/>',
game: '<rect x="2" y="6" width="20" height="12" rx="4"/><path d="M6 12h4M8 10v4"/><circle cx="16" cy="11" r="1"/><circle cx="18.5" cy="13.5" r="1"/>',
image: '<rect x="3" y="3" width="18" height="18" rx="3"/><circle cx="9" cy="9" r="2"/><path d="m21 15-5-5L5 21"/>'
};
function matrixGlyph(cat) { return MATRIX_GLYPH[cat] || MATRIX_GLYPH.knowledge; }
const PK_LABEL = {
'agent-native': 'Agent-native', 'public-cli': 'Public CLI', 'harness-cli': 'Harness CLI',
'agent-skill': 'Agent skill', 'bundled-script': 'Bundled script', 'api': 'API',
'native': 'Native', 'python': 'Python', 'web-search': 'Web search'
};
const PK_KNOWN = { 'agentnative':1,'publiccli':1,'harnesscli':1,'agentskill':1,'bundledscript':1,'api':1,'native':1,'python':1,'websearch':1 };
function pkClass(k) {
const c = String(k || '').replace(/-/g, '');
return PK_KNOWN[c] ? ('pk-' + c) : 'pk-fallback';
}
function initials(s) {
const parts = String(s).replace(/[^A-Za-z0-9 ]/g, ' ').trim().split(/\s+/);
if (parts.length === 1) return parts[0].slice(0, 2);
return (parts[0][0] || '') + (parts[1][0] || '');
}
function firstSentence(s) {
if (!s) return '';
const i = s.indexOf('. ');
return (i > 0 && i < 90) ? s.slice(0, i + 1) : (s.length > 96 ? s.slice(0, 95) + '…' : s);
}
function matrixCmdRow(cmd) {
return `<div class="mcmd"><code><span class="pfx">$ </span>${esc(cmd)}</code>` +
`<button class="card-copy-btn" type="button" onclick='copyCmd(this, ${JSON.stringify(cmd)})'>Copy</button></div>`;
}
function renderShelf() {
const shelf = document.getElementById('shelf');
if (!shelf) return;
if (!STATE.matrices.length) { shelf.innerHTML = '<div class="empty">No matrices found in the registry.</div>'; return; }
shelf.innerHTML = STATE.matrices.map(matrixFlipCard).join('');
}
function matrixFlipCard(m) {
const hue = catHue(m.category);
const members = m.clis || [];
const caps = m.capabilities || [];
const install = 'cli-hub matrix install ' + m.name;
const preflight = 'cli-hub matrix preflight ' + m.name;
const glyph = `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linejoin="round" stroke-linecap="round">${matrixGlyph(m.category)}</svg>`;
// FRONT — cover: display_name, id chip, description, counts, member software icons, primary install copy
const memberIcons = members.slice(0, 6).map(n => {
const t = STATE.byName.get(n);
return iconFor(t || n, t ? '' : 'miss');
}).join('');
const moreN = members.length - 6;
const front = `
<div class="mcard-top">
<span class="mcard-glyph">${glyph}</span>
</div>
<h3>${esc(m.display_name || m.name)}</h3>
<p class="mdesc">${esc(m.description || '')}</p>
<div class="meta-row"><span><b>${caps.length}</b> capabilities</span><span><b>${members.length}</b> CLIs</span></div>
<div class="icon-stack">${memberIcons}${moreN > 0 ? `<span class="more">+${moreN}</span>` : ''}</div>
${matrixCmdRow(install)}
<div class="mcard-front-foot">
<span class="flip-hint">Flip: ${caps.length} capabilities, providers &amp; tools</span>
${flipAffordance()}
</div>`;
// BACK — full capability list, both commands, member chips, scope action (internally scrolling)
const capsHtml = caps.map(c => {
const io = []
.concat((c.inputs || []).map(x => `<span>${esc(x)}</span>`))
.concat((c.outputs || []).map(x => `<span class="out">${esc(x)} →</span>`))
.join('');
const provs = (c.providers || []).map(p => {
const meta = [];
if (p.cost_tier) meta.push(esc(p.cost_tier));
if (p.quality_tier) meta.push(esc(p.quality_tier) + ' quality');
if (p.offline) meta.push('offline');
if (p.requires) meta.push('needs ' + esc(p.requires));
return `<div class="prov">` +
`<span class="pk ${pkClass(p.kind)}">${esc(PK_LABEL[p.kind] || p.kind || 'other')}</span>` +
`<div class="pbody"><b>${esc(p.name || '')}</b>` +
(meta.length ? `<div class="pmeta">${meta.join(' · ')}</div>` : '') +
(p.notes ? `<div class="pnotes">${esc(p.notes)}</div>` : '') +
`</div></div>`;
}).join('');
return `<details class="cap">` +
`<summary><span class="cap-id">${esc(c.id || '')}</span><span class="cap-title">${esc(firstSentence(c.intent))}</span>` +
`<svg class="chev" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2"><path d="m9 6 6 6-6 6"/></svg></summary>` +
`<div class="cap-detail">` +
`<p class="cap-intent">${esc(c.intent || '')}</p>` +
(io ? `<div class="io">${io}</div>` : '') +
provs +
`</div></details>`;
}).join('');
const memberChips = members.map(n => {
const t = STATE.byName.get(n);
if (!t) return `<button class="miss" type="button" disabled>${esc(n)}<span class="badge-mini">unlisted</span></button>`;
const bk = t._deck === 'public' ? 'public' : 'harness';
return `<button type="button" data-goto="${escAttr(t.name)}">${esc(t.display_name || t.name)}<span class="badge-mini">${bk}</span></button>`;
}).join('');
const skillLink = m.skill_md
? `<a class="mbtn" href="${m.skill_md.startsWith('http') ? esc(m.skill_md) : `${REPO}/blob/main/${esc(m.skill_md)}`}" target="_blank" rel="noopener"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><path d="M14 2v6h6"/></svg>SKILL.md</a>`
: '';
const back = `
<div class="card-back-head">
<span class="card-back-title">${esc(m.display_name || m.name)} · ${esc(m.matrix_id || '')}</span>
${flipBackBtn()}
</div>
<div class="mback-scroll">
<div class="msub">Install &amp; preflight</div>
${matrixCmdRow(install)}
${matrixCmdRow(preflight)}
<div class="msub">${caps.length} capabilities · intent → providers</div>
${capsHtml || '<p style="color:var(--ink-3);font-size:.82rem">No capabilities listed.</p>'}
<div class="msub">${members.length} member CLIs <span style="font-weight:400;text-transform:none;letter-spacing:0;color:var(--ink-3)">— tap to jump into the catalog</span></div>
<div class="member">${memberChips}</div>
<div class="mback-cta">
<button class="mbtn primary" type="button" data-scope="${escAttr(m.name)}"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M3 6h18M7 12h10M11 18h2"/></svg>Show only these ${members.length} tools</button>
${skillLink}
</div>
</div>`;
return `
<div class="mcard" style="--ch:${hue}" data-cat="${escAttr(m.category || 'other')}" data-matrix="${escAttr(m.name)}">
<div class="card-sizer" aria-hidden="true">
<div class="card-face card-front">${front}</div>
<div class="card-face card-back">${back}</div>
</div>
<div class="card-inner">
<div class="card-face card-front">${front}</div>
<div class="card-face card-back" aria-hidden="true">${back}</div>
</div>
</div>`;
}
// open a matrix card (flip it + scroll into view) — used by catalog membership chips
function openMatrix(name) {
const card = document.querySelector('.mcard[data-matrix="' + cssEsc(name) + '"]');
if (!card) return;
document.getElementById('matrices').scrollIntoView({ behavior: 'smooth', block: 'start' });
requestAnimationFrame(() => setCardFlipped(card, true));
}
function cssEsc(s) { return String(s).replace(/["\\]/g, '\\$&'); }
function cssId(s) { return String(s).replace(/[^A-Za-z0-9_-]/g, '_'); }
// ── Catalog scoping + jump-to-tool (the bidirectional stitching) ──
function jumpToTool(name) {
// clear any scope/filter so the tool is guaranteed visible, switch to its deck
const t = STATE.byName.get(name);
clearScope(true);
const deck = (t && t._deck === 'public') ? 'public' : 'harness';
deckState.harness.filter = 'all'; deckState.public.filter = 'all';
deckState[deck].query = '';
const si = document.getElementById('search-' + deck); if (si) si.value = '';
setFilter(deck, 'all');
switchDeck(deck);
// phones render the deck in batches — materialize until the target exists
let guard = 0;
while (!document.getElementById('tool-' + cssId(name)) &&
deckState[deck].fullList && deckState[deck].shownCount < deckState[deck].fullList.length &&
guard++ < 50) {
appendDeckBatch(deck);
}
requestAnimationFrame(() => {
const el = document.getElementById('tool-' + cssId(name));
if (el) {
el.scrollIntoView({ behavior: 'smooth', block: 'center' });
const card = el.querySelector('.card-face') ? el : el;
el.classList.remove('flash'); void el.offsetWidth; el.classList.add('flash');
}
});
}
function applyScope(matName) {
const m = STATE.matrices.find(x => x.name === matName);
if (!m) return;
STATE.scope = matName;
// reset both decks' filters/search so every scoped member is visible
deckState.harness.filter = 'all'; deckState.harness.query = '';
deckState.public.filter = 'all'; deckState.public.query = '';
const sh = document.getElementById('search-harness'); if (sh) sh.value = '';
const sp = document.getElementById('search-public'); if (sp) sp.value = '';
setFilter('harness', 'all'); setFilter('public', 'all');
updateScopeBanner();
renderDeck('harness'); renderDeck('public');
syncFlipA11y();
// surface whichever deck holds more of the matrix's members
const set = new Set(m.clis || []);
const inHarness = harnessClis.filter(c => set.has(c.name)).length;
const inPublic = publicClis.filter(c => set.has(c.name)).length;
switchDeck(inPublic > inHarness ? 'public' : 'harness');
const nav = document.querySelector('.deck-nav');
if (nav) nav.scrollIntoView({ behavior: 'smooth', block: 'start' });
}
function clearScope(silent) {
STATE.scope = null;
updateScopeBanner();
if (!silent) { renderDeck('harness'); renderDeck('public'); syncFlipA11y(); }
}
function updateScopeBanner() {
const b = document.getElementById('scopeBanner');
if (!b) return;
if (STATE.scope) {
const m = STATE.matrices.find(x => x.name === STATE.scope);
if (!m) { b.classList.remove('on'); return; }
b.style.setProperty('--ch', String(catHue(m.category)));
const members = (m.clis || []).filter(n => STATE.byName.has(n)).length;
document.getElementById('scopeName').textContent = (m.display_name || m.name) + ' (' + (m.matrix_id || '') + ')';
document.getElementById('scopeCount').textContent = members;
b.classList.add('on');
} else {
b.classList.remove('on');
}
}
// delegated clicks for member chips / scope / membership chips
document.addEventListener('click', (e) => {
const goto = e.target.closest('[data-goto]');
if (goto && !goto.disabled) { jumpToTool(goto.getAttribute('data-goto')); return; }
const scopeBtn = e.target.closest('[data-scope]');
if (scopeBtn) { applyScope(scopeBtn.getAttribute('data-scope')); return; }
const inM = e.target.closest('[data-openmatrix]');
if (inM) { openMatrix(inM.getAttribute('data-openmatrix')); return; }
});
(function () {
const sc = document.getElementById('scopeClear');
if (sc) sc.addEventListener('click', () => clearScope(false));
})();
// membership chip ("In S1 · S4") — opens the matrix flip card; the core unification proof
function membershipChip(name) {
const mems = STATE.membership.get(name) || [];
if (!mems.length) return '';
const ids = mems.map(m => esc(m.matrix_id || '')).filter(Boolean).join(' · ');
const title = mems.length > 1
? ('In ' + mems.length + ' matrices — open ' + (mems[0].display_name || mems[0].name))
: ('In ' + (mems[0].display_name || mems[0].name) + ' — open it');
return `<button class="in-matrix" type="button" data-openmatrix="${escAttr(mems[0].name)}" title="${escAttr(title)}">In <span class="mid">${ids}</span></button>`;
}
function renderHarnessCard(c) {
const hue = catHue(c.category);
const memChip = membershipChip(c.name);
const requiresHtml = c.requires
? `<div class="card-requires"><strong>Requires</strong> ${esc(c.requires)}</div>` : '';
const skillFront = renderSkillFront(c.skill_md);
const sourceLink = c.source_url
? `<a href="${esc(c.source_url)}" target="_blank">Source</a>`
: `<a href="${REPO}/tree/main/${c.name}/agent-harness" target="_blank">Source</a>`;
const titleHtml = c.homepage
? `<a href="${esc(c.homepage)}" target="_blank">${esc(c.display_name)}${EXTERNAL_LINK_SVG}</a>`
: esc(c.display_name);
const titleRow = `<span class="card-title"><span class="card-title-row">${iconFor(c, 'title-icon')}<span>${titleHtml}</span></span></span>`;
const backTitleRow = `<span class="card-back-title">${esc(c.display_name || c.name)}</span>`;
const contributors = c.contributors || (c.contributor ? [{name: c.contributor, url: c.contributor_url}] : []);
const isTeam = contributors.length === 1 && contributors[0].name === 'CLI-Anything-Team';
const contributorHtml = contributors.length
? `<div class="card-contributor">
${contributors.map(ct => `<a href="${esc(ct.url)}" target="_blank">${esc(ct.name)}</a>`).join(', ')}${isTeam ? '' : '<span class="community-badge">Community</span>'}
</div>`
: '';
const versionHtml = c.version
? `<div class="card-back-field"><div class="card-back-label">Version</div><div class="card-requires">${esc(c.version)}</div></div>` : '';
const updatedHtml = c.last_modified
? `<div class="card-back-field"><div class="card-back-label">Updated</div><div class="card-requires">${esc(c.last_modified)}</div></div>` : '';
const runHtml = c.entry_point
? `<div class="card-back-field"><div class="card-back-label">Run directly</div><div class="card-install card-install--note"><code>${esc(c.entry_point)}</code></div></div>` : '';
// Two-step install (clean box: command lines only, NO inline comments).
const installSteps = [{ cmd: `cli-hub install ${c.name}` }];
const skillCmd = harnessSkillCmd(c);
if (skillCmd) installSteps.push({ cmd: skillCmd });
// FRONT: full two-line install with a short plain-language label above the box.
const frontInstall = installSteps.length === 2
? renderInstallStack(installSteps, null, true)
: renderInstallStack(installSteps, 'Install the CLI');
// FRONT (page 1): title · description · install box · contributor(s) · Details
const front = `
<div class="card-head">
${titleRow}
</div>
<div class="card-desc">${esc(c.description)}</div>
${memChip ? `<div class="tool-foot" style="display:flex;margin-top:11px">${memChip}</div>` : ''}
${frontInstall}
${(contributorHtml || skillFront) ? `<div style="margin-top:12px;display:flex;align-items:center;justify-content:space-between;gap:10px;flex-wrap:wrap">${contributorHtml || '<span></span>'}${skillFront}</div>` : ''}
<div class="card-front-foot">
<span class="flip-hint">Requirements, version &amp; source</span>
${flipAffordance()}
</div>`;
// BACK (page 2): requirements · version · run directly · source · secondary fields
const back = `
<div class="card-back-head">
${backTitleRow}
${flipBackBtn()}
</div>
<div class="card-back-scroll">
${requiresHtml ? `<div class="card-back-field">${requiresHtml}</div>` : ''}
${versionHtml}
${runHtml}
${updatedHtml}
<div class="card-back-field">
<div class="card-back-label">Links</div>
<div class="card-links">${sourceLink}</div>
</div>
</div>`;
return flipCardShell(c, hue, front, back);
}
function formatPublicManager(c) {
const manager = (c.package_manager || c.install_strategy || 'public').toLowerCase();
const labelMap = { npm: 'npm', brew: 'brew', bundled: 'bundled' };
const label = labelMap[manager] || manager;
return `<span class="public-badge public-badge--${esc(manager)}">${esc(label)}</span>`;
}
function publicDirectCmd(c) { return c.direct_cmd || c.entry_point || c.npx_cmd || ''; }
function renderNpmCard(c) {
const hue = catHue(c.category);
const memChip = membershipChip(c.name);
const isBundled = (c.install_strategy || c.package_manager) === 'bundled';
const requiresHtml = c.requires
? `<div class="card-requires"><strong>Requires</strong> ${esc(c.requires)}</div>` : '';
const titleHtml = c.homepage
? `<a href="${esc(c.homepage)}" target="_blank">${esc(c.display_name)}${EXTERNAL_LINK_SVG}</a>`
: esc(c.display_name);
const titleRow = `<span class="card-title"><span class="card-title-row">${iconFor(c, 'title-icon')}<span>${titleHtml}</span></span></span>`;
const backTitleRow = `<span class="card-back-title">${esc(c.display_name || c.name)}</span>`;
const skillFront = renderSkillFront(c.skill_md);
const sourceLink = c.source_url ? `<a href="${esc(c.source_url)}" target="_blank">Source</a>` : '';
const contributors = c.contributors || [];
const contributorHtml = contributors.length
? `<div class="card-contributor">
${contributors.map(ct => `<a href="${esc(ct.url)}" target="_blank">${esc(ct.name)}</a>`).join(', ')}
</div>`
: '';
const directCmd = c.install_cmd || publicDirectCmd(c);
const directCmdHtml = directCmd && !isBundled
? `<div class="card-npx card-npx--direct"><strong>Or run directly:</strong><span>${esc(directCmd)}</span></div>` : '';
const installNotesHtml = c.install_notes
? `<div class="card-npx card-npx--comment">${esc(c.install_notes)}</div>` : '';
const publicNotesHtml = (directCmdHtml || installNotesHtml)
? `<div class="card-public-notes">${directCmdHtml}${installNotesHtml}</div>` : '';
const updatedHtml = c.last_modified
? `<div class="card-back-field"><div class="card-back-label">Updated</div><div class="card-requires">${esc(c.last_modified)}</div></div>` : '';
// Clean install box — command lines only (no inline comments).
const installSteps = isBundled ? [] : [{ cmd: `cli-hub install ${c.name}` }];
const skillCmd = publicSkillCmd(c);
if (skillCmd) installSteps.push({ cmd: skillCmd });
let frontInstall;
if (isBundled) {
frontInstall = `<div class="install-steps"><div class="install-steps-label">Bundled with the upstream app — no install needed</div>` +
`<div class="card-install card-install--note"><code>Bundled with the upstream app</code></div></div>`;
} else if (installSteps.length === 2) {
frontInstall = renderInstallStack(installSteps, null, true);
} else {
frontInstall = renderInstallStack(installSteps, 'Install the CLI');
}
const linksHtml = [sourceLink].filter(Boolean).join(' &middot; ');
// FRONT (page 1): title (+ manager badge) · description · install box · contributor(s) · Details
const front = `
<div class="card-head">
${titleRow}
<div class="card-meta">
${formatPublicManager(c)}
<span class="category-tag">${catLabel(c.category)}</span>
</div>
</div>
<div class="card-desc">${esc(c.description)}</div>
${memChip ? `<div class="tool-foot" style="display:flex;margin-top:11px">${memChip}</div>` : ''}
${frontInstall}
${(contributorHtml || skillFront) ? `<div style="margin-top:12px;display:flex;align-items:center;justify-content:space-between;gap:10px;flex-wrap:wrap">${contributorHtml || '<span></span>'}${skillFront}</div>` : ''}
<div class="card-front-foot">
<span class="flip-hint">Requirements, run command &amp; source</span>
${flipAffordance()}
</div>`;
// BACK (page 2): requirements · run directly / notes · updated · source
const back = `
<div class="card-back-head">
${backTitleRow}
${flipBackBtn()}
</div>
<div class="card-back-scroll">
${requiresHtml ? `<div class="card-back-field">${requiresHtml}</div>` : ''}
${publicNotesHtml ? `<div class="card-back-field"><div class="card-back-label">Run &amp; notes</div>${publicNotesHtml}</div>` : ''}
${updatedHtml}
<div class="card-back-field">
<div class="card-back-label">Links</div>
<div class="card-links">${linksHtml}</div>
</div>
</div>`;
return flipCardShell(c, hue, front, back);
}
function copyCmd(btn, cmd) {
navigator.clipboard.writeText(cmd).then(() => {
btn.textContent = 'Copied!';
setTimeout(() => btn.textContent = 'Copy', 1500);
});
}
function animateCount(el, target, duration = 900) {
if (!el) return;
const finalValue = Number(target) || 0;
if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) { el.textContent = finalValue; return; }
const start = performance.now();
let shown = null;
function step(now) {
const progress = Math.min((now - start) / duration, 1);
const eased = 1 - Math.pow(1 - progress, 3);
const value = progress < 1 ? Math.round(finalValue * eased) : finalValue;
// write only when the displayed integer changes — most frames it doesn't
if (value !== shown) { shown = value; el.textContent = value; }
if (progress < 1) window.requestAnimationFrame(step);
}
window.requestAnimationFrame(step);
}
function initHeroTypingBar() {
const shell = document.querySelector('[data-typing-text]');
if (!shell) return;
const target = shell.querySelector('[data-typing-target]');
const fullText = shell.dataset.typingText || '';
if (!target || !fullText) return;
if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) { target.textContent = fullText; return; }
let index = 0, deleting = false;
function tick() {
target.textContent = fullText.slice(0, index);
let delay = deleting ? 44 : 86;
if (!deleting && index < fullText.length) { index += 1; }
else if (!deleting && index === fullText.length) { deleting = true; delay = 1650; }
else if (deleting && index > 0) { index -= 1; }
else { deleting = false; delay = 480; }
window.setTimeout(tick, delay);
}
window.setTimeout(tick, 420);
}
function esc(s) {
if (!s) return '';
const d = document.createElement('div');
d.textContent = s;
return d.innerHTML;
}
function escAttr(s) {
if (!s) return '';
return String(s).replace(/&/g, '&amp;').replace(/"/g, '&quot;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
}
// ── Flip interaction (delegated) ──
function setCardFlipped(card, flipped) {
// Cards are flat by default (see the .card CSS notes); grant this card its real 3D
// context just for the flip transition, then drop back to the cheap flat state.
card.classList.add('is-3d');
clearTimeout(card._flip3dTimer);
card._flip3dTimer = setTimeout(() => card.classList.remove('is-3d'), 780);
card.classList.toggle('flipped', flipped);
const inner = card.querySelector('.card-inner');
if (!inner) return;
const front = inner.querySelector('.card-front');
const back = inner.querySelector('.card-back');
if (front) front.toggleAttribute('aria-hidden', flipped);
if (back) back.toggleAttribute('aria-hidden', !flipped);
// toggle tabbability of the hidden face
const hidden = flipped ? front : back;
const shown = flipped ? back : front;
if (hidden) hidden.querySelectorAll('a,button,input,select,summary,[tabindex]').forEach(el => el.setAttribute('tabindex', '-1'));
if (shown) shown.querySelectorAll('a,button,input,select,summary,[tabindex]').forEach(el => el.removeAttribute('tabindex'));
// update flip-toggle aria-pressed on shown face
const tgl = (shown || inner).querySelector('.flip-toggle');
if (tgl) tgl.setAttribute('aria-pressed', String(flipped));
}
// initialise hidden-face tabbability after each render (CLI cards + matrix cards)
function syncFlipA11y(root) {
(root || document).querySelectorAll('.card, .mcard').forEach(card => {
const inner = card.querySelector('.card-inner');
if (!inner) return;
const back = inner.querySelector('.card-back');
if (back && !card.classList.contains('flipped')) {
back.setAttribute('aria-hidden', 'true');
back.querySelectorAll('a,button,input,select,summary,[tabindex]').forEach(el => el.setAttribute('tabindex', '-1'));
}
});
}
document.addEventListener('click', (e) => {
const tgl = e.target.closest('.flip-toggle');
if (tgl) {
const card = tgl.closest('.card, .mcard');
if (card) { setCardFlipped(card, !card.classList.contains('flipped')); }
return;
}
});
// Enter/Space when the flip affordance is focused (native button handles this,
// but ensure Space doesn't scroll and both keys flip reliably)
document.addEventListener('keydown', (e) => {
if ((e.key === 'Enter' || e.key === ' ' || e.key === 'Spacebar') && document.activeElement && document.activeElement.classList && document.activeElement.classList.contains('flip-toggle')) {
e.preventDefault();
const card = document.activeElement.closest('.card, .mcard');
if (card) setCardFlipped(card, !card.classList.contains('flipped'));
}
});
// ── Skill popover (public cards) ──
function toggleSkillPopover(trigger) {
const popover = trigger.closest('.skill-popover');
if (!popover) return;
const shouldOpen = !popover.classList.contains('is-open');
document.querySelectorAll('.skill-popover.is-open').forEach((node) => {
node.classList.remove('is-open');
const btn = node.querySelector('.skill-trigger');
if (btn) btn.setAttribute('aria-expanded', 'false');
});
if (!shouldOpen) return;
popover.classList.add('is-open');
trigger.setAttribute('aria-expanded', 'true');
}
document.addEventListener('click', (e) => {
const copyBtn = e.target.closest('.skill-popover-copy');
if (copyBtn) { e.preventDefault(); e.stopPropagation(); copyCmd(copyBtn, copyBtn.dataset.copyValue); return; }
const trigger = e.target.closest('.skill-trigger');
if (trigger) { e.preventDefault(); e.stopPropagation(); toggleSkillPopover(trigger); return; }
document.querySelectorAll('.skill-popover.is-open').forEach((node) => {
node.classList.remove('is-open');
const btn = node.querySelector('.skill-trigger');
if (btn) btn.setAttribute('aria-expanded', 'false');
});
});
document.addEventListener('keydown', (e) => {
if (e.key !== 'Escape') return;
document.querySelectorAll('.skill-popover.is-open').forEach((node) => {
node.classList.remove('is-open');
const btn = node.querySelector('.skill-trigger');
if (btn) btn.setAttribute('aria-expanded', 'false');
});
});
// Sync flip a11y whenever a grid is (re)rendered
(function () {
const obsTargets = ['grid-harness', 'grid-public', 'shelf'].map(id => document.getElementById(id)).filter(Boolean);
if ('MutationObserver' in window) {
const mo = new MutationObserver(() => syncFlipA11y());
obsTargets.forEach(t => mo.observe(t, { childList: true }));
}
})();
// Wire up search and sort for each deck
document.getElementById('search-harness').addEventListener('input', (e) => {
deckState.harness.query = e.target.value; renderDeck('harness');
});
document.getElementById('sort-harness').addEventListener('change', (e) => {
deckState.harness.sort = e.target.value; renderDeck('harness');
});
document.getElementById('search-public').addEventListener('input', (e) => {
deckState.public.query = e.target.value; renderDeck('public');
});
document.getElementById('sort-public').addEventListener('change', (e) => {
deckState.public.sort = e.target.value; renderDeck('public');
});
let masonryResizeTimer = null;
let masonryLastWidth = window.innerWidth;
window.addEventListener('resize', () => {
updateDeckPosition();
// iOS Safari fires resize when its toolbar collapses during scroll — a
// height-only change. Rebuilding both masonry grids mid-fling is a huge
// main-thread stall for a layout that cannot have changed (columns depend
// only on width), so bail unless the width actually moved.
if (window.innerWidth === masonryLastWidth) return;
masonryLastWidth = window.innerWidth;
// debounce the (re)layout: recompute column count + redistribute cards
clearTimeout(masonryResizeTimer);
masonryResizeTimer = setTimeout(relayoutAllGrids, 150);
});
initDeckGestures();
initHeroTypingBar();
loadRegistries().then(() => { updateDeckPosition(); syncFlipA11y(); });
// ── Live analytics via Cloudflare Worker proxy ──
(function () {
const ANALYTICS_PROXY_URL = 'https://analytics.clianything.cc/';
const ZERO_STATS = { web_unique_visitors: 0, cli_hub_total: 0, cli_hub_human: 0, cli_hub_agent: 0 };
function applyVisitorStats(stats) {
const formatK = (val) => {
const n = Number(val) || 0;
if (n < 1000) return String(n);
return (n / 1000).toFixed(1).replace(/\.0$/, '') + 'K';
};
const setText = (id, val) => { const el = document.getElementById(id); if (el) el.textContent = formatK(val); };
setText('stat-web-unique', stats.web_unique_visitors);
setText('stat-cli-total', stats.cli_hub_total);
setText('stat-cli-human', stats.cli_hub_human);
setText('stat-cli-agent', stats.cli_hub_agent);
}
function normalizeStats(stats) {
const cliHubHuman = Number(stats && stats.cli_hub_human) || 0;
const cliHubAgent = Number(stats && stats.cli_hub_agent) || 0;
return {
web_unique_visitors: Number(stats && (stats.web_unique_visitors ?? stats.total)) || 0,
cli_hub_total: Number(stats && stats.cli_hub_total) || cliHubHuman + cliHubAgent,
cli_hub_human: cliHubHuman,
cli_hub_agent: cliHubAgent,
};
}
async function fetchWorkerStats() {
const response = await fetch(ANALYTICS_PROXY_URL + 'stats');
if (!response.ok) throw new Error('analytics stats http ' + response.status);
const data = await response.json();
return normalizeStats(data.stats || data);
}
function hasAnyStats(s) { return Object.keys(ZERO_STATS).some((key) => (s[key] || 0) > 0); }
async function refreshVisitorStats() {
const stats = await fetchWorkerStats();
if (!hasAnyStats(stats)) return false;
applyVisitorStats(stats);
return true;
}
async function loadVisitorStats() { await refreshVisitorStats().catch(() => false); }
loadVisitorStats();
if (typeof loadGitHubStars === 'function') loadGitHubStars();
})();
</script>
</body>
</html>