diff --git a/.github/workflows/pages.yml b/.github/workflows/pages.yml new file mode 100644 index 0000000..4b88227 --- /dev/null +++ b/.github/workflows/pages.yml @@ -0,0 +1,35 @@ +name: pages + +on: + push: + branches: [main] + paths: [site/**, docs/assets/**, shelldeck/static/icon.svg, .github/workflows/pages.yml] + workflow_dispatch: + +permissions: + contents: read + pages: write + id-token: write + +concurrency: + group: pages + cancel-in-progress: true + +jobs: + deploy: + runs-on: ubuntu-latest + environment: + name: github-pages + url: ${{ steps.deploy.outputs.page_url }} + steps: + - uses: actions/checkout@v4 + # the site reuses the README screenshots and the app icon instead of keeping copies + - run: | + cp -r docs/assets site/assets + cp shelldeck/static/icon.svg site/icon.svg + - uses: actions/configure-pages@v5 + - uses: actions/upload-pages-artifact@v3 + with: + path: site + - id: deploy + uses: actions/deploy-pages@v4 diff --git a/.gitignore b/.gitignore index cf3937b..02248a9 100644 --- a/.gitignore +++ b/.gitignore @@ -283,9 +283,6 @@ venv.bak/ # Rope project settings .ropeproject -# mkdocs documentation -/site - # mypy .mypy_cache/ .dmypy.json diff --git a/CLAUDE.md b/CLAUDE.md index 49173f2..3579a81 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -162,7 +162,8 @@ uv tool install --force -e . # global sd/shelldeck from this checkout (th - `.github/workflows/ci.yml`: lint, a Windows+Ubuntu × py3.12–3.14 test matrix, and an installer smoke test. - `release.yml`: on a `v*` tag, reuses CI, checks the tag matches the pyproject version, then builds, publishes to PyPI (trusted publishing, environment `pypi`) and creates the GitHub release. - Manual run (`workflow_dispatch`, input `version`) publishes a release candidate from a `feature/` or `fix/` branch: `x.y.zrcN` only, newer than pyproject, patched into pyproject in the build (never committed), no git tag pushed, but a GitHub prerelease `v` on the branch's commit with the wheel and standalone assets (`releases/latest` skips prereleases, so OTA never offers an rc). It lives in `release.yml` because the PyPI trusted publisher is bound to that file. - - README screenshots live in `docs/assets/`. `docs/superpowers/` and `plans/` are local-only (gitignored). + - README screenshots live in `docs/assets/`. + - `site/`: the GitHub Pages site (plain HTML/CSS/JS; GSAP + ScrollTrigger + SplitText and Lenis from jsDelivr, pinned versions). `.github/workflows/pages.yml` copies `docs/assets/` and the app icon into it and deploys on pushes to `main` that touch it. Preview: copy the same files into a scratch folder and serve it. `docs/superpowers/` and `plans/` are local-only (gitignored). - `chapters/`: spec corpus for a future agent platform, not implemented. `_ref_code/`: reference only. ## Terminal flow diff --git a/site/index.html b/site/index.html new file mode 100644 index 0000000..8aa0d8d --- /dev/null +++ b/site/index.html @@ -0,0 +1,179 @@ + + + + + +shelldeck: your terminals, organised by project + + + + + + + + + + + + + + + + +
+ +
+

Windows · Linux · Free and open source

+

Your terminals,
organised by project.

+

Real shells grouped by project, in split or floating panes that survive restarts, with your AI coding agents working side by side. In the browser, on your own machine.

+
+ Install shelldeck + +
+
+
+
127.0.0.1:5455
+ shelldeck with four named terminals in three projects: a git log, Claude Code, Codex asking to trust a folder, and sd top +
+ pwshbashWSL + zshGit Bashfish +
+
+ + +
+
+ PowerShellcmdGit BashWSLbashzshfish + Claude CodeCodexGemini CLIDevinCopilot + Cursor AgentopencodeAiderAmpGooseKiro +
+
+ + +
+
+

The tour

+
+
01

Projects, not tabs

Every folder gets a color, its own terminals and a ±N chip for uncommitted work. Close the browser; the shells keep running.

+
02

Agents side by side

Claude Code, Codex, Gemini, Devin and more get an AI chip with the model and how full the context window is.

+
03

It knows when they need you

Only when an agent asks something: an amber chip, a chime, and a toast or desktop notification.

+
04

Context that outlives the agent

Task, next step, facts, decisions and recent activity per project. A new agent runs sd context and continues.

+
+
+
+
+
127.0.0.1:5455
+
+ + +
+
+
+
+
+
+
+
+ + +
+

Everything else

+

The rest of your workday, built in.

+ +
+ + +
+
+

Agents as a team

+

Agents that talk to each other.

+

Each terminal has a name. From inside one, an agent can list the others, read their screens, type into them, spawn a sub-agent in a new pane or hand off work and get told when it's done. shelldeck installs a skill so agents know these commands.

+
    +
  • Hooks for Claude Code, Codex, Gemini and Devin report working, blocked and done exactly
  • +
  • A sub-agent's questions go to its parent, not to you
  • +
  • Sessions can be resumed after a restart
  • +
+
+
+
Maya · pwsh
+

+    
+
+ + +
+
+

Anywhere

+

On your PC. And your phone.

+
+
+
+ Share dialog +

Share in one click

A Cloudflare tunnel with a one-use link, valid for 10 minutes. The device waits until you allow it, after matching a 4-digit code.

+
+
+
iPhone terminaliPhone terminals list
+

iPhone and iPad app

End-to-end encrypted with X25519 and AES-256-GCM, so Cloudflare and your Wi-Fi only see ciphertext.

+
+
+ iPad with four tiled terminals +

Four panes on iPad

Projects in a sidebar, up to four tiled terminals beside it.

+
+
+ Devices page +

One device at a time

A password is mandatory. Devices lists every signed-in browser and signs them out.

+
+
+
+ + +
+ +

Start in one line.

+
+ +
+
+ + + +
+

Then run sd. Needs Windows 10 1809+ or Linux x64, and a modern browser.

+ +
+
+ + + + + + + + + + diff --git a/site/main.js b/site/main.js new file mode 100644 index 0000000..302f9a2 --- /dev/null +++ b/site/main.js @@ -0,0 +1,126 @@ +// shelldeck site motion: GSAP + ScrollTrigger + SplitText, Lenis for smooth scroll. +// Everything degrades to a static page when the CDN scripts fail or the user prefers reduced motion. +const $ = (s, el = document) => el.querySelector(s) +const $$ = (s, el = document) => [...el.querySelectorAll(s)] + +// copy buttons and install tabs work without any animation library +$$('.copy').forEach(b => b.addEventListener('click', async () => { + try { await navigator.clipboard.writeText(b.dataset.copy) } catch { return } + b.classList.add('done'); $('i', b).textContent = 'Copied' + setTimeout(() => { b.classList.remove('done'); $('i', b).textContent = 'Copy' }, 1600) +})) +$$('.tabs button').forEach(t => t.addEventListener('click', () => { + $$('.tabs button').forEach(x => x.classList.toggle('on', x === t)) + $$('.installs .cmd').forEach(c => { c.hidden = c.dataset.tab !== t.dataset.tab }) +})) +// marquee loops by translating -50%, so it needs its content twice +const track = $('.track'); track.innerHTML += track.innerHTML +// pointer glow on feature cards +$$('.card').forEach(c => c.addEventListener('pointermove', e => { + const r = c.getBoundingClientRect() + c.style.setProperty('--x', `${e.clientX - r.left}px`); c.style.setProperty('--y', `${e.clientY - r.top}px`) +})) + +const TERM = [ + ['c', "# who's working in this project"], ['p', 'sd agents'], + ['c', "# read another terminal's screen"], ['p', 'sd peek Lina -n 20'], + ['c', '# ask it to do something'], ['p', 'sd tell Lina "run the tests and fix what fails"'], + ['c', '# a helper in a new pane'], ['p', 'sd spawn "write tests for auth.py" --model small'], + ['c', '# hand work over, hear back when done'], ['p', 'sd handoff Kofi "review the diff"'], + ['c', '# leave context for the next agent'], ['p', 'sd remember "integration tests need docker"'], +] +const termHTML = n => TERM.map(([k, s], i) => { + const text = i < Math.floor(n) ? s : i === Math.floor(n) ? s.slice(0, Math.round((n % 1) * s.length)) : null + if (text === null) return '' + return k === 'p' ? `>> ${esc(text)}\n` : `${esc(text)}\n` +}).join('') + '' +const esc = s => s.replace(/[&<>"]/g, c => ({ '&': '&', '<': '<', '>': '>', '"': '"' })[c]) + +const reduce = matchMedia('(prefers-reduced-motion: reduce)').matches +if (!window.gsap || !window.ScrollTrigger || reduce) { + $('#termOut').innerHTML = termHTML(TERM.length) + $$('.tour .step').forEach(s => (s.style.opacity = 1)) + const view = $('.tour .view'), shot = $('.tour .shot'), fit = () => (shot.style.transform = `scale(${view.clientWidth / 1600})`) + fit(); addEventListener('resize', fit) +} else { + motion() +} + +function motion() { + gsap.registerPlugin(ScrollTrigger, ...(window.SplitText ? [SplitText] : [])) + + if (window.Lenis) { // drive Lenis from GSAP's ticker so ScrollTrigger never reads a stale position + const lenis = new Lenis({ lerp: 0.1 }) + lenis.on('scroll', ScrollTrigger.update) + gsap.ticker.add(t => lenis.raf(t * 1000)) + gsap.ticker.lagSmoothing(0) + $$('a[href^="#"]').forEach(a => a.addEventListener('click', e => { e.preventDefault(); lenis.scrollTo(a.getAttribute('href'), { offset: -80 }) })) + } + + // headlines: words rise out of a mask + document.fonts.ready.then(() => $$('.split').forEach((h, i) => { + if (!window.SplitText) return + const split = SplitText.create(h, { type: 'words', mask: 'words', wordsClass: 'sw' }) + const hero = h.closest('.hero') + gsap.from(split.words, { yPercent: 110, duration: 1, stagger: .06, ease: 'power4.out', delay: hero ? .2 : 0, + scrollTrigger: hero ? undefined : { trigger: h, start: 'top 85%' } }) + })) + gsap.utils.toArray('.reveal').forEach(el => gsap.from(el, { y: 30, opacity: 0, duration: .9, ease: 'power3.out', + delay: el.closest('.hero') ? .6 : 0, scrollTrigger: el.closest('.hero') ? undefined : { trigger: el, start: 'top 88%' } })) + + // hero: the app window lies back and stands up as you scroll; shell chips drift at different speeds + gsap.fromTo('#heroWin', { rotateX: 28, scale: .88, y: 40 }, { rotateX: 0, scale: 1, y: 0, ease: 'none', + scrollTrigger: { trigger: '.hero-shot', start: 'top 95%', end: 'top 25%', scrub: true } }) + gsap.from('.hero-shot', { opacity: 0, y: 120, duration: 1.4, ease: 'expo.out', delay: .5 }) + $$('.float').forEach((f, i) => { + gsap.from(f, { opacity: 0, scale: .4, duration: .6, ease: 'back.out(2)', delay: 1 + i * .08 }) + gsap.to(f, { y: (i % 2 ? -1 : 1) * (60 + i * 25), ease: 'none', scrollTrigger: { trigger: '.hero-shot', start: 'top bottom', end: 'bottom top', scrub: true } }) + }) + + const mm = gsap.matchMedia() + mm.add('(min-width: 901px)', () => { tour(); horizontal() }) + mm.add('(max-width: 900px)', () => { fitTour(); $$('.tour .step').forEach(s => (s.style.opacity = 1, s.style.gridArea = 'auto', s.style.marginBottom = '32px')) }) + + // feature cards and terminal typing + gsap.from('.card', { y: 60, opacity: 0, duration: .8, ease: 'power3.out', stagger: { each: .06, from: 'start' }, scrollTrigger: { trigger: '.bento', start: 'top 80%' } }) + const out = $('#termOut'), state = { n: 0 } + out.innerHTML = termHTML(0) + gsap.to(state, { n: TERM.length, duration: 7, ease: 'none', onUpdate: () => (out.innerHTML = termHTML(state.n)), scrollTrigger: { trigger: '#term', start: 'top 70%' } }) + gsap.from('.big-logo', { scale: .3, rotate: -20, duration: 1.1, ease: 'back.out(1.7)', scrollTrigger: { trigger: '.install', start: 'top 70%' } }) +} + +// pinned tour: the window zooms to each part of the app while the copy changes +function tour() { + const view = $('.tour .view'), shot = $('.tour .shot'), steps = $$('.tour .step') + const imgs = $$('.shot img'), boxes = $$('.shot .box') + const REGIONS = [{ x: 0, y: 170, w: 520, h: 292 }, { x: 936, y: 48, w: 662, h: 372 }, { x: 270, y: 476, w: 664, h: 374 }, { x: 296, y: 196, w: 760, h: 428 }] + const zoom = r => { const W = view.clientWidth, H = view.clientHeight, s = Math.min(W / r.w, H / r.h); return { scale: s, x: W / 2 - (r.x + r.w / 2) * s, y: H / 2 - (r.y + r.h / 2) * s } } + const full = () => zoom({ x: 0, y: 0, w: 1600, h: 900 }) + gsap.set(shot, full()) + gsap.set(imgs, { opacity: i => (i ? 0 : 1) }) + const tl = gsap.timeline({ defaults: { ease: 'power2.inOut' }, scrollTrigger: { trigger: '.tour', start: 'top top', end: '+=3200', pin: true, scrub: .6, invalidateOnRefresh: true } }) + REGIONS.forEach((r, i) => { + const t = i * 2 + if (i) tl.to(steps[i - 1], { opacity: 0, y: -30, duration: .3 }, t).to(boxes[i - 1], { opacity: 0, duration: .3 }, t) + if (i) tl.fromTo(steps[i], { opacity: 0, y: 30 }, { opacity: 1, y: 0, duration: .5 }, t + .4) + else gsap.set(steps[0], { opacity: 1 }) + tl.to(shot, { scale: () => zoom(r).scale, x: () => zoom(r).x, y: () => zoom(r).y, duration: 1 }, t) + imgs.forEach(img => tl.to(img, { opacity: +img.dataset.k === (i === 3 ? 3 : 0) ? 1 : 0, duration: .4 }, t)) + tl.fromTo(boxes[i], { opacity: 0, scale: 1.1 }, { opacity: 1, scale: 1, duration: .4 }, t + .9) + }) + tl.to({}, { duration: .8 }) + ScrollTrigger.addEventListener('refreshInit', () => gsap.set(shot, full())) +} + +// phones: no pinning, the whole screenshot fits the window +function fitTour() { + const view = $('.tour .view'), fit = () => gsap.set('.tour .shot', { scale: view.clientWidth / 1600, x: 0, y: 0 }) + fit(); addEventListener('resize', fit) +} + +// horizontal panels slide sideways while the section is pinned +function horizontal() { + const track = $('.htrack') + const dist = () => track.scrollWidth - innerWidth + gsap.to(track, { x: () => -dist(), ease: 'none', scrollTrigger: { trigger: '.anywhere', start: 'top top', end: () => '+=' + dist(), pin: true, scrub: .6, invalidateOnRefresh: true } }) +} diff --git a/site/style.css b/site/style.css new file mode 100644 index 0000000..da7347b --- /dev/null +++ b/site/style.css @@ -0,0 +1,151 @@ +:root { + --bg: #0a0a0b; --panel: #131316; --elev: #18181b; --border: #26262b; --border-strong: #34343a; + --text: #f4f4f5; --muted: #a1a1aa; --faint: #71717a; --accent: #a78bfa; --violet: #8b5cf6; --warn: #fbbf24; + --grad: linear-gradient(90deg, #c4b5fd, #8b5cf6); + --pad: clamp(16px, 4vw, 64px); +} +* { box-sizing: border-box; margin: 0; padding: 0 } +html { background: var(--bg); color-scheme: dark } +html.lenis, html.lenis body { height: auto } +body { font-family: Inter, system-ui, sans-serif; color: var(--text); background: var(--bg); -webkit-font-smoothing: antialiased; overflow-x: hidden; line-height: 1.5 } +img { display: block; max-width: 100%; height: auto } +[hidden] { display: none !important } +a { color: inherit; text-decoration: none } +.mono, code, pre, kbd { font-family: 'JetBrains Mono', Consolas, monospace } +em, em .sw { font-style: normal; background: var(--grad); -webkit-background-clip: text; background-clip: text; color: transparent } +code { font-size: .9em; background: var(--elev); border: 1px solid var(--border); border-radius: 6px; padding: 1px 6px } +kbd { display: inline-block; font-size: .8em; padding: 1px 7px; border-radius: 6px; background: var(--elev); border: 1px solid var(--border-strong); border-bottom-width: 3px; margin: 0 2px } +section { position: relative; padding: clamp(80px, 12vw, 160px) var(--pad) } +h2 { font-size: clamp(36px, 5.4vw, 76px); line-height: 1.04; letter-spacing: -.04em; font-weight: 800; max-width: 15ch } +.eyebrow { font-size: 14px; letter-spacing: .2em; text-transform: uppercase; color: var(--accent); font-weight: 700; margin-bottom: 20px } +.lede { font-size: clamp(17px, 1.6vw, 21px); color: #d4d4d8; max-width: 58ch } + +/* background */ +.bg { position: fixed; inset: 0; z-index: -1; pointer-events: none } +.bg-glow { position: absolute; inset: -20%; background: radial-gradient(50% 40% at 50% 20%, rgba(139, 92, 246, .22), transparent 70%), radial-gradient(40% 30% at 90% 70%, rgba(167, 139, 250, .08), transparent 70%) } +.bg-grid { position: absolute; inset: 0; background-image: linear-gradient(rgba(255, 255, 255, .035) 1px, transparent 1px), linear-gradient(90deg, rgba(255, 255, 255, .035) 1px, transparent 1px); background-size: 72px 72px; -webkit-mask-image: radial-gradient(70% 60% at 50% 30%, #000, transparent) } + +/* nav */ +.nav { position: fixed; top: 14px; left: 50%; transform: translateX(-50%); z-index: 50; display: flex; align-items: center; gap: 28px; padding: 8px 8px 8px 18px; border-radius: 999px; + background: rgba(19, 19, 22, .72); border: 1px solid var(--border); backdrop-filter: blur(14px); -webkit-backdrop-filter: blur(14px); width: max-content; max-width: calc(100% - 32px) } +.brand { display: flex; align-items: center; gap: 10px; font-weight: 700; letter-spacing: -.02em } +.nav nav { display: flex; gap: 22px; font-size: 14px; color: var(--muted) } +.nav nav a:hover { color: var(--text) } +.btn { display: inline-flex; align-items: center; height: 44px; padding: 0 22px; border-radius: 999px; font-weight: 600; font-size: 15px; transition: transform .2s, background .2s, box-shadow .2s } +.btn.primary { background: var(--text); color: var(--bg); box-shadow: 0 0 0 0 rgba(139, 92, 246, .5) } +.btn.primary:hover { transform: translateY(-2px); box-shadow: 0 10px 40px -8px rgba(139, 92, 246, .8) } +.btn.ghost { border: 1px solid var(--border-strong); background: rgba(255, 255, 255, .02) } +.btn.ghost:hover { background: var(--elev) } +.nav .btn { height: 36px; padding: 0 16px; font-size: 14px } + +/* hero */ +.hero { min-height: 100vh; padding-top: 150px; display: flex; flex-direction: column; align-items: center; text-align: center } +.hero h1 { font-size: clamp(44px, 8.4vw, 128px); line-height: 1; letter-spacing: -.05em; font-weight: 800 } +.hero .lede { margin: 28px auto 0 } +.cta { display: flex; gap: 14px; justify-content: center; flex-wrap: wrap; margin-top: 36px } +.cmd { display: inline-flex; align-items: center; gap: 12px; min-height: 44px; padding: 10px 16px 10px 18px; border-radius: 12px; background: var(--panel); border: 1px solid rgba(167, 139, 250, .35); + color: var(--text); font: 500 15px/1.4 'JetBrains Mono', monospace; cursor: pointer; text-align: left; transition: border-color .2s, box-shadow .2s } +.cmd:hover { border-color: var(--accent); box-shadow: 0 0 40px -10px rgba(139, 92, 246, .7) } +.cmd span { color: var(--accent); flex: none } +.cmd i { font: 600 12px Inter, sans-serif; font-style: normal; color: var(--muted); border-left: 1px solid var(--border); padding-left: 12px; margin-left: 4px; white-space: nowrap; flex: none } +.cmd.done i { color: #4ade80 } +.hero-shot { position: relative; width: min(1200px, 100%); margin-top: 72px; perspective: 1800px } +.window { border-radius: 14px; overflow: hidden; background: var(--panel); border: 1px solid var(--border-strong); + box-shadow: 0 60px 140px -40px rgba(0, 0, 0, .9), 0 0 120px -30px rgba(139, 92, 246, .45) } +.bar { height: 32px; background: #0f0f11; border-bottom: 1px solid var(--border); display: flex; align-items: center; gap: 8px; padding: 0 14px } +.bar i { width: 11px; height: 11px; border-radius: 50%; background: var(--border-strong) } +.bar span { margin: 0 auto; transform: translateX(-28px); font-size: 12px; color: var(--muted) } +#heroWin { transform-origin: 50% 0; will-change: transform } +.float { position: absolute; padding: 8px 16px; border-radius: 999px; background: var(--elev); border: 1px solid var(--border-strong); font-size: 14px; box-shadow: 0 16px 40px -14px #000 } +.float::before { content: ""; display: inline-block; width: 8px; height: 8px; border-radius: 50%; background: var(--accent); margin-right: 8px; vertical-align: 1px } +.f1 { left: -3%; top: 12% } .f2 { right: -3%; top: 8% } .f3 { left: -5%; top: 46% } .f4 { right: -5%; top: 42% } .f5 { left: 2%; top: 80% } .f6 { right: 1%; top: 76% } + +/* marquee */ +.marquee { padding: 28px 0; border-block: 1px solid var(--border); overflow: hidden; -webkit-mask-image: linear-gradient(90deg, transparent, #000 10%, #000 90%, transparent) } +.track { display: flex; gap: 48px; width: max-content; animation: marquee 40s linear infinite } +.track span { font-size: 22px; font-weight: 600; color: var(--faint); white-space: nowrap } +.track span.ai { color: var(--muted) } +.track span.ai::before { content: "✦ "; color: var(--accent) } +@keyframes marquee { to { transform: translateX(-50%) } } + +/* pinned tour */ +.tour { display: grid; grid-template-columns: minmax(280px, 1fr) 2fr; gap: 56px; align-items: center; min-height: 100vh } +.steps { position: relative; display: grid } +.step { grid-area: 1 / 1; opacity: 0 } +.step .n { font: 700 14px 'JetBrains Mono', monospace; color: var(--faint) } +.step h3 { font-size: clamp(32px, 3.6vw, 52px); line-height: 1.05; letter-spacing: -.035em; margin: 14px 0 18px } +.step p { color: var(--muted); font-size: 18px; max-width: 40ch } +.tour .view { position: relative; aspect-ratio: 16 / 9; overflow: hidden; background: var(--bg) } +.shot { position: absolute; left: 0; top: 0; width: 1600px; height: 900px; transform-origin: 0 0 } +.shot img { position: absolute; inset: 0; width: 1600px; height: 900px; max-width: none } +.box { position: absolute; border: 3px solid var(--accent); border-radius: 8px; opacity: 0; box-shadow: 0 0 0 9999px rgba(10, 10, 11, .45), 0 0 30px rgba(167, 139, 250, .6) } +.box.amber { border-color: var(--warn); box-shadow: 0 0 0 9999px rgba(10, 10, 11, .35), 0 0 40px rgba(251, 191, 36, .6) } + +/* bento */ +.features h2 { margin-bottom: 56px } +.bento { display: grid; grid-template-columns: repeat(4, 1fr); grid-auto-rows: 300px; gap: 16px } +.card { position: relative; overflow: hidden; border-radius: 18px; background: var(--panel); border: 1px solid var(--border); display: flex; align-items: flex-end; isolation: isolate; + transition: border-color .3s, transform .3s } +.card.wide { grid-column: span 2 } +.card::before { content: ""; position: absolute; inset: 0; z-index: -2; background: var(--img) var(--pos) / cover no-repeat; transform: scale(1.35); transform-origin: var(--pos); transition: transform .8s cubic-bezier(.2, .7, .2, 1); opacity: .85 } +.card::after { content: ""; position: absolute; inset: 0; z-index: -1; background: linear-gradient(180deg, rgba(10, 10, 11, 0) 20%, rgba(10, 10, 11, .96) 78%), + radial-gradient(400px circle at var(--x, 50%) var(--y, 50%), rgba(167, 139, 250, .18), transparent 60%) } +.card:hover { border-color: rgba(167, 139, 250, .5) } +.card:hover::before { transform: scale(1.5) } +.card div { padding: 24px } +.card h3 { font-size: 22px; letter-spacing: -.02em; margin-bottom: 6px } +.card p { color: var(--muted); font-size: 15px } + +/* agents */ +.agents { display: grid; grid-template-columns: 1fr 1fr; gap: 64px; align-items: center } +.agents .lede { margin-top: 24px } +.ticks { list-style: none; margin-top: 28px; display: grid; gap: 12px; color: #d4d4d8 } +.ticks li::before { content: "✓"; color: var(--accent); font-weight: 700; margin-right: 12px } +.ticks b { color: var(--text) } +.term { border-radius: 14px; overflow: hidden; background: #0d0d10; border: 1px solid var(--border-strong); box-shadow: 0 40px 100px -30px #000, 0 0 100px -40px rgba(139, 92, 246, .6) } +.term pre { padding: 22px 24px; min-height: 380px; font-size: 14.5px; line-height: 1.75; white-space: pre-wrap; color: #d4d4d8 } +.term .p { color: var(--accent) } .term .c { color: var(--faint) } .term .caret { display: inline-block; width: 9px; height: 18px; background: var(--text); vertical-align: -3px; animation: blink 1s steps(1) infinite } +@keyframes blink { 50% { opacity: 0 } } + +/* horizontal */ +.anywhere { padding-inline: 0; overflow: hidden } +.anywhere-head { padding: 0 var(--pad); margin-bottom: 48px } +.htrack { display: flex; gap: 28px; padding: 0 var(--pad); width: max-content } +.panel { width: min(640px, 80vw); flex: none; border-radius: 20px; background: var(--panel); border: 1px solid var(--border); padding: 18px 18px 28px } +.panel > img { border-radius: 12px; border: 1px solid var(--border); aspect-ratio: 16 / 9; object-fit: cover; object-position: 50% 0; width: 100% } +.panel h3 { font-size: 26px; letter-spacing: -.02em; margin: 22px 8px 8px } +.panel p { color: var(--muted); margin: 0 8px } +.phones { display: flex; gap: 16px; justify-content: center; aspect-ratio: 16 / 9; overflow: hidden; border-radius: 12px; background: radial-gradient(circle at 50% 30%, #1c1530, #0d0d10); padding-top: 20px } +.phones img { width: 30%; border-radius: 22px 22px 0 0; border: 6px solid #1c1c20; border-bottom: 0; object-fit: cover; object-position: top } + +/* install */ +.install { text-align: center; display: flex; flex-direction: column; align-items: center } +.install h2 { margin: 0 auto } +.big-logo { margin-bottom: 36px; filter: drop-shadow(0 0 70px rgba(139, 92, 246, .8)) } +.tabs { display: inline-flex; gap: 4px; padding: 4px; border-radius: 999px; background: var(--panel); border: 1px solid var(--border); margin: 40px 0 18px } +.tabs button { border: 0; background: none; color: var(--muted); font: 600 14px Inter, sans-serif; padding: 8px 18px; border-radius: 999px; cursor: pointer } +.tabs button.on { background: var(--elev); color: var(--text); box-shadow: inset 0 0 0 1px var(--border-strong) } +.installs .cmd { max-width: min(860px, 92vw); font-size: 14px; word-break: break-all } +.small { color: var(--faint); margin-top: 18px; font-size: 15px } + +.foot { display: flex; justify-content: space-between; gap: 16px; flex-wrap: wrap; padding: 32px var(--pad); border-top: 1px solid var(--border); color: var(--faint); font-size: 14px } +.foot span { display: inline-flex; align-items: center; gap: 8px } +.foot a:hover { color: var(--text) } + +@media (max-width: 900px) { + .nav nav { display: none } + .tour, .agents { grid-template-columns: 1fr } + .bento { grid-template-columns: 1fr 1fr; grid-auto-rows: 240px } + .float { display: none } + .htrack { width: auto; overflow-x: auto; scroll-snap-type: x mandatory; padding-bottom: 12px } + .panel { scroll-snap-align: center } +} +@media (max-width: 560px) { + .bento { grid-template-columns: 1fr } + .card.wide { grid-column: auto } + .cmd { font-size: 13px } +} +@media (prefers-reduced-motion: reduce) { + .track { animation: none } + .step { opacity: 1; grid-area: auto; margin-bottom: 32px } +}