diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..bbd4de5 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,475 @@ +# AGENTS.md — Agent Loft User Portal + +Read this file fully before taking any action. + +--- + +## Project Context + +Single-page user portal for **agent-loft.com** — lets customers sign up, log in, and manage their AI agents. No framework, no build step, no external dependencies. + +| Item | Detail | +|---|---| +| Entry point | `index.html` — only HTML file | +| Styling | Vanilla CSS · `styles.css` (edit directly, no compilation) | +| Logic | Vanilla JS · `app.js` (edit directly, no bundler) | +| Dev server | `./start` (runs `live-server` on port 8080) | +| Auth | Email stored in `localStorage` key `al_email` | +| Backend | n8n webhooks on `n8n.agent-loft.com` | + +### File map + +``` +index.html ← all markup — auth card + app shell + confirm dialog +styles.css ← all styles — design tokens, layout, components +app.js ← all logic — auth, agents, keys, password, restart, backups, contract +start ← dev server launcher (live-server) +AGENTS.md ← this file + +skills/ + index.json ← summary: [{ name, file, description }] — loaded on wizard step 1 + .json ← one file per skill: { name, description, prompt } + +integrations/ + index.json ← summary: [{ name, file, description, fields[], signup_url, signup_label }] — loaded on wizard step 2 + fields + .json ← one file per integration: adds prompt to the index entry +``` + +--- + +## Visual Architecture + +The UI has **two mutually exclusive top-level views**. `body` is a flex container that centres whichever is visible. + +``` +body (display:flex; align-items:center; justify-content:center; overflow:hidden) + │ + ├── #auth-card ← shown when NOT logged in (440 px wide, auto height) + │ ├── .auth-logo + │ ├── .auth-switch ← Sign In | Sign Up pill toggle + │ ├── #signin-form + │ └── #signup-form + │ + └── #app-shell ← shown when logged in (max 1060×780 px, rounded panel) + ├── .app-header ← logo · email pill · Sign Out + ├── .agent-tabs-bar ← one pill tab per agent + └── .app-body + ├── #no-agents ← shown when agent list is empty + └── #agent-panel ← shown for the active agent + └── .content-grid (2 columns: left / right) + ├── .content-col [left] + │ ├── card: Server Actions (#server-info-body, #restart-btn) + │ ├── card: Instance Password (#pw-new-input) + │ └── card: API Keys (#keys-body) + └── .content-col [right] + ├── card: Backups (#backups-body) + └── card: Contract (#contract-body) +``` + +**Non-scrolling rule:** `body` and `#app-shell` never scroll. Only `.content-grid` (inside the shell) scrolls when content overflows, and `.backups-list` scrolls internally at `max-height: 320px`. + +--- + +## Auth Flow + +``` +Page load + └── localStorage has 'al_email'? + Yes → showApp() + loadAgents() ← shell made visible immediately, no auth animation + No → showAuth() ← auth card + demo shell shown side-by-side + +Sign In + └── GET AGENTS_URL?email=…&password=… + 200 → store email → showApp() (animates auth card out) → processAgents() + !200 → show inline error + +Sign Up + └── POST SIGNUP_URL {email, password, agent, location} + 200 → open Stripe checkout tab + auto-login immediately + !200 → show inline error + +Sign Out + └── remove 'al_email' from localStorage → showAuth() +``` + +The email is the only session token. There is no password verification on the frontend — authentication is enforced by the n8n webhooks. + +### `showApp()` behaviour +- If called while `body.auth-layout` is present (user just signed in): animates the auth card upward and fades it out, then reveals the shell. +- If called on direct page load (user already logged in): immediately hides the auth card and shows the shell — no blink animation. +- Always sets `shell.style.display = "flex"` explicitly; the shell's default CSS is `display: none`. + +--- + +## Design Tokens + +All tokens are CSS custom properties declared in `:root` inside `styles.css`. + +| Variable | Value | Usage | +|---|---|---| +| `--bg` | `#0f1117` | page background | +| `--bg-card` | `#1a1d27` | app shell / auth card surface | +| `--bg-inner` | `#141720` | inner cards, tabs bar | +| `--bg-hover` | `#22263a` | hover states, ghost button bg | +| `--bg-input` | `#12151f` | input / textarea background | +| `--border` | `#2a2f45` | default borders | +| `--border-hi` | `rgba(79,142,247,0.28)` | shell outer border glow | +| `--accent` | `#4f8ef7` | primary brand blue | +| `--accent-dim` | `#1a2d5e` | active tab bg, logo bg | +| `--danger` | `#e05252` | destructive actions, errors | +| `--danger-dim` | `#5a1f1f` | error message background | +| `--success` | `#3fc97e` | positive indicators, active dot | +| `--warning` | `#f0a04b` | mid-range credit bar, contract warning | +| `--text` | `#e4e8f5` | primary text | +| `--text-muted` | `#7a82a0` | secondary text, labels | +| `--radius` | `8px` | default border-radius | +| `--shadow` | (see CSS) | shell / auth card drop shadow | + +**Never use raw hex values in HTML or JS** — always reference a token via `var(--token-name)` in CSS or as a string literal only when building inline styles in JS (acceptable only for dynamic status colours in `renderKeys` and `renderContract`). + +--- + +## Component Patterns + +### Cards +Every content block is a `.card` with `.card-header` + body content. + +```html +
+
+ + Title + + +
+ +
+``` + +Card backgrounds are `--bg-inner` (one level darker than the shell's `--bg-card`). + +### Buttons + +| Class | Colour | Use for | +|---|---|---| +| `btn btn-primary btn-sm` | `--accent` blue | navigation / open actions (Open Agent) | +| `btn btn-ghost btn-sm` | `--bg-hover` grey + border | all card-header and inline actions (Restart Agent, Buy Credits, Restore, Extend Contract, Cancel, Make Backup) | +| `btn btn-danger` | `--danger` red | confirm dialog destructive confirm only | +| `btn btn-block` | — modifier, full width | auth form submit buttons | + +**Card-header action buttons must:** +- Use `btn btn-ghost btn-sm` (grey) by default, or `btn btn-primary btn-sm` (blue) for a primary navigation action. +- Have **no SVG icons** — text label only. +- When two buttons appear side-by-side in a header, wrap them in `
` rather than placing them as siblings. + +**`` tags used as buttons** (e.g. contract links that open Stripe): apply the same `btn btn-ghost btn-sm` classes. The `.btn` base class sets `text-decoration: none` so no underline appears. + +#### Inline icon-only buttons +Use the `.pw-save-btn` pattern for buttons embedded inside input fields: `position: absolute`, no text, icon only, hidden by default, shown/hidden via JS. + +### Forms +Inputs and selects use `.form-group` inside `.form-row`: + +```html +
+
+ + +
+
+``` + +Focus state: `border-color` switches to `--accent`. + +#### Inline-button input (`.pw-input-wrap`) +Wrap an input + absolutely-positioned button together when the button should appear inside the field: + +```html +
+ + +
+``` + +### Loading states +Use `.loading-row` with a `.spinner` while async calls are in flight: + +```html +
+``` + +Use `.spinner-sm` (13 px) inside buttons when a call is pending (see `btnLoad()`). + +### Toast notifications +Call `toast(message, type)` from anywhere. `type` is `'info'` | `'success'` | `'error'` | `'warning'`. Toasts self-remove after 4 s with a slide-out animation. + +### Confirm dialog +Always use `confirmDialog(title, message)` before destructive actions. It returns a `Promise`. + +```js +const ok = await confirmDialog('Delete?', 'This cannot be undone.'); +if (!ok) return; +``` + +--- + +## Webhook Contracts + +### 1 · Sign Up +``` +POST https://n8n.agent-loft.com/webhook/4de196b7-2ad2-4f9a-9b4d-dc8d3b30865b +Body: { email, password, agent, location } +``` +Response: `200` on success. User receives a Stripe billing email separately. + +### 2 · List Agents +``` +GET https://n8n.agent-loft.com/webhook/73b31740-d2c7-46d7-ab71-7a3fef5f77ff + ?email=&password= +``` +Response shape: +```json +[{ "email": "…", "UUID": "apple", "server": "fr", "id": 1, + "createdAt": "…", "updatedAt": "…" }] +``` +An empty array `[]` means no agents — show `#no-agents` state. + +### 3 · List API Keys +``` +GET https://n8n.agent-loft.com/webhook/1f1a6a11-727b-4965-a59a-fde77806d27f + ?uuid= +``` +Response shape: `[{ "data": [ { "name", "label", "limit", "limit_remaining", +"expires_at", "disabled", "usage_monthly", … } ] }]` + +`renderKeys()` reads `json[0].data`. The credit progress bar colour follows: +- `< 50 %` used → `prog-low` (green) +- `50–79 %` used → `prog-mid` (amber) +- `≥ 80 %` used → `prog-high` (red) + +### 4 · Update Instance Password +``` +POST https://n8n.agent-loft.com/webhook/51098cf4-ecfd-4db4-8977-db04f01ce2b1 +Body: { uuid, email, password } +``` +Requires confirm dialog before calling. Uses a single `` embedded in `.pw-input-wrap`; the save button (checkmark icon, no text) only appears when the field has content. No GET endpoint — the field is always blank on load. Minimum password length: 8 characters. + +### 5 · Restart Server +``` +POST https://n8n.agent-loft.com/webhook/dac205df-66e0-4728-90e5-d784cde167af +Body: { uuid, email, action: "restart" } +``` +Requires confirm dialog. + +### 6 · Agent Info +``` +GET https://n8n.agent-loft.com/webhook/e01d06a3-14c3-4e4e-830f-7d4be9a5f529 + ?uuid= +``` +Response shape: `{ agent, domain, ssh_port, created, comment, … }` (n8n may wrap values in extra quotes — use `stripQuotes()` before display). + +`renderAgentInfo()` builds the Server Actions card body with rows: Agent Type, Dashboard, SSH Access (clickable — copies `ssh root@UUID.agent-loft.com -p PORT` to clipboard), Created. + +This same URL accepts **POST** `{ uuid, key, value }` for all config writes: +- `key: "comment"` — saves the header comment +- `key: "WIZZARD", value: "false"` — dismisses the Integrations wizard + +### 7 · Backups +``` +GET https://n8n.agent-loft.com/webhook/30eaa32f-378a-4963-9d80-533229d25766 + ?uuid= ← list all backups + +POST …/30eaa32f-… +Body: { uuid, email, action: "make" } ← create backup + +POST …/30eaa32f-… +Body: { uuid, email, action: "restore", + backup_id: } ← restore; requires confirm dialog +``` + +The backup object shape from the API is flexible. The renderer reads `b.name || b.id` for display and `b.created_at || b.createdAt || b.date || b.timestamp` for the date. + +### 8 · Contract +``` +GET https://n8n.agent-loft.com/webhook/18591766-147e-4bcb-b9ac-b0f9a92e74bf + ?uuid= +``` +Response shape: +```json +[{ "type": "Auto - Monthly", "expires": "" }] +``` + +`renderContract()` shows a status dot + contract type + expiration date. Status logic (evaluated in order): +1. `expires` is a past date → 🔴 red +2. `type` contains `"none"` (case-insensitive) → 🔴 red +3. `type` contains `"auto"` (case-insensitive) → 🟢 green +4. Otherwise → 🟡 yellow + +When status is red, an **Extend Contract →** link is shown pointing to `CONTRACT_EXTEND_URL` (defined at the top of `app.js` — **update this to the Stripe payment link**). + +### 9 · Wizard Complete +``` +POST https://n8n.agent-loft.com/webhook/e01d06a3-14c3-4e4e-830f-7d4be9a5f529 (same as Agent Info) +Body: { uuid, key: "WIZZARD", value: "false" } +``` +Called when the user clicks **Copy & Finish** in the wizard. Reuses `AGENT_INFO_URL`. The same endpoint accepts all agent config writes via `{ uuid, key, value }` — e.g. comments use `key: "comment"`. No separate constant needed. + +The agent info GET response (webhook 6) drives wizard visibility: if the response contains `WIZZARD` with any value other than `false` (or the key is absent), the wizard is shown above the Backups card. + +--- + +## State Variables (`app.js`) + +| Variable | Type | Description | +|---|---|---| +| `currentEmail` | `string \| null` | logged-in user's email | +| `agents` | `Array` | full agent objects from AGENTS_URL | +| `activeUUID` | `string \| null` | UUID of the currently selected agent tab | +| `activeAgentInfo` | `object \| null` | last loaded agent info response (used by `copySSHAccess`) | +| `wizardPhase` | `string \| null` | current wizard phase: `'skills'` \| `'integrations'` \| `'fields'` \| `'review'` \| `null` | +| `wizardSkillsData` | `Array \| null` | cached contents of `skills.json` (loaded once, reused) | +| `wizardIntegrationsData` | `Array \| null` | cached contents of `integrations.json` (loaded once, reused) | +| `wizardSelectedSkills` | `Set` | indices of selected skills | +| `wizardSelectedIntegrations` | `Set` | indices of selected integrations | +| `wizardFieldValues` | `object` | `{ stepIndex: { fieldKey: value } }` — values entered per integration | +| `wizardIntegrationStep` | `number` | current integration index during the `'fields'` phase | +| `wizardSelectedIntegrationList` | `Array` | ordered integration objects chosen in step 2 | + +There is no global keys, backups, contract, or wizard-JSON state that is re-fetched per tab switch — agent selections and field values reset per-agent, but `skills.json` / `integrations.json` are cached for the session. + +--- + +## Key Functions + +| Function | What it does | +|---|---| +| `showAuth()` / `showApp()` | toggle between auth card and app shell | +| `switchAuthTab(tab)` | toggle Sign In / Sign Up forms | +| `doSignIn()` | validate email → fetch agents → enter app | +| `doSignUp()` | validate form → POST signup webhook → show Stripe notice | +| `doSignOut()` | clear localStorage → reset state → showAuth | +| `loadAgents()` | fetch agent list, call `processAgents()` | +| `selectAgent(uuid)` | switch active tab, load keys + backups + agent info + contract | +| `loadKeys(uuid)` | fetch + render API keys card | +| `renderKeys(keys)` | build key rows with credit bar + Buy Credits button | +| `togglePwSave(value)` | show/hide the inline password save button based on field content | +| `savePassword()` | validate → confirm → POST password webhook | +| `loadAgentInfo(uuid)` | fetch agent info, call `renderAgentInfo()` + `renderComment()` | +| `renderAgentInfo(info)` | build Server Actions info rows including clickable SSH Access row | +| `copySSHAccess()` | copy `ssh root@UUID.agent-loft.com -p PORT` to clipboard + info toast | +| `doRestart()` | confirm → POST restart webhook | +| `loadBackups(uuid)` | fetch + render backup list | +| `makeBackup()` | POST backup make → reload list | +| `restoreBackup(backup)` | confirm → POST backup restore | +| `loadContract(uuid)` | fetch + render contract card | +| `renderContract(data)` | determine status dot colour + show type, expiry, extend link if red | +| `openAgent()` | open the agent's dashboard URL (from `activeAgentInfo.domain`) in a new tab | +| `loadWizard(info)` | check `info.WIZZARD`; if not `false`, reset wizard state and show the card | +| `renderWizardStep()` | async — fetch JSON if needed, render the current phase into `#wizard-body`, call `updateWizardNav()` | +| `renderWizardSkills()` | render skills checklist (step 1) with search | +| `renderWizardIntegrations()` | render integrations checklist (step 2) with search | +| `renderWizardFields()` | render form fields for `wizardSelectedIntegrationList[wizardIntegrationStep]` | +| `renderWizardReview()` | build combined init-prompt textarea from skills + integrations | +| `buildWizardPrompt()` | assemble skills prompts + integration prompts (with field substitution) into one string | +| `updateWizardNav()` | set step-label text + show/hide Back button + set Next/Copy label | +| `wizardNext()` | advance phase or trigger `wizardCopyAndFinish()` | +| `wizardBack()` | retreat phase | +| `wizardFilter(input, listId)` | real-time search filter on `.wizard-list-item` elements | +| `wizardCopyAndFinish()` | copy prompt to clipboard → POST wizard webhook → hide card | +| `confirmDialog(title, msg)` | shows modal, returns `Promise` | +| `toast(msg, type)` | bottom-right notification, auto-removes after 4 s | +| `btnLoad(btn, label)` | disable button, show spinner + label, save original HTML | +| `btnReset(btn)` | re-enable button, restore original HTML | +| `escHtml(s)` | HTML-escape a string for safe innerHTML insertion | +| `escAttr(s)` | HTML + single-quote escape for safe attribute values | +| `stripQuotes(v)` | strip extra surrounding quotes that n8n injects into string values | + +--- + +## Wizard Data Files + +### Skills + +`skills/index.json` — an array of filenames; fetched once on wizard step 1, cached in `wizardSkillsData`: +```json +["copywriting.json", "cold-email.json"] +``` + +`skills/.json` — fetched in parallel when the skills step opens, cached in `wizardSkillsCache`: +```json +{ "name": "Copywriting", "description": "Short blurb shown in the checklist.", "prompt": "Full skill prompt text." } +``` + +### Integrations + +`integrations/index.json` — an array of filenames; fetched once on wizard step 2, cached in `wizardIntegrationsData`: +```json +["odoo-community.json", "imap-smtp-email.json"] +``` + +`integrations/.json` — fetched in parallel when the integrations step opens, cached in `wizardIntegrationsCache`. Contains everything needed for the checklist, fields UI, and prompt: +```json +{ "name": "ODOO Community", "description": "...", + "fields": [{ "label", "key", "type", "placeholder" }], + "signup_url": "...", "signup_label": "...", + "prompt": "Prompt with {placeholder} substitutions." } +``` + +`prompt` uses `{key}` placeholders that are replaced with user input from the fields form. + +All JSON files are plain static files — no build step required. + +--- + +## How to Add a New Feature + +1. **New card** — add a `.card` inside the appropriate `.content-col` in `index.html`. Give the dynamic content container a unique `id`. +2. **New webhook call** — declare the URL as a `const` at the top of `app.js`. Use `btnLoad` / `btnReset` for the trigger button and wrap the call in `try/catch` with `toast(err.message, 'error')` in the catch. +3. **Destructive action** — always gate with `await confirmDialog(...)` before the fetch. +4. **New CSS component** — add it to `styles.css` in the appropriate section (marked with `/* ─── Section ─ */` comments). Use only existing token variables. +5. **New agent-tab action** — call it from `selectAgent()` so it reloads when the user switches agents. Note: `loadWizard(info)` is called from inside `loadAgentInfo()` (which is called by `selectAgent()`), not directly from `selectAgent()` — this is acceptable when the loader depends on async data fetched by another loader. +6. **Demo shell** — update `populateDemoShell()` to populate the new card's `id` with representative static data so the auth screen preview stays consistent. + +--- + +## Commands + +```bash +# Start dev server (live-reload on file save) +./start +# → http://localhost:8080 + +# No build step required — CSS and JS are plain files. +``` + +--- + +## Rules + +- **NEVER** add external scripts, CDN links, or npm packages. +- **NEVER** introduce a framework (React, Vue, Alpine, etc.). +- **NEVER** use raw hex colour values — reference CSS token variables. +- **NEVER** scroll the outer page — keep `overflow: hidden` on `body`. +- **NEVER** call a destructive webhook (restart, restore, password update) without `confirmDialog`. +- On successful sign-up, open the Stripe checkout tab **and** log the user in immediately via `showApp()` + `loadAgents()`. +- **ALWAYS** use `escHtml()` when inserting user-supplied or API-returned strings into `innerHTML`. +- **ALWAYS** use `btnLoad` / `btnReset` around async operations on buttons. +- **ALWAYS** show an error toast (`toast(err.message, 'error')`) when a webhook call fails. +- **ALWAYS** call new per-agent loaders from `selectAgent()` and populate them in `populateDemoShell()`. + +--- + +## Failure Conditions + +These actions constitute a failure: + +- Removing `escHtml()` from any innerHTML insertion of external data. +- Skipping `confirmDialog` before restart, password update, or backup restore. +- Adding a ` + + diff --git a/integrations/imap-smtp-email.json b/integrations/imap-smtp-email.json new file mode 100644 index 0000000..ef0e719 --- /dev/null +++ b/integrations/imap-smtp-email.json @@ -0,0 +1,14 @@ +{ + "name": "IMAP / SMTP Email", + "description": "Read, send, and manage email from any standard mailbox.", + "fields": [ + {"label": "Email Address", "key": "email", "type": "text", "placeholder": "you@yourdomain.com"}, + {"label": "Your Name", "key": "display_name", "type": "text", "placeholder": "Jane Smith"}, + {"label": "IMAP Host", "key": "imap_host", "type": "text", "placeholder": "imap.hostinger.com"}, + {"label": "SMTP Host", "key": "smtp_host", "type": "text", "placeholder": "smtp.hostinger.com"}, + {"label": "Password", "key": "password", "type": "password", "placeholder": ""} + ], + "signup_url": "https://hostinger.com", + "signup_label": "Need a domain or email? Sign up at Hostinger", + "prompt": "Create a skill for IMAP / SMTP Email.\n\nUse the himalaya CLI to read, send, and manage emails.\n\n## Install himalaya (if not already installed)\n\ncurl -sSL https://raw.githubusercontent.com/pimalaya/himalaya/master/install.sh | PREFIX=~/.local sh\nexport PATH=\"$HOME/.local/bin:$PATH\" # add to ~/.bashrc if not present\nhimalaya --version\n\n## Write the config file\n\nCreate ~/.config/himalaya/config.toml with exactly this content:\n\n[accounts.default]\ndefault = true\nemail = \"{email}\"\ndisplay-name = \"{display_name}\"\n\nfolder.aliases.inbox = \"INBOX\"\nfolder.aliases.sent = \"Sent\"\nfolder.aliases.drafts = \"Drafts\"\nfolder.aliases.trash = \"Trash\"\n\nbackend.type = \"imap\"\nbackend.host = \"{imap_host}\"\nbackend.port = 993\nbackend.encryption.type = \"tls\"\nbackend.login = \"{email}\"\nbackend.auth.type = \"password\"\nbackend.auth.raw = \"{password}\"\n\nmessage.send.backend.type = \"smtp\"\nmessage.send.backend.host = \"{smtp_host}\"\nmessage.send.backend.port = 587\nmessage.send.backend.encryption.type = \"start-tls\"\nmessage.send.backend.login = \"{email}\"\nmessage.send.backend.auth.type = \"password\"\nmessage.send.backend.auth.raw = \"{password}\"\n\n## Key commands\n\nhimalaya folder list\nhimalaya envelope list\nhimalaya envelope list -f Sent\nhimalaya message read \nhimalaya message reply \nhimalaya message forward \nhimalaya message delete \nhimalaya --output json envelope list\n\n## Sending email\n\nprintf 'From: {display_name} <{email}>\\nTo: recipient@example.com\\nSubject: Hello\\n\\nBody text here.' | himalaya message send\n\n## Solved cases log\n\nEvery time you successfully solve an email task, append a short entry to ~/email_solutions.md:\n\n ## ()\n Goal: \n Key commands: \n Gotchas: \n\nRead ~/email_solutions.md at the start of each task — if a matching solved case exists, reuse it directly." +} diff --git a/integrations/index.json b/integrations/index.json new file mode 100644 index 0000000..4b097b0 --- /dev/null +++ b/integrations/index.json @@ -0,0 +1 @@ +["odoo-community.json", "imap-smtp-email.json", "n8n-crm.json"] diff --git a/integrations/n8n-crm.json b/integrations/n8n-crm.json new file mode 100644 index 0000000..3931ae3 --- /dev/null +++ b/integrations/n8n-crm.json @@ -0,0 +1,13 @@ +{ + "name": "N8N CRM", + "description": "Manage contacts and deal stages via your n8n webhook CRM.", + "fields": [ + {"label": "Webhook Base URL", "key": "webhook_url", "type": "text", "placeholder": "https://your-n8n.example.com/webhook/crm"}, + {"label": "Auth Header Name", "key": "auth_header_name", "type": "text", "placeholder": "Authorization"}, + {"label": "Auth Header Value", "key": "auth_header_value", "type": "password", "placeholder": ""}, + {"label": "Example Contact JSON (paste one record from your CRM)", "key": "example_json", "type": "textarea", "placeholder": "{\n \"email\": \"alice@example.com\",\n \"name\": \"Alice Smith\",\n \"company\": \"Acme Corp\",\n \"stage\": \"Cold\",\n \"followup\": false,\n \"notes\": \"\"\n}"} + ], + "signup_url": "https://n8n.io", + "signup_label": "Need a webhook backend? Try n8n — self-host or cloud", + "prompt": "Create a skill for an N8N CRM backed by a webhook.\n\n## Configuration\n\nWEBHOOK_URL={webhook_url}\nAUTH_HEADER_NAME={auth_header_name}\nAUTH_HEADER_VALUE={auth_header_value}\n\n## Example record schema (auto-detected from user input)\n\n{example_json}\n\nParse the keys of the JSON above to discover the available columns. Always use exactly those column names when building payloads or displaying data. The column named \"email\" is the unique identifier for every contact.\n\n## Stages\n\n| Stage | Meaning |\n|------------|--------------------------------|\n| Cold | No contact so far |\n| Contacted | First message / email sent |\n| Warm | Response received |\n| Won | Deal closed successfully |\n| Lost | Deal closed unsuccessfully |\n\n## API contract\n\n### List contacts\n\n curl -s -H \"$AUTH_HEADER_NAME: $AUTH_HEADER_VALUE\" \"$WEBHOOK_URL\"\n curl -s -H \"$AUTH_HEADER_NAME: $AUTH_HEADER_VALUE\" \"$WEBHOOK_URL?stage=Warm\"\n curl -s -H \"$AUTH_HEADER_NAME: $AUTH_HEADER_VALUE\" \"$WEBHOOK_URL?email=alice@example.com\"\n curl -s -H \"$AUTH_HEADER_NAME: $AUTH_HEADER_VALUE\" \"$WEBHOOK_URL?followup=true\"\n\n### Create or update a contact (upsert by email)\n\n curl -s -X POST \"$WEBHOOK_URL\" \\\n -H \"$AUTH_HEADER_NAME: $AUTH_HEADER_VALUE\" \\\n -H 'Content-Type: application/json' \\\n -d '{\"email\":\"alice@example.com\",\"name\":\"Alice Smith\",\"stage\":\"Contacted\",\"followup\":false}'\n\n### Delete a contact\n\n curl -s -X DELETE \"$WEBHOOK_URL\" \\\n -H \"$AUTH_HEADER_NAME: $AUTH_HEADER_VALUE\" \\\n -H 'Content-Type: application/json' \\\n -d '{\"email\":\"alice@example.com\"}'\n\n## Solved cases log\n\nAfter every successful CRM task, append an entry to ~/crm_solutions.md:\n\n ## ()\n Goal: \n Key commands: \n Gotchas: \n\nRead ~/crm_solutions.md at task start — if a matching case exists, reuse it directly." +} diff --git a/integrations/odoo-community.json b/integrations/odoo-community.json new file mode 100644 index 0000000..bab253c --- /dev/null +++ b/integrations/odoo-community.json @@ -0,0 +1,13 @@ +{ + "name": "ODOO Community", + "description": "Automate ERP tasks — sales, accounting, and inventory — via REST API.", + "fields": [ + {"label": "Site URL", "key": "site_url", "type": "text", "placeholder": "https://ODOO4projects.com"}, + {"label": "Username", "key": "username", "type": "text", "placeholder": "admin"}, + {"label": "Password / API Key", "key": "api_key", "type": "password", "placeholder": ""}, + {"label": "Database Name", "key": "db_name", "type": "text", "placeholder": "mycompany"} + ], + "signup_url": "https://ODOO4projects.com", + "signup_label": "Need ODOO Community hosting? Sign up here", + "prompt": "create a skill for ODOO community. You are an Odoo automation agent. Your only tools are curl and jq. Never write Python scripts, never develop modules or addons, never use xmlrpc libraries. Every task must be solved with shell one-liners or short shell scripts using curl + jq only.\n\n## Credentials\n\nSITE_URL={site_url}\nUSER={username}\nAPI_KEY={api_key}\nDB={db_name}\n\n## API call pattern\n\nEvery call is a POST to {site_url}/json/2//\nRequired headers on every request:\n Authorization: Bearer {api_key}\n X-Openerp-Database: {db_name}\n Content-Type: application/json\n\nBody: a FLAT JSON object of the method arguments (NOT JSON-RPC — no \"params\" wrapper).\nResponse: the raw return value directly (no \"result\" wrapper).\n\n## Always request only the fields you need\n\nNever call search_read without a \"fields\" list. Fetching all fields is wasteful and slow.\nDecide upfront which fields the task actually requires, then pass exactly those.\n\n curl -s -X POST '{site_url}/json/2/sale.order/search_read' \\\n -H 'Authorization: Bearer {api_key}' \\\n -H 'X-Openerp-Database: {db_name}' \\\n -H 'Content-Type: application/json' \\\n -d '{\"domain\": [[\"state\",\"=\",\"sale\"]], \"fields\": [\"name\",\"partner_id\",\"amount_total\"], \"limit\": 20}' \\\n | jq '.[] | {order: .name, customer: .partner_id[1], total: .amount_total}'\n\nUse fields_get only when you genuinely do not know a model's field names:\n curl -s -X POST '{site_url}/json/2/account.move/fields_get' \\\n -H 'Authorization: Bearer {api_key}' \\\n -H 'X-Openerp-Database: {db_name}' \\\n -H 'Content-Type: application/json' \\\n -d '{\"attributes\": [\"string\",\"type\"]}' \\\n | jq 'to_entries[] | {field: .key, label: .value.string, type: .value.type}'\n\n## First-run: build your API digest\n\nIf ~/odoo_digest.md does NOT exist yet, run this once and write the output to the file:\n\n curl -s '{site_url}/json/2/doc' \\\n -H 'Authorization: Bearer {api_key}' \\\n -H 'X-Openerp-Database: {db_name}' \\\n | jq '[to_entries[] | {model: .key, label: .value.string}]' > /tmp/odoo_models.json\n\nFrom /tmp/odoo_models.json, write ~/odoo_digest.md containing:\n - The standard ORM methods available on every model\n - A compact table of installed business models with their technical name and human label\n - The exact one-liner curl template for POST calls\n - Any non-standard or custom routes found in /doc\n\nOn every future task, read ~/odoo_digest.md first.\n\n## Solved cases log\n\nAfter successfully solving any business task, append to ~/odoo_solutions.md:\n\n ## ()\n Goal: \n Models: \n Fields queried: \n Key calls: \n Gotchas: \n\nRead ~/odoo_solutions.md at task start — if a matching case exists, reuse it directly." +} diff --git a/skills/cold-email.json b/skills/cold-email.json new file mode 100644 index 0000000..d716dcc --- /dev/null +++ b/skills/cold-email.json @@ -0,0 +1,5 @@ +{ + "name": "Cold Email", + "description": "Craft B2B outreach sequences that get replies, not unsubscribes.", + "prompt": "Create the skill Cold Email.\n\nYou are an expert cold email writer. Write B2B outreach emails that sound like they came from a sharp, thoughtful human — not a sales machine.\n\nBefore writing, understand: who they are writing to and why, the desired outcome (reply, meeting, demo), the specific problem solved and value delivered, a proof point or credibility signal, and any research signals (funding, hiring, news, tech stack). Work with whatever is provided — do not block on missing inputs.\n\nPrinciples: Write like a peer sharing something relevant, not a vendor pitching. Every sentence must earn its place — if it does not move toward a reply, cut it. Personalization must connect to the problem — if removing it leaves the email intact, it is not working. Lead with their world (use you/your more than I/we). One low-friction ask per email — \"Worth a quick look?\" beats \"Can we book 30 minutes?\"\n\nCommon structures: Observation → Problem → Proof → Ask. Question → Value → Ask. Trigger → Insight → Ask.\n\nSubject lines: 2-4 words, lowercase, no punctuation, internal-looking. Never pitch in the subject line.\n\nFollow-up sequences: 3-5 emails with increasing gaps. Each one adds a new angle or proof point. Never send a \"just checking in\" follow-up.\n\nAvoid: \"I hope this email finds you well,\" feature lists, synergy/leverage/best-in-class, HTML or images, fake Re:/Fwd: subject lines, asking for a 30-minute call on first touch." +} diff --git a/skills/competitor-profiling.json b/skills/competitor-profiling.json new file mode 100644 index 0000000..cd81903 --- /dev/null +++ b/skills/competitor-profiling.json @@ -0,0 +1,5 @@ +{ + "name": "Competitor Profiling", + "description": "Profile competitors using live web scraping and SEO data.", + "prompt": "Create the skill Competitor Profiling.\n\nYou are an expert competitive intelligence analyst. Take a list of competitor URLs and produce structured competitor profile documents by combining live site scraping with SEO and market data.\n\nBefore profiling, confirm: competitor URLs, your product, depth level (quick scan vs. deep profile), and any focus areas. If .agents/product-marketing.md exists, read it first and only ask for what is not covered.\n\nCore principles: Facts over opinions — every claim must be traceable to a source. Consistent template across all profiles. Include the date generated. Honest assessment of strengths and weaknesses.\n\nResearch process:\n1. Site scraping (Firecrawl): Map the site, scrape key pages (homepage, pricing, features, about, customers, integrations, changelog). Extract positioning, features, pricing, and proof. Optionally scrape G2/Capterra/Product Hunt reviews.\n2. SEO data (DataForSEO): Domain authority, backlinks, referring domains, ranked keywords, organic traffic estimates, and top competitor domains.\n3. Synthesis: Combine scraped content with SEO data into the profile. Cross-reference claims against traffic and backlink scale.\n\nSave all raw scrape and SEO data to competitor-profiles/raw/// before synthesizing.\n\nOutput one markdown profile per competitor at competitor-profiles/.md. After all profiles are done, generate competitor-profiles/_summary.md with a landscape overview, side-by-side comparison table, positioning map, key takeaways, and market gaps.\n\nDefault to quick scan (homepage + pricing only, domain overview + keywords) unless the user requests a deep profile or provides 3 or fewer competitors." +} diff --git a/skills/conversion-optimization.json b/skills/conversion-optimization.json new file mode 100644 index 0000000..f57dee3 --- /dev/null +++ b/skills/conversion-optimization.json @@ -0,0 +1,5 @@ +{ + "name": "Conversion Optimization", + "description": "Audit pages and prioritise changes to lift conversion rates.", + "prompt": "Create the skill Conversion Optimization.\n\nYou are a conversion rate optimization expert. Analyze marketing pages and provide actionable recommendations to improve conversion rates.\n\nBefore analyzing, identify: page type (homepage, landing page, pricing, feature, blog), primary conversion goal (signup, demo request, purchase, download), and traffic source. Read .agents/product-marketing.md first if it exists.\n\nCRO analysis framework — evaluate in this order of impact:\n1. Value proposition clarity: Can a visitor understand what this is and why they should care within 5 seconds? Is the benefit specific and in customer language, not company jargon?\n2. Headline effectiveness: Does it communicate the core value prop? Is it specific enough to be meaningful? Does it match the traffic source messaging?\n3. CTA placement and copy: One clear primary action visible without scrolling. Button copy communicates what they get — \"Start Free Trial\" not \"Submit.\" CTAs repeated at key decision points down the page.\n4. Visual hierarchy and scannability: Main message readable while scanning. Most important elements visually prominent. Adequate white space.\n5. Trust signals and social proof: Customer logos, specific attributed testimonials with real numbers, review scores near CTAs.\n6. Objection handling: Address price concerns, fit anxiety, implementation difficulty, and risk through FAQs, guarantees, and comparison content.\n7. Friction points: Excess form fields, unclear next steps, mobile experience issues, slow load times.\n\nOutput structure:\n- Quick Wins: Easy changes with likely immediate impact\n- High-Impact Changes: Bigger effort, significant conversion improvement\n- Test Ideas: Hypotheses worth A/B testing\n- Copy Alternatives: 2-3 options for headlines and CTAs with rationale" +} diff --git a/skills/copywriting.json b/skills/copywriting.json new file mode 100644 index 0000000..559f499 --- /dev/null +++ b/skills/copywriting.json @@ -0,0 +1,5 @@ +{ + "name": "Copywriting", + "description": "Write conversion-focused copy for landing pages, ads, and emails.", + "prompt": "Create the skill Copywriting.\n\nYou are an expert conversion copywriter. Write marketing copy that is clear, compelling, and drives action.\n\nBefore writing, gather: page type and the single primary action, target audience and their problem, objections, and language, product differentiators and proof points, and traffic source. Read .agents/product-marketing.md first if it exists.\n\nPrinciples: Clarity over cleverness. Benefits over features — what does it mean for the customer, not what does it do. Specificity over vagueness — use numbers and timeframes. Use customer language, not company jargon. One idea per section. Active voice. Remove qualifiers. No buzzwords without substance.\n\nPage structure:\n- Above the fold: Headline (core value prop, specific), Subheadline (1-2 sentences expanding), Primary CTA (action + what they get)\n- Body sections: Social proof, Problem/Pain, Solution/Benefits (3-5), How It Works (3-4 steps), Objection Handling, Final CTA\n\nCTA copy: Action verb + what they get. Strong: \"Start Free Trial,\" \"Get [Specific Thing],\" \"See Pricing for My Team.\" Weak: Submit, Sign Up, Learn More, Get Started.\n\nOutput: Page copy organized by section, brief annotations on key choices, and 2-3 alternatives for headlines and primary CTAs with rationale." +} diff --git a/skills/customer-research.json b/skills/customer-research.json new file mode 100644 index 0000000..db35ac6 --- /dev/null +++ b/skills/customer-research.json @@ -0,0 +1,5 @@ +{ + "name": "Customer Research", + "description": "Mine transcripts, reviews, and communities for real customer insights.", + "prompt": "Create the skill Customer Research.\n\nYou are an expert customer researcher. Uncover what customers actually think, feel, say, and struggle with — so positioning, product, and copy are grounded in reality rather than assumption.\n\nBefore starting, clarify: the research goal (improve messaging, build personas, find product gaps, understand churn), what assets already exist (transcripts, surveys, tickets, reviews, or nothing), the target segment, and the desired deliverable. Read .agents/product-marketing.md first if it exists. Lead with these two questions first, then follow up as needed.\n\nTwo modes:\n\nMode 1 — Analyze existing assets (transcripts, surveys, support tickets, win/loss notes, NPS responses):\nFrom each asset extract: Jobs to Be Done (functional, emotional, and social), Pain Points (prioritize those mentioned unprompted with emotional language), Trigger Events (what changed that made them seek a solution), Desired Outcomes (in exact customer words — never paraphrase), Language and Vocabulary (direct copy fuel), and Alternatives Considered.\nSynthesize by clustering themes, scoring frequency x intensity, segmenting by customer profile, collecting the best verbatim quotes, and flagging contradictions. Label confidence: High (3+ independent sources, unprompted), Medium (2 sources or prompted), Low (single source).\n\nMode 2 — Digital research (Reddit, G2/Capterra, Hacker News, LinkedIn, app store reviews, communities):\nB2B buyers: G2, LinkedIn, role-specific subreddits. SMB/founders: Reddit, Indie Hackers. Developers: Hacker News, r/devops. B2C: 1-3 star app store reviews, lifestyle subreddits.\nFor every piece found, capture: verbatim quote, source URL and date, sentiment, theme tag, and customer profile signals. Never paraphrase.\n\nDeliverables — ask which is needed before generating: research synthesis report, VOC quote bank organized by theme, persona documents, JTBD map, competitive intelligence summary, or research gap analysis.\n\nPersona guardrail: Do not build a persona from fewer than 5 independent data points per segment." +} diff --git a/skills/index.json b/skills/index.json new file mode 100644 index 0000000..a7c63d0 --- /dev/null +++ b/skills/index.json @@ -0,0 +1,9 @@ +[ + "competitor-profiling.json", + "copywriting.json", + "cold-email.json", + "social-media.json", + "video-production.json", + "conversion-optimization.json", + "customer-research.json" +] diff --git a/skills/social-media.json b/skills/social-media.json new file mode 100644 index 0000000..cc52f9d --- /dev/null +++ b/skills/social-media.json @@ -0,0 +1,5 @@ +{ + "name": "Social Media", + "description": "Plan and produce platform-specific content for LinkedIn, X, Instagram, and TikTok.", + "prompt": "Create the skill Social Media.\n\nYou are an expert social media strategist. Create engaging content that builds audience, drives engagement, and supports business goals across LinkedIn, Twitter/X, Instagram, TikTok, and Facebook.\n\nBefore creating, gather: primary objective (awareness, leads, traffic, community), target audience and active platforms, brand voice and any topics to avoid, and available time and existing content to repurpose. Read .agents/product-marketing.md first if it exists.\n\nPlatform quick reference: LinkedIn (B2B/thought leadership, 3-5x/week, carousels and long posts). Twitter/X (tech and community, 3-10x/day, threads and hot takes). Instagram (visual brands, 1-2 posts + Stories daily, Reels and carousels). TikTok (awareness, 1-4x/day, short-form video). Facebook (communities, 1-2x/day).\n\nContent pillars: Build 3-5 pillars — e.g. industry insights 30%, educational 25%, behind-the-scenes 25%, personal 15%, promotional 5%.\n\nHook formulas: Curiosity (\"I was wrong about [X]\"), Story (\"Last week, X happened\"), Value (\"How to [outcome] without [pain]:\"), Contrarian (\"Unpopular opinion: [bold statement]\").\n\nRepurposing system: Extract 5-10 content atoms from each long-form piece — quotes, tips, data, story arcs — and adapt format and tone for each platform. Spread posts across the week.\n\nShort-form video (Reels, TikTok, Shorts): Hook in the first 3 seconds with simultaneous visual hook + verbal hook + text overlay. Always add captions — most social video is watched without sound. Common structures: Problem-Solution (15-30 sec), List Format (30-60 sec), Tutorial showing end result first." +} diff --git a/skills/video-production.json b/skills/video-production.json new file mode 100644 index 0000000..679d287 --- /dev/null +++ b/skills/video-production.json @@ -0,0 +1,5 @@ +{ + "name": "Video Production", + "description": "Produce marketing videos using AI generation, avatars, and programmatic tools.", + "prompt": "Create the skill Video Production.\n\nYou are an expert video producer. Help create marketing videos using AI generation models, AI avatars, and programmatic frameworks.\n\nBefore starting, gather: video type (demo, explainer, social clip, ad, tutorial), target platform and desired length, whether a human presenter is needed, existing assets (screenshots, footage, scripts), and tech stack and budget. Read .agents/product-marketing.md first if it exists.\n\nChoose the right approach:\n- Programmatic (Hyperframes or Remotion): For templated, data-driven, or batch video. Hyperframes uses plain HTML/CSS and is the preferred choice for agents — any coding agent can generate frames without learning a framework. Remotion uses React for complex animations.\n- AI Generation (Veo 3, Sora 2, Runway, Kling, Seedance): For original footage from text or image prompts — B-roll, hero visuals, scenes you cannot film. Prompts must specify subject + action + camera movement + style + mood. AI models cannot reliably render readable text — use programmatic overlays for titles.\n- AI Avatars (HeyGen, Synthesia): For talking-head presenter videos without filming. HeyGen has an official MCP server so agents can generate avatar videos directly.\n- Editing and repurposing (Descript, Opus Clip, CapCut): For cutting long-form content into short social clips.\n\nAgent-native pipeline: Agent writes script → Hyperframes generates templated video → HeyGen MCP generates avatar video → video model API generates B-roll → agent assembles final output.\n\nCommon mistakes: Choosing tools before defining the video goal. AI-generated text is unreadable — always use programmatic overlays. Skipping captions (85% of social video is watched without sound). Wrong aspect ratio — 9:16 for social, 16:9 for YouTube and website, 1:1 for feeds." +} diff --git a/start b/start new file mode 100755 index 0000000..a1db1d4 --- /dev/null +++ b/start @@ -0,0 +1,21 @@ +#!/bin/bash + + +#!/bin/bash + +# Simple static file server with live reload +# Requires Node.js and live-server + +# Check if live-server is installed +if ! command -v live-server &> /dev/null +then + echo "Installing live-server..." + npm install -g live-server +fi + +# Serve the current directory with auto-reload +echo "Starting live-server on http://localhost:8080 ..." +live-server . + + + diff --git a/styles.css b/styles.css new file mode 100644 index 0000000..4b32f28 --- /dev/null +++ b/styles.css @@ -0,0 +1,1905 @@ +/* ─── Reset ──────────────────────────────────────────────────── */ +*, +*::before, +*::after { + box-sizing: border-box; + margin: 0; + padding: 0; +} + +/* ─── Design tokens ──────────────────────────────────────────── */ +:root { + --bg: #0f1117; + --bg-card: #1a1d27; + --bg-inner: #141720; + --bg-hover: #22263a; + --bg-input: #12151f; + --border: #2a2f45; + --border-hi: rgba(79, 142, 247, 0.28); + --accent: #4f8ef7; + --accent-dim: #1a2d5e; + --danger: #e05252; + --danger-dim: #5a1f1f; + --success: #3fc97e; + --warning: #f0a04b; + --text: #e4e8f5; + --text-muted: #7a82a0; + --radius: 8px; + --shadow: + 0 28px 72px rgba(0, 0, 0, 0.72), 0 0 0 1px rgba(79, 142, 247, 0.06); +} + +/* ─── Base ───────────────────────────────────────────────────── */ +html, +body { + height: 100%; + font-family: + "Segoe UI", + system-ui, + -apple-system, + sans-serif; + background: var(--bg); + color: var(--text); + font-size: 14px; + overflow: hidden; + display: flex; + align-items: center; + justify-content: center; +} + +/* Subtle ambient glow in background */ +body::before { + content: ""; + position: fixed; + inset: 0; + background: + radial-gradient( + ellipse 60% 50% at 25% 30%, + rgba(79, 142, 247, 0.07) 0%, + transparent 70% + ), + radial-gradient( + ellipse 50% 60% at 75% 70%, + rgba(100, 50, 200, 0.05) 0%, + transparent 70% + ); + pointer-events: none; + z-index: 0; +} + +/* ─── AUTH CARD ─────────────────────────────────────────────── */ +.auth-card { + position: relative; + z-index: 10; + width: min(440px, calc(100vw - 24px)); + background: var(--bg-card); + border: 1px solid var(--border-hi); + border-radius: 18px; + box-shadow: var(--shadow); + overflow: hidden; + animation: fadeUp 0.3s ease both; +} + +@keyframes fadeUp { + from { + opacity: 0; + transform: translateY(18px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +.auth-logo { + text-align: center; + padding: 36px 24px 20px; +} + +.auth-logo-icon { + display: inline-flex; + align-items: center; + justify-content: center; + width: 56px; + height: 56px; + border-radius: 16px; + background: var(--accent-dim); + border: 1px solid rgba(79, 142, 247, 0.3); + color: var(--accent); + margin-bottom: 14px; +} + +.auth-title { + font-size: 22px; + font-weight: 700; + color: var(--text); + letter-spacing: -0.3px; +} + +.auth-sub { + font-size: 13px; + color: var(--text-muted); + margin-top: 4px; +} + +/* Auth tab switch */ +.auth-switch { + display: flex; + gap: 0; + margin: 4px 24px 0; + background: var(--bg-inner); + border: 1px solid var(--border); + border-radius: 10px; + padding: 3px; +} + +.auth-switch-btn { + flex: 1; + padding: 8px; + border: none; + border-radius: 7px; + background: transparent; + color: var(--text-muted); + font-size: 13px; + font-weight: 500; + cursor: pointer; + transition: + background 0.15s, + color 0.15s; +} + +.auth-switch-btn.active { + background: var(--bg-card); + color: var(--text); + box-shadow: 0 1px 4px rgba(0, 0, 0, 0.3); +} + +.auth-switch-btn:hover:not(.active) { + color: var(--text); +} + +/* Auth form */ +.auth-form { + display: flex; + flex-direction: column; + gap: 12px; + padding: 20px 24px 28px; +} + +.auth-error { + display: none; + align-items: flex-start; + gap: 8px; + padding: 9px 12px; + background: var(--danger-dim); + border: 1px solid var(--danger); + border-radius: 6px; + font-size: 12px; + color: var(--danger); + line-height: 1.5; +} + +.auth-success { + display: none; + align-items: flex-start; + gap: 10px; + padding: 12px 14px; + background: #0e2e1c; + border: 1px solid var(--success); + border-radius: 6px; + font-size: 13px; + color: var(--success); + line-height: 1.55; +} + +/* ─── APP SHELL ─────────────────────────────────────────────── */ +.app-shell { + position: relative; + z-index: 10; + display: none; /* shown by JS */ + flex-direction: column; + width: min(1060px, calc(100vw - 16px)); + height: min(780px, calc(100vh - 16px)); + background: var(--bg-card); + border: 1px solid var(--border-hi); + border-radius: 18px; + box-shadow: var(--shadow); + overflow: hidden; + animation: fadeUp 0.25s ease both; +} + +/* App header */ +.app-header { + display: flex; + align-items: center; + justify-content: space-between; + height: 54px; + padding: 0 22px; + border-bottom: 1px solid var(--border); + background: linear-gradient( + 90deg, + rgba(79, 142, 247, 0.06) 0%, + transparent 60% + ); + flex-shrink: 0; + gap: 12px; +} + +.app-brand { + display: flex; + align-items: center; + gap: 9px; + font-weight: 700; + font-size: 15px; + color: var(--accent); + letter-spacing: 0.2px; + flex-shrink: 0; +} + +.header-comment { + flex: 1; + display: flex; + justify-content: center; + align-items: center; + padding: 0 12px; + min-width: 0; +} +#header-comment-display { + cursor: pointer; + font-size: 13px; + color: var(--text); + max-width: 360px; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + padding: 3px 8px; + border-radius: var(--radius); + border: 1px solid transparent; + transition: + border-color 0.15s, + color 0.15s; +} +#header-comment-display:hover { + border-color: var(--border); +} +#header-comment-display.comment-empty { + color: var(--text-muted); + font-style: italic; +} +#header-comment-edit { + align-items: center; + gap: 6px; +} +#header-comment-input { + background: var(--bg-input); + border: 1px solid var(--accent); + border-radius: 6px; + color: var(--text); + padding: 4px 10px; + font-size: 13px; + width: 260px; + outline: none; +} + +.app-user { + display: flex; + align-items: center; + gap: 10px; + position: relative; +} + +.user-pill { + display: flex; + align-items: center; + gap: 7px; + padding: 4px 10px; + background: var(--bg-inner); + border: 1px solid var(--border); + border-radius: 20px; + font-size: 12px; + color: var(--text-muted); + cursor: pointer; + transition: + border-color 0.15s, + color 0.15s; + user-select: none; +} + +.user-pill:hover { + border-color: var(--accent); + color: var(--text); +} + +/* ─── User account panel dropdown ─────────────────────────── */ +.user-panel { + position: absolute; + top: calc(100% + 10px); + right: 0; + min-width: 264px; + background: var(--bg-card); + border: 1px solid var(--border); + border-radius: var(--radius); + box-shadow: var(--shadow); + z-index: 200; + overflow: hidden; + animation: panelIn 0.15s ease; +} + +@keyframes panelIn { + from { + opacity: 0; + transform: translateY(-6px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +.user-panel-header { + display: flex; + align-items: center; + gap: 7px; + padding: 11px 14px 10px; + font-size: 11px; + font-weight: 600; + color: var(--text-muted); + text-transform: uppercase; + letter-spacing: 0.06em; + border-bottom: 1px solid var(--border); + background: var(--bg-inner); +} + +.user-panel-row { + display: flex; + align-items: center; + justify-content: space-between; + padding: 10px 14px; + gap: 12px; +} + +.user-panel-label { + font-size: 11px; + color: var(--text-muted); + flex-shrink: 0; +} + +.user-panel-val { + font-size: 12px; + color: var(--text); + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + max-width: 180px; +} + +.user-panel-divider { + height: 1px; + background: var(--border); + margin: 0; +} + +.user-panel-pw { + padding: 12px 14px 14px; +} + +.user-panel-pw .form-group { + margin-bottom: 4px; +} + +.pulse-dot { + width: 7px; + height: 7px; + border-radius: 50%; + background: var(--success); + flex-shrink: 0; + animation: pulse 2s ease infinite; +} + +@keyframes pulse { + 0%, + 100% { + box-shadow: 0 0 0 0 rgba(63, 201, 126, 0.5); + } + 50% { + box-shadow: 0 0 0 4px rgba(63, 201, 126, 0); + } +} + +/* Agent tabs bar */ +.agent-tabs-bar { + display: flex; + align-items: center; + gap: 6px; + padding: 0 20px; + height: 50px; + border-bottom: 1px solid var(--border); + background: var(--bg-inner); + flex-shrink: 0; + overflow-x: auto; + scrollbar-width: none; +} + +.agent-tabs-bar::-webkit-scrollbar { + display: none; +} + +.agent-tab { + display: inline-flex; + align-items: center; + gap: 7px; + padding: 6px 14px; + border-radius: 20px; + border: 1px solid var(--border); + background: transparent; + color: var(--text-muted); + font-size: 13px; + font-weight: 500; + cursor: pointer; + white-space: nowrap; + transition: + border-color 0.15s, + background 0.15s, + color 0.15s; + flex-shrink: 0; +} + +.agent-tab:hover:not(.active) { + border-color: rgba(79, 142, 247, 0.4); + color: var(--text); + background: var(--bg-hover); +} + +.agent-tab.active { + background: var(--accent-dim); + border-color: var(--accent); + color: var(--accent); +} + +.agent-tab-dot { + width: 7px; + height: 7px; + border-radius: 50%; + background: var(--success); + flex-shrink: 0; +} + +.agent-tab-badge { + font-size: 10px; + font-weight: 600; + padding: 1px 6px; + border-radius: 4px; + background: var(--bg-hover); + color: var(--text-muted); + letter-spacing: 0.04em; +} + +.agent-tab.active .agent-tab-badge { + background: rgba(79, 142, 247, 0.2); + color: var(--accent); +} + +.tabs-loading, +.tabs-empty, +.tabs-error { + display: flex; + align-items: center; + gap: 8px; + font-size: 13px; + color: var(--text-muted); + padding: 0 4px; +} + +.tabs-error { + color: var(--danger); +} + +/* App body */ +.app-body { + flex: 1; + overflow: hidden; + position: relative; + display: flex; + flex-direction: column; +} + +/* Empty / no-agents state */ +.empty-center { + flex: 1; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + gap: 10px; + color: var(--text-muted); + font-size: 14px; +} + +.empty-center svg { + opacity: 0.2; +} + +/* Agent panel + content grid */ +#agent-panel { + flex: 1; + overflow: hidden; + display: flex; + flex-direction: column; +} + +.content-grid { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 16px; + padding: 18px 20px; + flex: 1; + overflow-y: auto; + align-content: start; + scrollbar-width: thin; + scrollbar-color: var(--border) transparent; +} + +.content-grid::-webkit-scrollbar { + width: 5px; +} +.content-grid::-webkit-scrollbar-track { + background: transparent; +} +.content-grid::-webkit-scrollbar-thumb { + background: var(--border); + border-radius: 4px; +} + +.content-col { + display: flex; + flex-direction: column; + gap: 14px; + min-width: 0; + position: relative; +} + +/* ─── Cards ─────────────────────────────────────────────────── */ +.card { + background: var(--bg-inner); + border: 1px solid var(--border); + border-radius: var(--radius); + padding: 16px 18px; +} + +.card-full { + flex: 1; +} + +.card-header { + display: flex; + align-items: center; + justify-content: space-between; + margin-bottom: 14px; + gap: 10px; + flex-wrap: wrap; +} + +.card-title { + display: flex; + align-items: center; + gap: 7px; + font-size: 13px; + font-weight: 600; + color: var(--text); +} + +/* ─── Keys ──────────────────────────────────────────────────── */ +.key-row { + display: flex; + align-items: flex-start; + gap: 12px; + padding: 6px 0; +} + +.key-row + .key-row { + border-top: 1px solid var(--border); + padding-top: 12px; + margin-top: 6px; +} + +.key-info { + flex: 1; + min-width: 0; +} + +.key-name { + font-size: 13px; + font-weight: 600; + color: var(--text); + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.key-label { + display: block; + font-family: "Cascadia Code", "Fira Code", monospace; + font-size: 11px; + color: var(--text-muted); + margin-top: 2px; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.key-stats { + display: flex; + align-items: center; + gap: 6px; + margin-top: 5px; + font-size: 12px; + color: var(--text-muted); + flex-wrap: wrap; +} + +.key-stat-val { + font-weight: 600; + color: var(--text); +} + +.sep-dot { + color: var(--border); +} + +/* ─── SSH ───────────────────────────────────────────────────── */ +.ssh-wrap { + display: flex; + gap: 8px; + align-items: flex-start; +} + +.ssh-wrap textarea { + flex: 1; + background: var(--bg-input); + border: 1px solid var(--border); + border-radius: 6px; + color: var(--text); + padding: 8px 10px; + font-size: 12px; + font-family: "Cascadia Code", "Fira Code", monospace; + outline: none; + resize: none; + transition: border-color 0.15s; + line-height: 1.5; +} + +.ssh-wrap textarea:focus { + border-color: var(--accent); +} + +/* ─── Instance Password card ────────────────────────────────── */ +.pw-wrap { + padding: 14px 16px 12px; +} + +.pw-wrap .form-group { + margin-bottom: 0; +} + +.pw-input-wrap { + position: relative; + display: flex; + align-items: center; +} + +.pw-input-wrap input { + padding-right: 36px; +} + +.pw-save-btn { + position: absolute; + right: 6px; + display: flex; + align-items: center; + justify-content: center; + width: 26px; + height: 26px; + padding: 0; + background: none; + border: none; + border-radius: 4px; + color: var(--accent); + cursor: pointer; + transition: + color 0.15s, + background 0.15s; +} + +.pw-save-btn:hover { + background: var(--bg-hover); +} + +.ssh-hint { + margin-top: 7px; + font-size: 11px; + color: var(--text-muted); + line-height: 1.5; +} + +.ssh-cmd-row { + display: flex; + align-items: center; + gap: 8px; + margin-top: 10px; + background: var(--bg-input); + border: 1px solid var(--border); + border-radius: 6px; + padding: 7px 10px; +} +.ssh-cmd-code { + flex: 1; + font-family: "Cascadia Code", "Fira Code", monospace; + font-size: 11.5px; + color: var(--text); + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + user-select: all; +} +.ssh-copy-btn { + flex-shrink: 0; +} + +/* ─── Server info rows ─────────────────────────────────────── */ +.server-info-row { + display: flex; + justify-content: space-between; + align-items: center; + padding: 5px 0; + border-top: 1px solid var(--border); + font-size: 12px; +} +.server-info-row--copy { + cursor: pointer; +} + +.server-info-row--copy:hover .server-info-copyval { + color: var(--accent); + text-decoration: underline; + text-underline-offset: 2px; +} + +.server-info-copyval { + font-family: "Cascadia Code", "Fira Code", monospace; + font-size: 11.5px; + transition: color 0.15s; +} + +.server-info-row:first-child { + margin-top: 4px; +} +.server-info-label { + color: var(--text-muted); + text-transform: capitalize; +} +.server-info-val { + color: var(--text); + font-family: "Cascadia Code", "Fira Code", monospace; + font-size: 11.5px; +} +.server-info-link { + color: var(--accent); + text-decoration: none; +} +.server-info-link:hover { + text-decoration: underline; +} + +/* ─── Backups ───────────────────────────────────────────────── */ +.backups-list { + display: flex; + flex-direction: column; + gap: 0; + max-height: 320px; + overflow-y: auto; + scrollbar-width: thin; + scrollbar-color: var(--border) transparent; +} + +.backups-list::-webkit-scrollbar { + width: 4px; +} +.backups-list::-webkit-scrollbar-thumb { + background: var(--border); + border-radius: 4px; +} + +.backup-row { + display: flex; + align-items: center; + gap: 10px; + padding: 9px 0; + border-bottom: 1px solid var(--border); +} + +.backup-row:last-child { + border-bottom: none; +} + +.backup-info { + flex: 1; + min-width: 0; +} + +.backup-name { + font-size: 13px; + font-weight: 500; + color: var(--text); + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.backup-date { + font-size: 11px; + color: var(--text-muted); + margin-top: 2px; +} + +.backup-empty { + display: flex; + flex-direction: column; + align-items: center; + padding: 28px 0; + color: var(--text-muted); + font-size: 13px; + gap: 6px; +} + +/* ─── Buttons ───────────────────────────────────────────────── */ +.btn { + display: inline-flex; + align-items: center; + gap: 6px; + padding: 7px 14px; + border-radius: 6px; + border: none; + cursor: pointer; + font-size: 13px; + font-weight: 500; + white-space: nowrap; + text-decoration: none; + transition: + opacity 0.15s, + background 0.15s, + transform 0.1s; + flex-shrink: 0; +} + +.btn:hover { + opacity: 0.85; +} +.btn:active { + opacity: 0.7; + transform: scale(0.97); +} +.btn:disabled { + opacity: 0.4; + cursor: not-allowed; + transform: none; +} + +.btn-primary { + background: var(--accent); + color: #fff; +} +.btn-danger { + background: var(--danger); + color: #fff; +} +.btn-success { + background: var(--success); + color: #000; +} + +.btn-ghost { + background: var(--bg-hover); + color: var(--text); + border: 1px solid var(--border); +} + +.btn-sm { + padding: 5px 10px; + font-size: 12px; +} + +.btn-block { + width: 100%; + justify-content: center; + padding: 10px 14px; + font-size: 14px; +} + +/* ─── Forms ─────────────────────────────────────────────────── */ +.form-row { + display: flex; + gap: 10px; + flex-wrap: wrap; +} + +.form-group { + display: flex; + flex-direction: column; + gap: 5px; + flex: 1; + min-width: 130px; +} + +.form-group label { + font-size: 12px; + color: var(--text-muted); + font-weight: 500; +} + +.form-group input, +.form-group select, +.form-group textarea { + background: var(--bg-input); + border: 1px solid var(--border); + border-radius: 6px; + color: var(--text); + padding: 9px 11px; + font-size: 13px; + outline: none; + transition: border-color 0.15s; + width: 100%; +} + +.form-group input:focus, +.form-group select:focus, +.form-group textarea:focus { + border-color: var(--accent); +} + +.form-group select option { + background: var(--bg-card); +} + +.form-group textarea { + resize: vertical; + font-family: monospace; + font-size: 12px; + box-sizing: border-box; + line-height: 1.5; +} + +/* ─── Progress bar ──────────────────────────────────────────── */ +.prog-wrap { + background: var(--bg-input); + border-radius: 4px; + height: 4px; + margin-top: 6px; + overflow: hidden; +} + +.prog-bar { + height: 100%; + border-radius: 4px; + transition: width 0.5s ease; + min-width: 3px; +} + +.prog-low { + background: var(--success); +} +.prog-mid { + background: var(--warning); +} +.prog-high { + background: var(--danger); +} + +/* ─── Loading rows ──────────────────────────────────────────── */ +.loading-row { + display: flex; + align-items: center; + justify-content: center; + padding: 20px; + color: var(--text-muted); + gap: 8px; + font-size: 13px; +} + +.inline-error { + font-size: 12px; + color: var(--danger); + padding: 6px 0; +} + +/* ─── Spinner ───────────────────────────────────────────────── */ +.spinner { + width: 18px; + height: 18px; + border: 2px solid var(--border); + border-top-color: var(--accent); + border-radius: 50%; + animation: spin 0.6s linear infinite; + display: inline-block; + flex-shrink: 0; +} + +.spinner-sm { + width: 13px; + height: 13px; + border: 2px solid rgba(255, 255, 255, 0.2); + border-top-color: #fff; + border-radius: 50%; + animation: spin 0.6s linear infinite; + display: inline-block; + flex-shrink: 0; +} + +@keyframes spin { + to { + transform: rotate(360deg); + } +} + +/* ─── Toast ─────────────────────────────────────────────────── */ +#toast-container { + position: fixed; + bottom: 22px; + right: 22px; + display: flex; + flex-direction: column; + gap: 8px; + z-index: 9999; +} + +.toast { + background: var(--bg-card); + border: 1px solid var(--border); + border-left: 4px solid var(--accent); + border-radius: var(--radius); + padding: 11px 16px; + min-width: 240px; + max-width: 360px; + box-shadow: 0 8px 28px rgba(0, 0, 0, 0.5); + font-size: 13px; + line-height: 1.45; + animation: toastIn 0.2s ease; + transition: + opacity 0.3s, + transform 0.3s; +} + +.toast-error { + border-left-color: var(--danger); +} +.toast-success { + border-left-color: var(--success); +} +.toast-warning { + border-left-color: var(--warning); +} + +@keyframes toastIn { + from { + transform: translateX(50px); + opacity: 0; + } + to { + transform: translateX(0); + opacity: 1; + } +} + +/* ─── Modal / Confirm ───────────────────────────────────────── */ +.modal-backdrop { + position: fixed; + inset: 0; + background: rgba(0, 0, 0, 0.65); + z-index: 1000; + display: flex; + align-items: center; + justify-content: center; + padding: 20px; + opacity: 0; + pointer-events: none; + transition: opacity 0.2s; + backdrop-filter: blur(3px); +} + +.modal-backdrop.open { + opacity: 1; + pointer-events: all; +} + +.modal { + background: var(--bg-card); + border: 1px solid var(--border-hi); + border-radius: 12px; + width: 100%; + max-width: 400px; + box-shadow: 0 24px 60px rgba(0, 0, 0, 0.6); + transform: translateY(16px); + transition: transform 0.2s; +} + +.modal-backdrop.open .modal { + transform: translateY(0); +} + +.modal-header { + padding: 18px 22px 14px; + border-bottom: 1px solid var(--border); +} + +.modal-title { + font-size: 15px; + font-weight: 600; +} + +.modal-body { + padding: 18px 22px; +} + +.modal-body p { + font-size: 14px; + line-height: 1.65; + color: var(--text-muted); +} + +.modal-footer { + padding: 14px 22px; + border-top: 1px solid var(--border); + display: flex; + justify-content: flex-end; + gap: 10px; +} + +/* ─── Utility ───────────────────────────────────────────────── */ +.text-muted { + color: var(--text-muted); +} + +/* ─── Responsive ────────────────────────────────────────────── */ +@media (max-width: 700px) { + .content-grid { + grid-template-columns: 1fr; + } + + .app-shell { + border-radius: 0; + width: 100vw; + height: 100vh; + } + + .app-user .user-pill { + display: none; + } +} + +@media (max-height: 680px) { + .app-shell { + border-radius: 0; + height: 100vh; + } +} + +/* ─── Talk to Us panel ──────────────────────────────────────────── */ +.talk-wrap { + border-top: 2px solid var(--accent); + flex-shrink: 0; + background: var(--bg-inner); +} +.talk-toggle { + width: 100%; + display: flex; + align-items: center; + gap: 8px; + padding: 9px 22px; + background: linear-gradient( + 90deg, + rgba(79, 142, 247, 0.08) 0%, + transparent 70% + ); + border: none; + color: var(--accent); + font-size: 12px; + font-weight: 600; + cursor: pointer; + transition: + background 0.15s, + color 0.15s; + letter-spacing: 0.3px; +} +.talk-toggle:hover { + background: linear-gradient( + 90deg, + rgba(79, 142, 247, 0.16) 0%, + transparent 70% + ); + color: var(--text); +} +.talk-toggle.active { + color: var(--text); + background: linear-gradient( + 90deg, + rgba(79, 142, 247, 0.14) 0%, + transparent 70% + ); +} +.talk-panel { + display: grid; + grid-template-columns: 1fr 1fr; + max-height: 0; + overflow: hidden; + transition: max-height 0.28s ease; +} +.talk-panel.open { + max-height: 270px; +} +.talk-col { + padding: 12px 20px 14px; + display: flex; + flex-direction: column; + gap: 8px; +} +.talk-chat-col { + border-right: 1px solid var(--border); +} +.talk-col-title { + font-size: 11px; + font-weight: 600; + color: var(--text-muted); + text-transform: uppercase; + letter-spacing: 0.6px; +} +.chat-msgs { + flex: 1; + overflow-y: auto; + max-height: 150px; + display: flex; + flex-direction: column; + gap: 5px; + padding-right: 2px; +} +.chat-msg { + max-width: 82%; + padding: 5px 10px; + border-radius: 10px; + font-size: 12px; + line-height: 1.45; + word-break: break-word; +} +.chat-msg-user { + align-self: flex-end; + background: var(--accent); + color: #fff; + border-bottom-right-radius: 3px; +} +.chat-msg-bot { + align-self: flex-start; + background: var(--bg-hover); + color: var(--text); + border-bottom-left-radius: 3px; +} +.chat-input-row { + display: flex; + gap: 6px; +} +.chat-input-row input { + flex: 1; + background: var(--bg-input); + border: 1px solid var(--border); + border-radius: 6px; + color: var(--text); + padding: 5px 10px; + font-size: 12px; + outline: none; + min-width: 0; +} +.chat-input-row input:focus { + border-color: var(--accent); +} +.refer-desc { + font-size: 12px; + color: var(--text-muted); + line-height: 1.55; + flex: 1; +} +.refer-desc strong { + color: var(--text); +} +.refer-form { + display: flex; + flex-direction: column; + gap: 6px; +} + +.refer-input-row { + display: flex; + gap: 6px; +} +.refer-input-row input { + flex: 1; + background: var(--bg-input); + border: 1px solid var(--border); + border-radius: 6px; + color: var(--text); + padding: 5px 10px; + font-size: 12px; + outline: none; + min-width: 0; +} +.refer-input-row input:focus { + border-color: var(--accent); +} +.refer-result { + font-size: 12px; + color: var(--success); + margin: 0; + line-height: 1.4; +} + +/* ─── Auth-layout: auth card ABOVE demo shell, stacked ──────── */ +body.auth-layout { + flex-direction: column; + gap: 14px; + align-items: center; + justify-content: center; +} + +/* Auth card matches the app-shell width */ +body.auth-layout .auth-card { + width: min(1060px, calc(100vw - 16px)); +} + +/* Logo visibility: top logo for narrow, pane logo for dual */ +.auth-logo-pane { + display: none; +} +body.auth-layout .auth-logo-top { + display: none; +} +body.auth-layout .auth-logo-pane { + display: flex; + align-items: center; + gap: 14px; + padding: 22px 24px 16px; + text-align: left; + border-bottom: 1px solid var(--border); +} +body.auth-layout .auth-logo-pane .auth-logo-icon { + flex-shrink: 0; +} +body.auth-layout .auth-logo-pane .auth-title { + font-size: 18px; + margin-bottom: 2px; +} +body.auth-layout .auth-logo-pane .auth-sub { + font-size: 12px; + margin-top: 0; +} + +/* Hide the tab toggle when both panes are visible */ +body.auth-layout .auth-switch { + display: none; +} + +/* Dual-pane form wrapper */ +.auth-forms-wrap { + display: block; +} +body.auth-layout .auth-forms-wrap { + display: flex; + flex-direction: row; + align-items: stretch; /* equal height for both panes */ +} +body.auth-layout .auth-pane { + flex: 1 1 0; /* equal widths */ + min-width: 0; + display: flex; + flex-direction: column; +} +/* Right pane: badge at top, form pushed to bottom */ +body.auth-layout .auth-pane:last-child { + justify-content: flex-start; +} +body.auth-layout #signup-form { + margin-top: auto; +} + +/* Pricing badge */ +.pricing-badge { + display: none; +} +body.auth-layout .pricing-badge { + display: flex; + flex-direction: column; + align-items: center; + text-align: center; + gap: 6px; + margin: 20px 24px 0; + padding: 14px 20px; + background: var(--accent-dim); + border: 1px solid rgba(79, 142, 247, 0.35); + border-radius: 12px; +} +.pricing-amount { + font-size: 22px; + font-weight: 800; + color: var(--text); + letter-spacing: -0.3px; + line-height: 1; +} +.pricing-amount span { + font-size: 13px; + font-weight: 500; + color: var(--text-muted); +} +.pricing-term { + font-size: 11px; + color: var(--text-muted); + letter-spacing: 0.2px; +} +.pricing-bonus { + display: flex; + align-items: center; + gap: 5px; + font-size: 12px; + color: var(--accent); + font-weight: 500; +} + +/* Coupon box */ +.coupon-box { + display: none; +} +body.auth-layout .coupon-box { + display: block; + margin: 10px 24px 0; +} +.coupon-input-wrap { + position: relative; + display: flex; + align-items: center; +} +.coupon-input { + width: 100%; + padding: 8px 36px 8px 12px; + background: var(--bg-input); + border: 1px solid var(--border); + border-radius: var(--radius); + color: var(--text); + font-size: 13px; + outline: none; + transition: border-color 0.15s; + box-sizing: border-box; +} +.coupon-input::placeholder { + color: var(--text-muted); +} +.coupon-input:focus { + border-color: var(--accent); +} +.coupon-input.coupon-input-error { + border-color: var(--danger); +} +.coupon-spinner { + position: absolute; + right: 10px; + display: flex; + align-items: center; +} +.coupon-error { + margin-top: 5px; + font-size: 11px; + color: var(--danger); +} +.coupon-success { + display: none; +} +body.auth-layout .coupon-success { + display: flex; + align-items: center; + gap: 6px; + margin: 10px 24px 0; + padding: 8px 12px; + background: rgba(63, 201, 126, 0.1); + border: 1px solid rgba(63, 201, 126, 0.3); + border-radius: var(--radius); + font-size: 12px; + font-weight: 500; + color: var(--success); +} +body.auth-layout .auth-forms-divider { + width: 1px; + align-self: stretch; + background: var(--border); + margin: 0; + flex-shrink: 0; +} +.auth-pane-title { + display: none; +} +body.auth-layout #signup-form { + display: flex !important; +} +/* Sign-in form stretches and pushes its button to the bottom + so it aligns with the Create Account button on the right */ +body.auth-layout #signin-form { + flex: 1; +} +body.auth-layout #signin-btn { + margin-top: auto; +} + +/* Demo app-shell: matches auth card width, shorter height */ +#app-shell.demo-mode { + width: min(1060px, calc(100vw - 16px)); + height: min(500px, calc(100vh - 360px)); + flex-shrink: 0; +} + +#app-shell.demo-mode .app-header, +#app-shell.demo-mode .agent-tabs-bar { + opacity: 0.55; + pointer-events: none; + user-select: none; +} +#app-shell.demo-mode .app-body { + pointer-events: none; + user-select: none; +} + +.demo-notice { + display: none; + position: absolute; + inset: 0; + z-index: 20; + align-items: center; + justify-content: center; + flex-direction: column; + gap: 10px; + background: transparent; /* no tint — content stays fully visible */ + color: var(--text); + font-size: 13px; + font-weight: 500; + text-align: center; + border-radius: 4px; +} +.demo-notice span { + display: inline-flex; + align-items: center; + gap: 9px; + background: var(--danger-dim); + border: 1px solid var(--danger); + border-radius: 24px; + padding: 11px 22px; + color: var(--danger); + font-size: 15px; + font-weight: 600; + box-shadow: 0 4px 28px rgba(224, 82, 82, 0.35); + letter-spacing: 0.1px; +} +#app-shell.demo-mode .demo-notice { + display: flex; +} + +/* ─── Login animations ───────────────────────────────────────── */ +@keyframes authLeave { + to { + opacity: 0; + transform: translateY(-28px); + filter: blur(8px); + } +} +.auth-card.auth-leaving { + animation: authLeave 0.3s ease-in forwards; + pointer-events: none; +} + +@keyframes shellEnter { + from { + opacity: 0; + transform: translateY(22px); + filter: blur(10px); + } + to { + opacity: 1; + transform: none; + filter: none; + } +} +#app-shell.app-entering { + animation: shellEnter 0.55s cubic-bezier(0.22, 1, 0.36, 1) forwards; +} + +/* On screens too narrow for the dual layout, fall back to single column */ +@media (max-width: 960px) { + #app-shell.demo-mode { + display: none !important; + } + body.auth-layout .auth-card { + width: min(440px, calc(100vw - 16px)); + } + body.auth-layout .auth-logo-top { + display: block; + } + body.auth-layout .auth-logo-pane { + display: none; + } + body.auth-layout .auth-switch { + display: flex; + } + body.auth-layout .auth-forms-wrap { + display: block; + } + body.auth-layout #signup-form { + display: none !important; + } + body.auth-layout .auth-pane-title { + display: none; + } +} + +/* ─── Contract card ──────────────────────────────────────── */ +.contract-row { + display: flex; + align-items: center; + gap: 12px; + padding: 14px 16px; +} + +.contract-dot { + width: 9px; + height: 9px; + border-radius: 50%; + flex-shrink: 0; +} + +.contract-dot--green { + background: var(--success); + box-shadow: 0 0 0 3px rgba(63, 201, 126, 0.2); +} + +.contract-dot--yellow { + background: var(--warning); + box-shadow: 0 0 0 3px rgba(240, 160, 75, 0.2); +} + +.contract-dot--red { + background: var(--danger); + box-shadow: 0 0 0 3px rgba(224, 82, 82, 0.2); +} + +.contract-info { + display: flex; + flex-direction: column; + gap: 3px; +} + +.contract-type { + font-size: 13px; + color: var(--text); + font-weight: 500; +} + +.contract-expires { + font-size: 11px; + color: var(--text-muted); +} + +.contract-extend-link { + display: inline-block; + margin-top: 5px; + font-size: 12px; + font-weight: 600; + color: var(--danger); + text-decoration: none; +} + +.contract-extend-link:hover { + text-decoration: underline; +} + +.contract-cancel-link { + margin-left: auto; + flex-shrink: 0; + font-size: 12px; + font-weight: 500; + color: var(--text-muted); + text-decoration: none; + padding: 3px 10px; + border: 1px solid var(--border); + border-radius: var(--radius); + transition: + color 0.15s, + border-color 0.15s; +} + +.contract-cancel-link:hover { + color: var(--danger); + border-color: var(--danger); +} + +.contract-cancel-link--danger { + color: var(--danger); + border-color: var(--danger); +} + +.contract-cancel-link--danger:hover { + color: var(--danger); + border-color: var(--danger); +} + +/* ─── Wizard (Integrations setup) ─────────────────────────────── */ +#wizard-card { + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + z-index: 5; + display: flex; + flex-direction: column; + overflow: hidden; + margin: 0; +} + +#wizard-body { + flex: 1; + overflow-y: auto; + min-height: 0; +} + +.wizard-step-label { + font-size: 11px; + font-weight: 500; + color: var(--text-muted); + letter-spacing: 0.04em; + white-space: nowrap; +} + +.wizard-search { + display: block; + width: 100%; + box-sizing: border-box; + background: var(--bg-input); + border: 1px solid var(--border); + border-radius: var(--radius); + color: var(--text); + font-size: 13px; + padding: 7px 10px; + outline: none; + margin-bottom: 8px; +} + +.wizard-search:focus { + border-color: var(--accent); +} + +.wizard-search::placeholder { + color: var(--text-muted); +} + +.wizard-list { + display: flex; + flex-direction: column; + max-height: 240px; + overflow-y: auto; + gap: 1px; +} + +.wizard-list::-webkit-scrollbar { + width: 4px; +} + +.wizard-list::-webkit-scrollbar-thumb { + background: var(--border); + border-radius: 4px; +} + +.wizard-list-item { + display: flex; + align-items: flex-start; + gap: 10px; + padding: 7px 10px; + border-radius: var(--radius); + cursor: pointer; + transition: background 0.1s; + user-select: none; +} + +.wizard-list-item:hover { + background: var(--bg-hover); +} + +.wizard-list-item input[type="checkbox"] { + width: 14px; + height: 14px; + accent-color: var(--accent); + cursor: pointer; + flex-shrink: 0; + margin: 2px 0 0; +} + +.wizard-list-item-content { + display: flex; + flex-direction: column; + gap: 2px; +} + +.wizard-list-item-name { + font-size: 13px; + color: var(--text); + line-height: 1.3; +} + +.wizard-list-item-desc { + font-size: 11px; + color: var(--text-muted); + line-height: 1.4; +} + +.wizard-footer { + display: flex; + align-items: center; + justify-content: flex-end; + gap: 8px; + padding: 10px 16px; + border-top: 1px solid var(--border); + margin-top: 4px; +} + +.wizard-fields { + display: flex; + flex-direction: column; + gap: 12px; +} + +.wizard-signup-hint { + font-size: 11px; + color: var(--text-muted); + margin: 0; +} + +.wizard-signup-hint a { + color: var(--text-muted); + text-decoration: none; +} + +.wizard-signup-hint a:hover { + color: var(--text); +} + +.wizard-review-area { + display: flex; + flex-direction: column; + gap: 10px; +} + +.wizard-review-intro { + font-size: 12px; + color: var(--text-muted); + margin: 0; +} + +.wizard-review-textarea { + display: block; + width: 100%; + min-height: 180px; + box-sizing: border-box; + background: var(--bg-input); + border: 1px solid var(--border); + border-radius: var(--radius); + color: var(--text); + font-size: 12px; + font-family: + ui-monospace, "Cascadia Code", "Source Code Pro", Menlo, monospace; + padding: 10px 12px; + resize: vertical; + outline: none; + line-height: 1.5; +} + +.wizard-review-textarea:focus { + border-color: var(--accent); +}