Profile
Back to NewsBack
GitHub Trending 17 min
Reader Mode
agent-sh/agentsys: AI writes code. This automates everything else · 24 plugins · 49 agents · 44 skills · for Claude Code, OpenCode, Codex, Cursor, Kiro.

agent-sh/agentsys: AI writes code. This automates everything else · 24 plugins · 49 agents · 44 skills · for Claude Code, OpenCode, Codex, Cursor, Kiro.

4 hours ago

AgentSys

AgentSys

A modular runtime and orchestration system for AI agents.

npm version npm downloads CI GitHub stars License: MIT Website Mentioned in Awesome Claude Code

11 plugins · 5 agents · 14 skills (across all repos) · 13k lines of lib code · tests on Linux and Windows · 5 platforms
Plugins distributed as standalone repos under agent-sh org - agentsys is the marketplace & installer

Running open models for your company? Tiyuvta helps with model choice, deployment and optimization, and fine-tuning on your hardware or cloud account.

Sponsor

Built and maintained with a lot of agent tokens and hours. If it saves you time, sponsoring helps keep it going.

Commands · Installation · Website · Discussions

Built for Claude Code · Codex CLI · OpenCode · Cursor · Kiro

New skills, agents, and integrations ship constantly. Follow for real-time updates:

Follow on X


AI models can write code. That's not the hard part anymore. The hard part is everything around it - branch management, code review, artifact cleanup, CI, PR comments. AgentSys is the runtime that orchestrates agents to handle all of it - structured pipelines, gated phases, specialized agents, and persistent state that survives session boundaries.


Building custom skills, agents, hooks, or MCP tools? agnix is the CLI + LSP linter that catches config errors before they fail silently - real-time IDE validation, auto suggestions, auto-fix, and 757 rules for Claude Code, Codex, OpenCode, Cursor, Kiro, Copilot, Gemini CLI, Cline, Windsurf, Roo Code, Amp, and more.

What's New in 6.1.1

  • OpenCode, Codex, Cursor and Kiro installs get the plugins that ship no .claude-plugin/plugin.json at their pin. The installer fetched such a plugin and installed nothing from it. It now makes one from the marketplace entry, as Claude Code does.
  • agnix installs its /agnix command, agent and skill on every platform. Claude Code installed only its skill before, and the other platforms nothing. The marketplace entry is now a git-subdir source for the plugin's plugin/ folder.
  • The installer fetches a git-subdir source as that folder, pinned by sha or commit.
  • Contributor docs: every change reaches main through a PR, and a release commit merges through a PR with only the tag pushed.

What This Is

An agent orchestration system - 11 plugins, 5 agents, and 14 skills that compose into structured pipelines for software development. Each plugin lives in its own standalone repo under the agent-sh org. agentsys is the marketplace and installer that ties them together.

Each agent has a single responsibility, a specific model assignment, and defined inputs/outputs. Pipelines enforce phase gates so agents can't skip steps. State persists across sessions so work survives interruptions.

The system runs on Claude Code, OpenCode, Codex CLI, Cursor, and Kiro. Install via the marketplace or the npm installer, and the plugins are fetched automatically from their repos.


The Approach

Code does code work. AI does AI work.

  • Detection: regex, AST analysis, static analysis - fast, deterministic, no tokens wasted
  • Judgment: LLM calls for synthesis, planning, review - where reasoning matters
  • Result: certainty-graded findings throughout
Certainty levels exist because not all findings are equal:

| Level | Meaning | Action | |-------|---------|--------| | HIGH | Definitely a problem | Safe to auto-fix | | MEDIUM | Probably a problem | Needs context | | LOW | Might be a problem | Needs human judgment |

This came from testing on 1,000+ repositories.


Commands

| Command | What it does | |---------|--------------| | /foreman | Work the GitHub issue queue with native subagents, from pick to land | | /banthis | Durable negative memory: persist banned agent behaviors | | /agnix | Lint agent configurations (757 rules) | | /deslop | Clean AI slop patterns | | /learn | Research topics, create learning guides | | /skillers | Workflow pattern learning and automation | | /skill-curator | Create and improve reliable SKILL.md files | | /system-prompt-curator | Create and improve autonomous agent system prompts |

Each command works standalone. Together, they compose into end-to-end pipelines.


Skills

14 skills included across the plugins:

| Category | Skills | |----------|--------| | Workflow | foreman | | Message Queues | glide-mq-migrate-bee, glide-mq-migrate-bullmq, glide-mq | | Enhancement | skill-curator, system-prompt-curator | | Cleanup | deslop | | AI Collaboration | learn, recommend, skillers-compact | | Memory | banthis | | Linting | agnix | | Languages | ada-spark, mojo |

External skill plugins (standalone repos, installed separately):

| Category | Skills | Plugin | |----------|--------|--------| | Message Queues | glide-mq, glide-mq-migrate-bullmq, glide-mq-migrate-bee | agent-sh/glidemq |

Skills are the reusable implementation units. Agents invoke skills; commands orchestrate agents. When you install a plugin, its skills become available to all agents in that session.


Quick Navigation

| Section | What's there | |---------|--------------| | The Approach | Why it's built this way | | Commands | Overview of every command | | Skills | 14 skills across plugins | | Skill-Only Plugins | glide-mq and other non-command plugins | | Command Details | Deep dive into each command | | How Commands Work Together | Standalone vs integrated | | Design Philosophy | The thinking behind the architecture | | Installation | Get started | | Research & Testing | What went into building this | | Documentation | Links to detailed docs |


Skill-Only Plugins

Plugins without a / command. The marketplace ones install with agentsys; glide-mq installs from its own repo.

| Plugin | What it does | |--------|--------------| | mojo | mojo skill: current Mojo syntax, ownership, SIMD and GPU kernels, Python interop | | ada-spark | ada-spark skill: Ada 2022 and SPARK contracts, proof, Alire and GNAT tooling | | zig-lsp | Zig language server for Claude Code via ZLS (no skill). Needs zls on PATH |

glide-mq

Build message queues, background jobs, and workflow orchestration with glide-mq - high-performance Node.js queue on Valkey/Redis. Not in the agentsys marketplace: install its skills from agent-sh/glidemq.

| Skill | What it does | |-------|--------------| | glide-mq | Greenfield queue development - queues, workers, ordering, rate limiting, flows, broadcast, step jobs | | glide-mq-migrate-bullmq | Migrate from BullMQ to glide-mq - API mapping, breaking changes, feature comparison | | glide-mq-migrate-bee | Migrate from Bee-Queue to glide-mq - API mapping, pattern conversion |

Key features: per-key ordering, group concurrency, runtime group rate limiting (job.rateLimitGroup()), token bucket, DAG workflows, broadcast pub/sub, step jobs, deduplication, serverless producers.

Skill plugin → | glide-mq docs → | npm →


Command Details

/foreman

Purpose: Work a repo's GitHub issues as a task queue. One main session picks the next ready issue, claims it, and hands it to a worktree subagent. Revuto reviews the PR, and foreman lands it at the exact head it checked. It replaces next-task and tower-crane.

There is no daemon and no state store. Labels, assignees and comments on GitHub are the state, and every command reads GitHub live, so several sessions can share one queue.

The loop:

  1. Capacity, pick, claim - checks load, CPU pressure and memory, takes the next ready issue (urgent first, then oldest, open dependencies skipped) and claims it
  2. Brief and spawn - one worktree subagent per issue from its brief, in parallel up to agents_max
  3. Review - reads Revuto's findings; every real finding gets fixed, a wrong one gets a reply. When Revuto is capped or down, the session reviews the diff itself and records it
  4. Land - every check green at the head, no unaddressed finding, the repo's own land command, then a squash merge pinned to that head and the issue closed
/foreman                    # Run the queue until nothing is ready
/foreman status             # Counts by label, claims and capacity
/foreman stats              # Revuto caps, self-reviews, manual review requests
/foreman 123                # Work issue #123 only, from claim to land

Config: optional .foreman.json in the repo root: label names, agents_max, load and memory holds, the check and land commands, required checks, Revuto round cap, forbidden words for posted comments.

Requires: Node 22+, gh (logged in), git and waitfor. run-long needs systemd user units (Linux).

Skill: foreman (no agents: workers are native worktree subagents)

Full workflow documentation → · Repo →


/banthis

Purpose: Durable negative memory for repeated agent mistakes. Turn a user's "stop doing this" correction into a persistent rule in CLAUDE.md or AGENTS.md.

banthis is a tiny standalone CLI plus skill. It maintains a managed banned-behaviors section, supports project or global targets, and includes an init meta-rule so agents learn when to invoke it automatically.

What it does:

| Command | Use | |---------|-----| | banthis add "" "<rule>"</code> | Add or update a banned behavior | | <code>banthis list</code> | List current bans | | <code>banthis show</code> | Print the managed section | | <code>banthis remove "<title>"</code> | Remove a ban | | <code>banthis init</code> | Install the meta-rule that teaches agents to call <code>banthis</code> |</p> <p><strong>Usage:</strong></p> <pre><code class="bash">/banthis "stop ending with vague optional follow-up offers" banthis add "No vague endings" "Do not end with vague optional follow-up offers." banthis init --file AGENTS.md</code></pre> <hr> <h3>/agnix</h3> <p><strong>Purpose:</strong> Lint agent configurations before they break your workflow. The first dedicated linter for AI agent configs.</p> <p><strong><a href="https://github.com/agent-sh/agnix" target="_blank" rel="noopener">agnix</a></strong> is a standalone open-source project that provides the validation engine. This plugin integrates it into your workflow.</p> <p><strong>The problem it solves:</strong></p> <p>Agent configurations are code. They affect behavior, security, and reliability. But unlike application code, they have no linting. You find out your SKILL.md is malformed when the agent fails. You discover your hooks have security issues when they're exploited. You realize your CLAUDE.md has conflicting rules when the AI behaves unexpectedly.</p> <p>agnix catches these issues before they cause problems.</p> <p><strong>What it validates:</strong></p> <p>| Category | What It Checks | |----------|----------------| | <strong>Structure</strong> | Required fields, valid YAML/JSON, proper frontmatter | | <strong>Security</strong> | Prompt injection vectors, overpermissive tools, exposed secrets | | <strong>Consistency</strong> | Conflicting rules, duplicate definitions, broken references | | <strong>Best Practices</strong> | Tool restrictions, model selection, trigger phrase quality | | <strong>Cross-Platform</strong> | Compatibility across Claude Code, Codex, OpenCode, Cursor, Kiro, Copilot, Gemini CLI, Cline, Windsurf, Roo Code, Amp, and more |</p> <p><strong>757 validation rules</strong> derived from: <ul><li>Official tool specifications (Claude Code, Codex CLI, OpenCode, Cursor, Kiro, GitHub Copilot, Gemini CLI, Cline, Windsurf, Roo Code, Amp, and more)</li> <li>Research papers on agent reliability and prompt injection</li> <li>Real-world testing across 500+ repositories</li> <li>Community-reported issues and edge cases</li></ul> <strong>Supported files:</strong></p> <p>| File Type | Examples | |-----------|----------| | Skills | <code>SKILL.md</code>, <code>*/SKILL.md</code> | | Memory | <code>CLAUDE.md</code>, <code>AGENTS.md</code>, <code>.github/CLAUDE.md</code> | | Hooks | <code>.claude/settings.json</code>, hooks configuration | | MCP | <code>*.mcp.json</code>, MCP server configs | | Cursor | <code>.cursor/rules/*.mdc</code>, <code>.cursorrules</code> | | Copilot | <code>.github/copilot-instructions.md</code> | | Kiro | <code>.kiro/steering/<em>*/</em>.md</code>, <code>.kiro/agents/<em>.json</code>, <code>.kiro/hooks/</em>.kiro.hook</code>, <code>POWER.md</code> | | Windsurf | <code>.windsurf/rules/<strong>/<em>.md</code>, <code>.windsurf/workflows/</strong>/</em>.md</code>, <code>.windsurfrules</code> | | Roo Code | <code>.roo/rules/<em>.md</code>, <code>.roo/rules-{mode}/</em>.md</code>, <code>.roomodes</code>, <code>.rooignore</code>, <code>.roorules</code> | | Gemini CLI | <code>GEMINI.md</code>, <code>.gemini/settings.json</code>, <code>gemini-extension.json</code> | | OpenCode | <code>opencode.json</code> | | Amp | <code>.agents/checks/<em>*/</em>.md</code>, <code>.amp/settings.json</code> |</p> <p><strong>CI/CD Integration:</strong></p> <p>agnix outputs SARIF format for GitHub Code Scanning. Add it to your workflow:</p> <pre><code class="yaml">- name: Lint agent configs run: agnix --format sarif > results.sarif <ul><li>uses: github/codeql-action/upload-sarif@v3</li></ul> with: sarif_file: results.sarif</code></pre> <p><strong>Usage:</strong></p> <pre><code class="bash">/agnix # Validate current project /agnix --fix # Auto-fix fixable issues /agnix --strict # Treat warnings as errors /agnix --target claude-code # Only Claude Code rules /agnix --format sarif # Output for GitHub Code Scanning</code></pre> <p><strong>Agent:</strong> agnix-agent (sonnet model)</p> <p><strong>External tool:</strong> Requires <a href="https://github.com/agent-sh/agnix" target="_blank" rel="noopener">agnix CLI</a></p> <pre><code class="bash">npm install -g agnix # Install via npm <h1>or</h1> cargo install agnix-cli # Install via Cargo <h1>or</h1> brew tap agent-sh/agnix && brew install agnix # Install via Homebrew</code></pre> <p><strong>Why use agnix:</strong> <ul><li>Catch config errors before they cause agent failures</li> <li>Enforce security best practices across your team</li> <li>Maintain consistency as your agent configs grow</li> <li>Integrate validation into CI/CD pipelines</li> <li>Support multiple AI tools from one linter</li></ul> <hr></p> <h3>/deslop</h3> <p><strong>Purpose:</strong> Finds AI slop - debug statements, placeholder text, verbose comments, TODOs - and removes it.</p> <p><strong>How detection works:</strong></p> <p>Three phases run in sequence:</p> <ol><li><strong>Phase 1: Regex Patterns</strong> (HIGH certainty)</li></ol> - <code>console.log</code>, <code>print()</code>, <code>dbg!()</code>, <code>println!()</code> - <code>// TODO</code>, <code>// FIXME</code>, <code>// HACK</code> - Empty catch blocks, disabled linters - Hardcoded secrets (API keys, tokens) <ol><li><strong>Phase 2: Multi-Pass Analyzers</strong> (MEDIUM certainty)</li></ol> - Doc-to-code ratio (excessive comments) - Verbosity ratio (AI preambles) - Over-engineering patterns - Buzzword inflation - Dead code detection - Stub functions <ol><li><strong>Phase 3: CLI Tools</strong> (LOW certainty, optional)</li></ol> - jscpd, madge, escomplex (JS/TS) - pylint, radon (Python) - golangci-lint (Go) - clippy (Rust) <p><strong>Languages supported:</strong> JavaScript/TypeScript, Python, Rust, Go, Java</p> <p><strong>Usage:</strong></p> <pre><code class="bash">/deslop # Report only (safe) /deslop apply # Fix HIGH certainty issues /deslop apply src/ # Fix issues in src/ only</code></pre> <p><strong>Thoroughness levels:</strong></p> <ul><li><code>quick</code> - Phase 1 only (fastest)</li> <li><code>normal</code> - Phase 1 + Phase 2 (default)</li> <li><code>deep</code> - All phases if tools available</li></ul> <a href="./docs/reference/SLOP-PATTERNS.md" target="_blank" rel="noopener">Pattern reference →</a> <hr> <h3>/learn</h3> <p><strong>Purpose:</strong> Research any topic online and create a comprehensive learning guide with RAG-optimized indexes.</p> <p><strong>What it does:</strong></p> <ol><li><strong>Progressive Discovery</strong> - Uses funnel approach (broad → specific → deep) to find quality sources</li> <li><strong>Quality Scoring</strong> - Scores sources by authority, recency, depth, examples, uniqueness</li> <li><strong>Just-In-Time Extraction</strong> - Fetches only high-scoring sources to save tokens</li> <li><strong>Synthesis</strong> - Creates structured learning guide with examples and best practices</li> <li><strong>RAG Index</strong> - Updates the <code>agent-knowledge/AGENTS.md</code> master index for future lookups</li></ol> <strong>Depth levels:</strong> <p>| Depth | Sources | Use Case | |-------|---------|----------| | brief | 10 | Quick overview | | medium | 20 | Default, balanced | | deep | 40 | Comprehensive |</p> <p><strong>Output structure:</strong></p> <pre><code class="">agent-knowledge/ AGENTS.md # Master index (updated each run) recursion.md # Topic-specific guide resources/ recursion-sources.json # Source metadata with quality scores</code></pre> <p><strong>Usage:</strong></p> <pre><code class="bash">/learn recursion # Default (20 sources) /learn react hooks --depth=deep # Comprehensive (40 sources) /learn kubernetes --depth=brief # Quick overview (10 sources)</code></pre> <p>A <code>CLAUDE.md</code> mirror of the index is written only with <code>--legacy-claude-index</code>.</p> <p><strong>Agent:</strong> learn-agent (sonnet model)</p> <hr> <h3>/skillers</h3> <blockquote>Learn from your workflow patterns and suggest automations</blockquote> <pre><code class="bash">/skillers show # Display current config and knowledge stats /skillers compact # Analyze recent transcripts, extract patterns /skillers compact --days=14 # Analyze older transcripts /skillers recommend # Get automation suggestions from accumulated knowledge</code></pre> <p>Reads your Claude Code, Codex and OpenCode transcripts, identifies recurring patterns (pain points, repeated workflows, wishes), clusters them into weighted themes, and suggests skills, hooks, or agents to automate them.</p> <p>No per-turn overhead - it reads transcripts your tools already save.</p> <p><strong>Agents:</strong> skillers-compactor (sonnet), skillers-recommender (opus)</p> <p><strong>Skills:</strong> skillers-compact, recommend</p> <hr> <h3>/skill-curator</h3> <blockquote>Create and improve reliable <code>SKILL.md</code> files</blockquote> <pre><code class="bash">/skill-curator "create a skill for reviewing background jobs" /skill-curator --improve path/to/SKILL.md --category review</code></pre> <p>The skill curator focuses on trigger quality, clear scope, router patterns, concrete <code>Skip unless:</code> gates, token budgets, and agnix-ready structure across Claude Code, Codex, OpenCode, Cursor, Kiro, and similar tools.</p> <p><strong>Skill:</strong> skill-curator</p> <hr> <h3>/system-prompt-curator</h3> <blockquote>Create and improve autonomous coding-agent system prompts</blockquote> <pre><code class="bash">/system-prompt-curator "GitHub issue resolver" /system-prompt-curator --improve path/to/prompt.md --for-orchestrator</code></pre> <p>The system prompt curator rewrites prompts around task-matched identity, phased workflow, explicit tools, evidence-based completion criteria, and realistic error recovery examples. It separates prompt guidance from harness-level checks that belong in code.</p> <p><strong>Skill:</strong> system-prompt-curator</p> <hr> <h2>How Commands Work Together</h2> <p><strong>Standalone use:</strong></p> <pre><code class="bash">/deslop apply # Just clean up your code /agnix # Just lint your agent configs /foreman # Work the issue queue, one worktree agent per issue /learn react hooks # Just research a topic</code></pre> <p><strong>Together:</strong></p> <p><a href="#foreman" target="_blank" rel="noopener"><code>/foreman</code></a> picks an issue and hands it to a worktree worker. The worker can run <a href="#deslop" target="_blank" rel="noopener"><code>/deslop</code></a> on its diff and <a href="#agnix" target="_blank" rel="noopener"><code>/agnix</code></a> on any agent config it touched before it reports back. foreman then reads Revuto's findings and lands the PR only when every check is green at the head it checked.</p> <hr> <h2>Design Philosophy</h2> <p><details> <summary><strong>Architecture decisions and trade-offs</strong> (click to expand)</summary></p> <h3>The Actual Problem</h3> <p>Frontier models write good code. That's solved. What's not solved:</p> <ul><li><strong>Context management</strong> - Models forget what they're doing mid-session</li> <li><strong>Compaction amnesia</strong> - Long sessions get summarized, losing critical state</li> <li><strong>Task drift</strong> - Without structure, agents wander from the actual goal</li> <li><strong>Skipped steps</strong> - Agents skip reviews, tests, or cleanup when not enforced</li> <li><strong>Token waste</strong> - Using LLM calls for work that static analysis can do faster</li> <li><strong>Babysitting</strong> - Manually orchestrating each phase of development</li> <li><strong>Repetitive requests</strong> - Asking for the same workflow every single session</li></ul> <h3>How This Addresses It</h3> <p><strong>1. One agent, one job, done extremely well</strong></p> <p>Same principle as good code: single responsibility. The deslop-agent cleans. The learn-agent researches. The agnix-agent lints agent configs. No agent tries to do everything. Specialized agents, each with narrow scope and clear success criteria.</p> <p><strong>2. Pipeline with gates, not a monolith</strong></p> <p>Same principle as DevOps. Each step must pass before the next begins. Can't push before review. Can't merge before CI passes. Hooks enforce this - agents literally cannot skip phases.</p> <p><strong>3. Tools do tool work, agents do agent work</strong></p> <p>If static analysis, regex, or a shell command can do it, don't ask an LLM. Pattern detection uses pre-indexed regex. File discovery uses glob. Platform detection uses file existence checks. The LLM only handles what requires judgment.</p> <p><strong>4. Agents don't need to know how tools work</strong></p> <p>The slop detector returns findings with certainty levels. The agent doesn't need to understand the three-phase pipeline, the regex patterns, or the analyzer heuristics. Good tool design means the consumer doesn't need implementation details.</p> <p><strong>5. Build tools where tools don't exist</strong></p> <p>Many tasks lack existing tools. Multi-pass analyzers for slop detection. A rule engine for agent configs. The result: agents receive structured data, not raw problems to figure out.</p> <p><strong>6. Research-backed prompt engineering</strong></p> <p>Documented techniques that measurably improve results: <ul><li><strong>Progressive disclosure</strong> - Agents see only what's needed for the current step</li> <li><strong>Structured output</strong> - JSON between delimiters, XML tags for sections</li> <li><strong>Explicit constraints</strong> - What agents MUST NOT do matters as much as what they do</li> <li><strong>Few-shot examples</strong> - Where patterns aren't obvious</li> <li><strong>Tool calling over generation</strong> - Let the model use tools rather than generate tool-like output</li></ul> <strong>7. Validate plan and results, not every step</strong></p> <p>Approve the plan. See the results. The middle is automated. /foreman picks, briefs, reads the review and lands without a prompt at each step.</p> <p><strong>8. Right model for the task</strong></p> <p>Match model capability to task complexity: <ul><li><strong>opus</strong> - Planning, implementation, review orchestration</li> <li><strong>sonnet</strong> - Pattern matching, validation, discovery</li> <li><strong>haiku</strong> - Git operations, file moves, CI polling</li></ul> Quality compounds. Poor exploration → poor plan → poor implementation → review cycles. Early phases deserve the best model.</p> <p><strong>9. Persistent state survives sessions</strong></p> <p>State files track the workflow phase. Sessions can die and resume. Multiple sessions run in parallel in separate worktrees.</p> <p><strong>10. Delegate everything automatable</strong></p> <p>Agents don't just write code. They: <ul><li>Clean their own output (deslop-agent)</li> <li>Work the issue queue from pick to land (<a href="#foreman" target="_blank" rel="noopener">/foreman</a>)</li> <li>Lint their own configs (<a href="#agnix" target="_blank" rel="noopener">/agnix</a>)</li> <li>Research what they need to know (<a href="#learn" target="_blank" rel="noopener">/learn</a>)</li></ul> If it can be specified, it can be delegated.</p> <p><strong>11. Orchestrator stays high-level</strong></p> <p>The main workflow orchestrator doesn't read files, search code, or write implementations. It launches specialized agents and receives their outputs. Keeps the orchestrator's context window available for coordination rather than filled with file contents.</p> <p><strong>12. Composable, not monolithic</strong></p> <p>Every command works standalone. <a href="#deslop" target="_blank" rel="noopener"><code>/deslop</code></a> cleans code without needing <a href="#foreman" target="_blank" rel="noopener"><code>/foreman</code></a>. <a href="#agnix" target="_blank" rel="noopener"><code>/agnix</code></a> lints configs without needing the full workflow. Pieces compose together, but each piece is useful on its own.</p> <h3>What This Gets You</h3> <ul><li><strong>Run multiple sessions</strong> - Different tasks in different worktrees, no interference</li> <li><strong>Fast iteration</strong> - Approve plan, check results, repeat</li> <li><strong>Stay in the interesting parts</strong> - Policy decisions, architecture choices, edge cases</li> <li><strong>Minimal review burden</strong> - Most issues caught and fixed before you see the output</li> <li><strong>No repetitive requests</strong> - The workflow you want, without asking each time</li> <li><strong>Scale horizontally</strong> - More sessions, more tasks, same oversight level</li></ul> </details> <hr> <h2>Installation</h2> <h3>Claude Code (Recommended way)</h3> <pre><code class="bash">/plugin marketplace add agent-sh/agentsys /plugin install foreman@agentsys</code></pre> <h3>All Platforms (npm)</h3> <pre><code class="bash">npm install -g agentsys && agentsys</code></pre> <p>Interactive installer for Claude Code, OpenCode, Codex CLI, Cursor, and Kiro.</p> <pre><code class="bash"># Non-interactive install agentsys --tool claude # Single tool agentsys --tool cursor # Cursor (~/.cursor: skills, commands, rules) agentsys --tool kiro # Kiro (~/.kiro: prompts, skills, agents) agentsys --tools "claude,opencode" # Multiple tools agentsys --development # Dev mode (bypasses marketplace) agentsys install deslop # One plugin, or plugin:component for one agent, skill or command</code></pre> <p><a href="./docs/INSTALLATION.md" target="_blank" rel="noopener">Full installation guide →</a></p> <hr> <h2>Requirements</h2> <p><strong>Required:</strong> <ul><li>Git</li> <li>Node.js 18+</li></ul> <strong>For GitHub workflows:</strong> <ul><li>GitHub CLI (<code>gh</code>) authenticated</li></ul> <strong>For GitLab workflows:</strong> <ul><li>GitLab CLI (<code>glab</code>) authenticated</li></ul> <strong>For repo-intel data (read by deslop):</strong> <ul><li><a href="https://github.com/agent-sh/agent-analyzer" target="_blank" rel="noopener">agent-analyzer</a>, downloaded automatically on first use</li></ul> <strong>For /foreman:</strong> <ul><li>Node.js 22+, <code>gh</code> authenticated, and <code>waitfor</code> on <code>PATH</code></li></ul> <strong>For /agnix:</strong> <ul><li><a href="https://github.com/agent-sh/agnix" target="_blank" rel="noopener">agnix CLI</a> installed (<code>npm install -g agnix</code>, <code>cargo install agnix-cli</code>, or <code>brew tap agent-sh/agnix && brew install agnix</code>)</li></ul> <strong>Local diagnostics (optional):</strong> <pre><code class="bash">npm run detect # Platform detection (CI, deploy, project type) npm run verify # Tool availability + versions</code></pre></p> <hr> <h2>Research & Testing</h2> <p>The system is built on research, not guesswork.</p> <p><strong>Knowledge base</strong> (<code>agent-docs/</code>): 8,000 lines of curated documentation from Anthropic, OpenAI, Google, and Microsoft covering: <ul><li>Agent architecture and design patterns</li> <li>Prompt engineering techniques</li> <li>Function calling and tool use</li> <li>Context efficiency and token optimization</li> <li>Multi-agent systems and orchestration</li> <li>Instruction following reliability</li></ul> <strong>Testing:</strong> <ul><li>Jest suite runs on Linux and Windows in CI</li> <li>Cross-platform validation (Claude Code, OpenCode, Codex CLI, Cursor, Kiro)</li></ul> <strong>Methodology:</strong> <ul><li>Certainty levels derived from pattern analysis across repositories</li></ul> <hr></p> <h2>Documentation</h2> <p>| Topic | Link | |-------|------| | Installation | <a href="./docs/INSTALLATION.md" target="_blank" rel="noopener">docs/INSTALLATION.md</a> | | Cross-Platform Setup | <a href="./docs/CROSS_PLATFORM.md" target="_blank" rel="noopener">docs/CROSS_PLATFORM.md</a> | | Usage Examples | <a href="./docs/USAGE.md" target="_blank" rel="noopener">docs/USAGE.md</a> | | Architecture | <a href="./docs/ARCHITECTURE.md" target="_blank" rel="noopener">docs/ARCHITECTURE.md</a> |</p> <h3>Workflow Deep-Dives</h3> <p>| Workflow | Link | |----------|------| | /foreman Flow | <a href="./docs/workflows/FOREMAN.md" target="_blank" rel="noopener">docs/workflows/FOREMAN.md</a> |</p> <h3>Reference</h3> <p>| Topic | Link | |-------|------| | Slop Patterns | <a href="./docs/reference/SLOP-PATTERNS.md" target="_blank" rel="noopener">docs/reference/SLOP-PATTERNS.md</a> | | Agent Reference | <a href="./docs/reference/AGENTS.md" target="_blank" rel="noopener">docs/reference/AGENTS.md</a> |</p> <hr> <h2>Support</h2> <ul><li><strong>Issues:</strong> <a href="https://github.com/agent-sh/agentsys/issues" target="_blank" rel="noopener">github.com/agent-sh/agentsys/issues</a></li> <li><strong>Discussions:</strong> <a href="https://github.com/agent-sh/agentsys/discussions" target="_blank" rel="noopener">github.com/agent-sh/agentsys/discussions</a></li></ul> <hr> <p>MIT License | Made by <a href="https://github.com/avifenesh" target="_blank" rel="noopener">Avi Fenesh</a></p> <h2>Contributor instructions</h2> <p>Repository instructions live in AGENTS.md. Claude Code v2.1.281 or later supports native loading across supported providers. If an ancestor instruction file masks AGENTS.md, select <code>claude-md-and-agents-md</code> in user-level Project instructions. The platform helpers retain support for analyzing older instruction layouts; they do not control the native CLI loader. See <a href="https://code.claude.com/docs/en/memory#agents-md" target="_blank" rel="noopener">Claude Code instruction loading</a>.</p> </article> <!-- Article Completion Section --> <div class="reader-footer mt-5 pt-5"> <div class="reader-completion-box rounded-3 p-4 text-center"> <div class="reader-completion-icon mb-3"> <i class="fas fa-check-circle"></i> </div> <p class="reader-completion-text fw-semibold mb-1" style="font-size: 1rem;">You finished the article!</p> <p class="reader-completion-subtext small mb-4">Want to go deeper or explore more stories?</p> <div class="d-flex justify-content-center gap-3 flex-wrap"> <a href="https://github.com/agent-sh/agentsys" target="_blank" rel="noopener noreferrer" class="btn btn-primary-custom btn-sm rounded-pill px-4"> <i class="fas fa-external-link-alt me-2"></i>Full Article on GitHub Trending </a> <a href="https://neshdevtech.com/news" class="reader-back-btn btn btn-sm rounded-pill px-4"> <i class="fas fa-newspaper me-2"></i>More Tech News </a> </div> </div> </div> <!-- Internal Linking CTA --> </div> </div> </div> <!-- Sidebar (desktop only) --> <div class="col-lg-4 col-xl-3 d-none d-lg-block"> <div class="sticky-top" style="top: 80px;"> <!-- Mini Controller Widget --> <div class="card border-0 shadow-sm mb-4 bg-surface"> <div class="card-body p-4"> <h6 class="fw-bold mb-3"><i class="fas fa-glasses me-2 text-primary-custom"></i>Reading Mode</h6> <p class="text-secondary-custom small mb-4">Adjust typography and theme to your preference.</p> <div class="mb-3"> <label class="text-muted-custom small d-block mb-2">Typography</label> <div class="d-flex gap-2"> <button class="btn btn-light btn-sm flex-grow-1 border-custom" id="sidebar-font-serif">Serif</button> <button class="btn btn-light btn-sm flex-grow-1 border-custom" id="sidebar-font-sans">Sans</button> </div> </div> <div class="mb-4"> <label class="text-muted-custom small d-block mb-2">Text Scale</label> <div class="d-flex gap-2"> <button class="btn btn-light btn-sm flex-grow-1 border-custom" id="sidebar-size-dec"><i class="fas fa-minus small"></i></button> <button class="btn btn-light btn-sm flex-grow-1 border-custom" id="sidebar-size-inc"><i class="fas fa-plus small"></i></button> </div> </div> <div class="border-top border-light-custom pt-3"> <a href="https://github.com/agent-sh/agentsys" target="_blank" rel="noopener noreferrer" class="btn btn-outline-primary btn-sm w-100 rounded-pill"> View Original <i class="fas fa-external-link-alt ms-1"></i> </a> </div> </div> </div> <!-- Sidebar Related News --> <div class="card border-0 shadow-sm bg-surface"> <div class="card-header bg-transparent border-0 pt-4 pb-0 px-4"> <h5 class="card-title fw-bold mb-0" style="font-size: 1.05rem;"> <i class="fas fa-newspaper me-2 text-primary-custom"></i>Related Stories </h5> </div> <div class="card-body p-0"> <div class="list-group list-group-flush rounded-0 bg-transparent"> <a href="https://neshdevtech.com/news/my-open-source-project-is-brand-new-and-already-has-contributors-heres-what-i-did-w3skT" class="list-group-item list-group-item-action border-0 px-4 py-3 bg-transparent border-bottom border-light-custom hover-bg-light"> <h6 class="mb-1 fw-semibold text-truncate-2" style="font-size: 0.875rem; line-height: 1.4;"> My open source project is brand new and already has contributors. Here's what I did. </h6> <div class="d-flex align-items-center justify-content-between text-muted-custom mt-1" style="font-size: 0.72rem;"> <span>Dev.to</span> <span>1 day ago</span> </div> </a> <a href="https://neshdevtech.com/news/azures-weakest-link-five-full-cross-tenant-compromises-SS1rq" class="list-group-item list-group-item-action border-0 px-4 py-3 bg-transparent border-bottom border-light-custom hover-bg-light"> <h6 class="mb-1 fw-semibold text-truncate-2" style="font-size: 0.875rem; line-height: 1.4;"> Azure's Weakest Link – Five Full Cross-Tenant Compromises </h6> <div class="d-flex align-items-center justify-content-between text-muted-custom mt-1" style="font-size: 0.72rem;"> <span>Hacker News</span> <span>1 day ago</span> </div> </a> <a href="https://neshdevtech.com/news/we-need-to-talk-about-how-gross-modern-web-design-has-become-and-how-we-built-an-ai-to-catch-it-hzYII" class="list-group-item list-group-item-action border-0 px-4 py-3 bg-transparent border-bottom border-light-custom hover-bg-light"> <h6 class="mb-1 fw-semibold text-truncate-2" style="font-size: 0.875rem; line-height: 1.4;"> We Need to Talk About How Gross Modern Web Design Has Become (And How We Built an AI to Catch It) </h6> <div class="d-flex align-items-center justify-content-between text-muted-custom mt-1" style="font-size: 0.72rem;"> <span>Dev.to</span> <span>1 day ago</span> </div> </a> <a href="https://neshdevtech.com/news/wasmer-yc-s19-is-hiring-oshypervisor-experts-in-europe-k4qMi" class="list-group-item list-group-item-action border-0 px-4 py-3 bg-transparent border-bottom border-light-custom hover-bg-light"> <h6 class="mb-1 fw-semibold text-truncate-2" style="font-size: 0.875rem; line-height: 1.4;"> Wasmer (YC S19) Is Hiring OS/Hypervisor Experts in Europe </h6> <div class="d-flex align-items-center justify-content-between text-muted-custom mt-1" style="font-size: 0.72rem;"> <span>Hacker News</span> <span>1 day ago</span> </div> </a> <a href="https://neshdevtech.com/news/zero-click-silent-remote-android-dos-cFpyw" class="list-group-item list-group-item-action border-0 px-4 py-3 bg-transparent border-bottom border-light-custom hover-bg-light"> <h6 class="mb-1 fw-semibold text-truncate-2" style="font-size: 0.875rem; line-height: 1.4;"> Zero click, silent, remote Android DoS </h6> <div class="d-flex align-items-center justify-content-between text-muted-custom mt-1" style="font-size: 0.72rem;"> <span>Hacker News</span> <span>2 days ago</span> </div> </a> </div> </div> </div> </div> </div> </div> <!-- Mobile: Related News horizontal scroll strip --> <div class="d-block d-lg-none mt-4"> <h6 class="fw-bold mb-3 px-1"> <i class="fas fa-newspaper me-2 text-primary-custom"></i>Related Stories </h6> <div class="related-scroll-strip"> <a href="https://neshdevtech.com/news/my-open-source-project-is-brand-new-and-already-has-contributors-heres-what-i-did-w3skT" class="related-scroll-card"> <p class="related-scroll-title">My open source project is brand new and already has contributors. Here...</p> <div class="related-scroll-meta"> <span>Dev.to</span> <span>1 day ago</span> </div> </a> <a href="https://neshdevtech.com/news/azures-weakest-link-five-full-cross-tenant-compromises-SS1rq" class="related-scroll-card"> <p class="related-scroll-title">Azure's Weakest Link – Five Full Cross-Tenant Compromises</p> <div class="related-scroll-meta"> <span>Hacker News</span> <span>1 day ago</span> </div> </a> <a href="https://neshdevtech.com/news/we-need-to-talk-about-how-gross-modern-web-design-has-become-and-how-we-built-an-ai-to-catch-it-hzYII" class="related-scroll-card"> <p class="related-scroll-title">We Need to Talk About How Gross Modern Web Design Has Become (And How...</p> <div class="related-scroll-meta"> <span>Dev.to</span> <span>1 day ago</span> </div> </a> <a href="https://neshdevtech.com/news/wasmer-yc-s19-is-hiring-oshypervisor-experts-in-europe-k4qMi" class="related-scroll-card"> <p class="related-scroll-title">Wasmer (YC S19) Is Hiring OS/Hypervisor Experts in Europe</p> <div class="related-scroll-meta"> <span>Hacker News</span> <span>1 day ago</span> </div> </a> <a href="https://neshdevtech.com/news/zero-click-silent-remote-android-dos-cFpyw" class="related-scroll-card"> <p class="related-scroll-title">Zero click, silent, remote Android DoS</p> <div class="related-scroll-meta"> <span>Hacker News</span> <span>2 days ago</span> </div> </a> </div> </div> </div> </section> </main> <!-- Footer --> <!-- Footer --> <footer class="py-5 mt-5 position-relative overflow-hidden" style="background: var(--surface); border-top: 1px solid var(--border); box-shadow: var(--shadow-lg);"> <!-- Decorative subtle background gradients --> <div class="position-absolute" style="top: -200px; right: -200px; width: 400px; height: 400px; background: rgba(var(--primary-rgb), 0.03); filter: blur(100px); border-radius: 50%; pointer-events: none;"></div> <div class="position-absolute" style="bottom: -200px; left: -200px; width: 400px; height: 400px; background: rgba(var(--accent-rgb), 0.03); filter: blur(100px); border-radius: 50%; pointer-events: none;"></div> <div class="container position-relative z-3"> <div class="row g-4"> <!-- Column 1: Brand & About --> <div class="col-lg-4 col-md-6"> <div class="d-flex align-items-center mb-3"> <span class="brand-mark me-2" style="width: 42px; height: 42px;" aria-hidden="true"> <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"><polyline points="16 18 22 12 16 6"/><polyline points="8 6 2 12 8 18"/></svg> </span> <span class="fs-4 fw-extrabold text-primary-custom" style="letter-spacing: -0.05em; font-family: 'JetBrains Mono', monospace;">NeshDevTech</span> </div> <p class="text-secondary-custom mb-4 pe-lg-4" style="font-size: 0.95rem; line-height: 1.6;">NeshDevTech is a professional technology firm passionate about creating innovative, high-performance web and mobile solutions.</p> <div class="d-flex gap-2"> </div> </div> <!-- Column 2: Navigation Links --> <div class="col-lg-2 col-md-6 col-6"> <h6 class="text-primary-custom fw-bold text-uppercase mb-3" style="font-size: 0.85rem; letter-spacing: 0.05em;">Quick Navigation</h6> <ul class="list-unstyled mb-0"> <li class="mb-2"><a href="https://neshdevtech.com" class="footer-link text-secondary-custom text-decoration-none d-inline-block">Home</a></li> <li class="mb-2"><a href="https://neshdevtech.com/projects" class="footer-link text-secondary-custom text-decoration-none d-inline-block">Projects</a></li> <li class="mb-2"><a href="https://neshdevtech.com/blog" class="footer-link text-secondary-custom text-decoration-none d-inline-block">Tech Blog</a></li> <li class="mb-2"><a href="https://neshdevtech.com/pricing" class="footer-link text-secondary-custom text-decoration-none d-inline-block">Get Quote</a></li> <li class="mb-2"><a href="https://neshdevtech.com/contact" class="footer-link text-secondary-custom text-decoration-none d-inline-block">Contact Me</a></li> </ul> </div> <!-- Column 3: Tech Stack & Services --> <div class="col-lg-3 col-md-6 col-6"> <h6 class="text-primary-custom fw-bold text-uppercase mb-3" style="font-size: 0.85rem; letter-spacing: 0.05em;">Core Services</h6> <ul class="list-unstyled mb-0"> <li class="text-secondary-custom mb-2 small"><i class="bi bi-rocket-takeoff text-primary-custom me-2"></i>Custom Laravel Apps</li> <li class="text-secondary-custom mb-2 small"><i class="bi bi-laptop text-primary-custom me-2"></i>SPA (React & Vue.js)</li> <li class="text-secondary-custom mb-2 small"><i class="bi bi-phone-vibrate text-primary-custom me-2"></i>Flutter Mobile Apps</li> <li class="text-secondary-custom mb-2 small"><i class="bi bi-search text-primary-custom me-2"></i>SEO & Performance Opt</li> </ul> </div> <!-- Column 4: Contact & Availability --> <div class="col-lg-3 col-md-6"> <h6 class="text-primary-custom fw-bold text-uppercase mb-3" style="font-size: 0.85rem; letter-spacing: 0.05em;">Get in Touch</h6> <ul class="list-unstyled mb-3"> <li class="text-secondary-custom mb-2 small d-flex align-items-center"> <i class="bi bi-geo-alt text-primary-custom me-2 fs-6"></i>Nairobi, Kenya </li> <li class="text-secondary-custom mb-2 small d-flex align-items-center"> <i class="bi bi-envelope-at text-primary-custom me-2 fs-6"></i> <a href="mailto:info@neshdevtech.com" class="text-secondary-custom text-decoration-none footer-link">info@neshdevtech.com</a> </li> </ul> <div class="bg-primary-custom-light border border-primary-custom border-opacity-10 p-3 rounded-3" style="border-radius: 12px !important;"> <small class="d-block fw-semibold text-primary-custom mb-1"><i class="bi bi-patch-check-fill me-1"></i>Available for Projects</small> <p class="mb-0 text-secondary-custom small" style="font-size: 0.8rem; line-height: 1.4;">Need a high-performing system? Let's discuss your project.</p> </div> </div> </div> <hr style="border-color: var(--border); opacity: 0.8; margin: 2.5rem 0 2rem;"> <div class="row align-items-center"> <div class="col-md-6 text-center text-md-start mb-2 mb-md-0"> <p class="mb-0 text-secondary-custom small">© 2026 NeshDevTech. All rights reserved.</p> </div> <div class="col-md-6 text-center text-md-end"> <p class="mb-0 text-secondary-custom small">Built with <i class="bi bi-heart-fill text-danger"></i></p> </div> </div> </div> </footer> <!-- Bootstrap JS --> <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script> <!-- Custom JavaScript --> <script> // Theme Management System class ThemeManager { constructor() { this.themeToggle = document.getElementById('themeToggle'); this.themeIcon = document.getElementById('theme-icon'); this.mobileThemeToggle = document.getElementById('mobileThemeToggle'); this.mobileThemeIcon = document.getElementById('mobile-theme-icon'); this.init(); } init() { // Get current theme (already applied in head) const currentTheme = document.documentElement.getAttribute('data-theme') || 'light'; // Update icons to match current theme this.updateIcon(currentTheme); // Listen for desktop theme toggle clicks if (this.themeToggle) { this.themeToggle.addEventListener('click', (e) => { e.preventDefault(); this.toggleTheme(); }); } // Listen for mobile theme toggle clicks if (this.mobileThemeToggle) { this.mobileThemeToggle.addEventListener('click', (e) => { e.preventDefault(); this.toggleTheme(); }); } // Listen for system theme changes window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', (e) => { if (!localStorage.getItem('theme')) { this.setTheme(e.matches ? 'dark' : 'light'); } }); } setTheme(theme) { document.documentElement.setAttribute('data-theme', theme); document.documentElement.setAttribute('data-bs-theme', theme); if (theme === 'dark') { document.documentElement.classList.add('dark'); } else { document.documentElement.classList.remove('dark'); } localStorage.setItem('theme', theme); this.updateThemeColor(theme); this.updateIcon(theme); } updateThemeColor(theme) { document.querySelectorAll('meta[name="theme-color"]').forEach((meta) => { meta.setAttribute('content', theme === 'dark' ? '#1c2028' : '#e4e8f0'); }); } toggleTheme() { const currentTheme = document.documentElement.getAttribute('data-theme'); const newTheme = currentTheme === 'dark' ? 'light' : 'dark'; this.setTheme(newTheme); // Close mobile menu if open const navbarCollapse = document.querySelector('.navbar-collapse'); if (navbarCollapse && navbarCollapse.classList.contains('show') && window.innerWidth < 992) { setTimeout(() => { const bsCollapse = new bootstrap.Collapse(navbarCollapse, { toggle: false }); bsCollapse.hide(); }, 150); } } updateIcon(theme) { const iconClass = theme === 'dark' ? 'bi bi-sun-fill' : 'bi bi-moon-fill'; // Update desktop icon if (this.themeIcon) { this.themeIcon.className = iconClass; } // Update mobile icon if (this.mobileThemeIcon) { this.mobileThemeIcon.className = iconClass; } } } // Initialize theme manager when DOM is loaded document.addEventListener('DOMContentLoaded', () => { new ThemeManager(); // Initialize navbar live clock (function initNavClock(){ const clockEls = Array.from(document.querySelectorAll('[data-clock]')); if (!clockEls.length) return; const locale = navigator.language || 'en-US'; // Detect 12/24h by locale: use resolvedOptions hourCycle when available const dtf = new Intl.DateTimeFormat(locale, { hour: 'numeric' }); const uses12h = (dtf.resolvedOptions().hour12 === true); const fmt = new Intl.DateTimeFormat(locale, { hour: '2-digit', minute: '2-digit', second: '2-digit', hour12: uses12h }); const update = () => { const now = new Date(); const text = fmt.format(now); clockEls.forEach(el => { el.textContent = text; }); }; update(); setInterval(update, 1000); })(); }); // Smooth scrolling for anchor links document.querySelectorAll('a[href^="#"]').forEach(anchor => { anchor.addEventListener('click', function (e) { e.preventDefault(); const target = document.querySelector(this.getAttribute('href')); if (target) { target.scrollIntoView({ behavior: 'smooth', block: 'start' }); } }); }); // Navbar background on scroll window.addEventListener('scroll', () => { const navbar = document.querySelector('.navbar'); if (window.scrollY > 50) { navbar.style.backdropFilter = 'blur(20px)'; navbar.style.backgroundColor = 'rgba(var(--background-rgb), 0.8)'; } else { navbar.style.backdropFilter = 'blur(10px)'; navbar.style.backgroundColor = 'var(--background)'; } }); // Add fade-in animation to elements when they come into view const observerOptions = { threshold: 0.1, rootMargin: '0px 0px -50px 0px' }; const observer = new IntersectionObserver((entries) => { entries.forEach(entry => { if (entry.isIntersecting) { entry.target.classList.add('animate-fade-in-up'); } }); }, observerOptions); // Observe elements with animation class document.addEventListener('DOMContentLoaded', () => { document.querySelectorAll('.animate-on-scroll').forEach(el => { observer.observe(el); }); }); // Enhanced mobile menu functionality document.addEventListener('DOMContentLoaded', function() { const navbarToggler = document.querySelector('.navbar-toggler'); const navbarCollapse = document.querySelector('.navbar-collapse'); const navLinks = document.querySelectorAll('.navbar-nav .nav-link'); const socialLinks = document.querySelectorAll('.navbar-nav .d-flex a'); let isToggling = false; // Prevent rapid clicking // Enhanced menu close function function closeMenu() { if (navbarCollapse && navbarCollapse.classList.contains('show')) { // Use Bootstrap's collapse API for smooth transition const bsCollapse = new bootstrap.Collapse(navbarCollapse, { toggle: false }); bsCollapse.hide(); } // Reset hamburger animation and aria-expanded if (navbarToggler) { navbarToggler.classList.remove('active'); navbarToggler.setAttribute('aria-expanded', 'false'); } } // Enhanced hamburger toggle functionality if (navbarToggler) { navbarToggler.addEventListener('click', function(e) { e.preventDefault(); // Prevent rapid clicking if (isToggling) return; isToggling = true; const isMenuOpen = navbarCollapse.classList.contains('show'); if (isMenuOpen) { closeMenu(); } else { // Open menu const bsCollapse = new bootstrap.Collapse(navbarCollapse, { toggle: false }); bsCollapse.show(); navbarToggler.setAttribute('aria-expanded', 'true'); } // Reset toggle flag after animation setTimeout(() => { isToggling = false; }, 350); }); } // Close menu when clicking outside document.addEventListener('click', function(event) { const isClickInsideNav = navbarCollapse.contains(event.target); const isToggler = navbarToggler.contains(event.target); const isMenuOpen = navbarCollapse.classList.contains('show'); if (!isClickInsideNav && !isToggler && isMenuOpen && window.innerWidth < 992) { closeMenu(); } }); // Close menu when clicking on navigation links (mobile) navLinks.forEach(function(link) { link.addEventListener('click', function(e) { const isMenuOpen = navbarCollapse.classList.contains('show'); if (isMenuOpen && window.innerWidth < 992 && !this.classList.contains('dropdown-toggle')) { // Add a small delay to let the link navigation start setTimeout(() => { closeMenu(); }, 100); } }); }); // Close menu when clicking on social links (mobile) socialLinks.forEach(function(link) { link.addEventListener('click', function() { if (window.innerWidth < 992) { setTimeout(() => { closeMenu(); }, 100); } }); }); // Escape key closes menu document.addEventListener('keydown', function(event) { if (event.key === 'Escape' && window.innerWidth < 992) { closeMenu(); } }); // Handle window resize to close menu when switching to desktop window.addEventListener('resize', function() { if (window.innerWidth >= 992) { closeMenu(); } }); // Theme toggle functionality is handled by ThemeManager class // No separate event listener needed here // Prevent menu items from being too small on touch devices if ('ontouchstart' in window) { navLinks.forEach(function(link) { link.style.minHeight = '44px'; link.style.display = 'flex'; link.style.alignItems = 'center'; }); } // Add smooth scrolling for anchor links within pages navLinks.forEach(function(link) { const href = link.getAttribute('href'); if (href && href.startsWith('#')) { link.addEventListener('click', function(e) { e.preventDefault(); const target = document.querySelector(href); if (target) { target.scrollIntoView({ behavior: 'smooth', block: 'start' }); } closeMenu(); }); } }); }); </script> <script> // Sticky navbar scroll enhancement (desktop only) (function () { const navbar = document.querySelector('.navbar'); if (!navbar) return; let ticking = false; const SCROLL_THRESHOLD = 20; // px function updateNavbar() { if (window.innerWidth >= 992) { if (window.scrollY > SCROLL_THRESHOLD) { navbar.classList.add('scrolled'); } else { navbar.classList.remove('scrolled'); } } else { // Always remove on mobile so it doesn't affect mobile look navbar.classList.remove('scrolled'); } ticking = false; } window.addEventListener('scroll', function () { if (!ticking) { requestAnimationFrame(updateNavbar); ticking = true; } }, { passive: true }); window.addEventListener('resize', updateNavbar, { passive: true }); // Run once on page load in case page is already scrolled (e.g. back-navigation) updateNavbar(); })(); </script> <script> // Lightweight toast badge notifications (function(){ function ensureContainer(){ let c = document.getElementById('toast-badge-container'); if(!c){ c = document.createElement('div'); c.id = 'toast-badge-container'; c.setAttribute('aria-live','polite'); c.setAttribute('aria-atomic','true'); document.body.appendChild(c); } return c; } function iconFor(type){ switch(type){ case 'success': return '<i class="bi bi-check-circle"></i>'; case 'warning': return '<i class="bi bi-exclamation-triangle"></i>'; case 'error': return '<i class="bi bi-x-circle"></i>'; default: return '<i class="bi bi-info-circle"></i>'; } } window.notifyBadge = function(message, opts={}){ const { type = 'success', timeout = 2500 } = opts; const container = ensureContainer(); const el = document.createElement('div'); el.className = 'toast-badge ' + type; el.setAttribute('role','status'); el.innerHTML = '<span class="icon">'+iconFor(type)+'</span><span>'+message+'</span>'; container.appendChild(el); requestAnimationFrame(()=>{ el.classList.add('show'); }); const remove = () => { el.classList.remove('show'); setTimeout(()=> el.remove(), 200); }; setTimeout(remove, timeout); return { remove }; }; // Global copy helper used by inline buttons window.copyToClipboard = function(url){ try { navigator.clipboard.writeText(url).then(()=>{ // Update the clicked button, if available let btn = document.activeElement && document.activeElement.tagName === 'BUTTON' ? document.activeElement : (window.event && window.event.target && window.event.target.closest ? window.event.target.closest('button') : null); if (btn){ const original = btn.innerHTML; btn.innerHTML = '<i class="fas fa-check me-1"></i> Copied!'; btn.classList.remove('btn-outline-secondary'); btn.classList.add('btn-success'); setTimeout(()=>{ btn.innerHTML = original; btn.classList.remove('btn-success'); btn.classList.add('btn-outline-secondary'); }, 2000); } notifyBadge('Link copied to clipboard', { type: 'success' }); }).catch((err)=>{ console.error('Clipboard error:', err); notifyBadge('Could not copy link', { type: 'error' }); }); } catch (e) { console.error(e); notifyBadge('Copy not supported', { type: 'error' }); } }; })(); </script> <a href="https://wa.me/254768033943?text=Hi%2C+I+visited+your+website+and+would+like+to+inquire+about+your+services." class="whatsapp-btn-float" target="_blank" rel="noopener noreferrer" aria-label="Chat on WhatsApp"> <i class="fab fa-whatsapp"></i> <span class="whatsapp-tooltip">Chat with me</span> </a> <style> .whatsapp-btn-float { position: fixed; bottom: 24px; right: 24px; z-index: 1050; display: flex; align-items: center; justify-content: center; background-color: #25d366; color: #fff !important; width: 60px; height: 60px; border-radius: 50%; box-shadow: 0 4px 16px rgba(37, 211, 102, 0.45); text-decoration: none; transition: all 0.3s cubic-bezier(0.175, 0.885, 0.32, 1.275); } .whatsapp-btn-float i { font-size: 32px; transition: transform 0.3s ease; } .whatsapp-btn-float:hover { background-color: #128c7e; transform: scale(1.1); box-shadow: 0 6px 20px rgba(18, 140, 126, 0.6); } .whatsapp-btn-float:hover i { transform: rotate(10deg); } /* Pulsing ring animation */ .whatsapp-btn-float::before { content: ''; position: absolute; width: 100%; height: 100%; border-radius: 50%; background-color: #25d366; opacity: 0.7; z-index: -1; animation: whatsapp-pulse 2s infinite; } @keyframes whatsapp-pulse { 0% { transform: scale(1); opacity: 0.7; } 100% { transform: scale(1.6); opacity: 0; } } /* Tooltip style utilizing site-wide theme colors */ .whatsapp-btn-float .whatsapp-tooltip { position: absolute; right: 76px; background-color: var(--surface); color: var(--text-primary); border: 1px solid var(--border); padding: 8px 14px; border-radius: 20px; font-size: 14px; font-weight: 600; white-space: nowrap; opacity: 0; visibility: hidden; transform: translateX(10px); transition: all 0.3s ease; box-shadow: var(--shadow); pointer-events: none; } .whatsapp-btn-float:hover .whatsapp-tooltip { opacity: 1; visibility: visible; transform: translateX(0); } /* Responsive Design */ @media (max-width: 768px) { .whatsapp-btn-float { bottom: 16px; right: 16px; width: 50px; height: 50px; } .whatsapp-btn-float i { font-size: 26px; } .whatsapp-btn-float .whatsapp-tooltip { display: none; /* Hide tooltip on small screens to prevent overlap issues */ } } </style> <!-- Mobile More Menu (Offcanvas Bottom Sheet) - Auth actions only --> <style> /* ══════════════════════════════════════════════════════════════ NEUMORPHISM REVAMP (front-end only) Soft-extruded UI: same-tone surfaces with dual light/dark shadows. This block sits at the end of the body so it layers over every page's own styles without touching page markup. ══════════════════════════════════════════════════════════════ */ /* ── Buttons: extruded, press-in on click ─────────────────────── */ .btn { box-shadow: var(--shadow) !important; transition: all 0.3s cubic-bezier(0.16, 1, 0.3, 1) !important; } .btn:hover { transform: translateY(-2px) !important; box-shadow: var(--shadow-md) !important; } .btn:active:not(.btn-link), .btn.show { transform: translateY(1px) scale(0.99) !important; box-shadow: var(--shadow-inset) !important; } .btn-primary-custom, .btn-primary, .btn-success { background: var(--primary-color) !important; border-color: var(--primary-color) !important; color: #fff !important; box-shadow: var(--shadow) !important; } .btn-primary-custom:hover, .btn-primary:hover, .btn-success:hover { background: var(--primary-dark) !important; border-color: var(--primary-dark) !important; color: #fff !important; box-shadow: var(--shadow-md) !important; } .btn-outline-primary-custom, .btn-outline-primary, .btn-outline-success, .btn-outline-secondary, .btn-outline-danger, .btn-outline-dark, .btn-light { background-color: var(--surface) !important; border-color: transparent !important; color: var(--text-primary) !important; box-shadow: var(--shadow) !important; } .btn-outline-primary-custom:hover, .btn-outline-primary:hover { background-color: var(--primary-color) !important; border-color: var(--primary-color) !important; color: #fff !important; box-shadow: var(--shadow-md) !important; } .btn-outline-success:hover { background-color: var(--primary-color) !important; border-color: var(--primary-color) !important; color: #fff !important; box-shadow: var(--shadow-md) !important; } .btn-outline-secondary:hover { background-color: var(--text-secondary) !important; border-color: var(--text-secondary) !important; color: #fff !important; box-shadow: var(--shadow-md) !important; } .btn-outline-danger:hover { background-color: #ef4444 !important; border-color: #ef4444 !important; color: #fff !important; box-shadow: var(--shadow-md) !important; } .btn-outline-dark:hover, .btn-light:hover { background-color: var(--text-secondary) !important; border-color: var(--text-secondary) !important; color: var(--background) !important; box-shadow: var(--shadow-md) !important; } .btn-dark { background-color: var(--text-primary) !important; border-color: var(--text-primary) !important; color: var(--background) !important; box-shadow: var(--shadow) !important; } /* ── Forms: pressed-in surfaces ────────────────────────────────── */ .form-control, .form-select { background-color: var(--surface) !important; border: 1px solid transparent !important; color: var(--text-primary) !important; border-radius: 14px !important; box-shadow: var(--shadow-inset) !important; transition: box-shadow 0.3s ease, background-color 0.25s ease !important; } .form-control:focus, .form-select:focus { background-color: var(--surface) !important; border-color: transparent !important; color: var(--text-primary) !important; box-shadow: var(--shadow-inset), 0 0 0 4px rgba(var(--primary-rgb), 0.15) !important; } .form-control::placeholder { color: var(--text-muted) !important; } .input-group-text { background-color: var(--surface-alt) !important; border: 1px solid transparent !important; color: var(--text-secondary) !important; box-shadow: var(--shadow-inset-sm) !important; } /* ── Navbar: floating neumorphic pill (tablet & desktop) ──────── */ .navbar { background-color: rgba(var(--background-rgb), 0.9) !important; border-bottom: 1px solid transparent !important; backdrop-filter: blur(16px) !important; -webkit-backdrop-filter: blur(16px) !important; box-shadow: var(--shadow-md) !important; } [data-theme="dark"] .navbar { box-shadow: var(--shadow-md) !important; } @media (min-width: 768px) { .navbar { top: 14px !important; left: 18px !important; right: 18px !important; width: auto !important; border-radius: 20px !important; padding: 0.6rem 0 !important; box-shadow: var(--shadow-lg) !important; } [data-theme="dark"] .navbar { box-shadow: var(--shadow-lg) !important; } .navbar.scrolled { background-color: rgba(var(--background-rgb), 0.95) !important; border-bottom-color: transparent !important; padding: 0.5rem 0 !important; box-shadow: var(--shadow-lg) !important; } [data-theme="dark"] .navbar.scrolled { background-color: rgba(28, 32, 40, 0.95) !important; box-shadow: var(--shadow-lg) !important; } body { padding-top: 96px !important; } .reading-progress-container { top: 88px !important; } } /* ── Tablet refinements (768px - 991.98px) ────────────────────── */ @media (min-width: 768px) and (max-width: 991.98px) { /* Hamburger dropdown panel blends with the floating pill */ .navbar-collapse { border-radius: 20px !important; border-color: transparent !important; } body { padding-bottom: 0 !important; } .whatsapp-btn-float { bottom: 24px !important; } /* Hero: tighten the inline 80px top padding that assumed a full-height bar, so tablets don't get excessive empty space */ .hero-section-bg { padding-top: 40px !important; } } /* ── Neumorphic brand icon mark ────────────────────────────────── */ .brand-mark { width: 40px; height: 40px; flex-shrink: 0; border-radius: 13px; background-color: var(--surface) !important; color: var(--primary-color) !important; display: inline-flex; align-items: center; justify-content: center; box-shadow: var(--shadow) !important; transition: box-shadow 0.3s cubic-bezier(0.16, 1, 0.3, 1), transform 0.3s cubic-bezier(0.16, 1, 0.3, 1); } .brand-mark svg { width: 21px; height: 21px; } a:hover .brand-mark { box-shadow: var(--shadow-inset-sm) !important; transform: translateY(1px); } .mobile-logo .brand-mark { width: 34px; height: 34px; border-radius: 11px; } .mobile-logo .brand-mark svg { width: 17px; height: 17px; } /* ── Uploaded brand logo image ─────────────────────────────────── */ .brand-img { height: 38px; width: auto; max-width: 160px; object-fit: contain; flex-shrink: 0; } .brand-img-sm { height: 28px; max-width: 120px; } .brand-img-footer { height: 44px; max-width: 180px; } /* ── Mobile bars: floating neumorphic pills (phones) ──────────── */ @media (max-width: 767.98px) { .mobile-top-bar { top: 8px !important; left: 12px !important; right: 12px !important; height: 52px !important; border-radius: 16px !important; border-bottom: none !important; background-color: rgba(var(--surface-rgb), 0.92) !important; box-shadow: var(--shadow-md) !important; } .mobile-bottom-bar { left: 12px !important; right: 12px !important; bottom: 12px !important; height: 58px !important; border-radius: 18px !important; border-top: none !important; background-color: rgba(var(--surface-rgb), 0.92) !important; box-shadow: var(--shadow-md) !important; } body { padding-top: 74px !important; padding-bottom: 86px !important; } .whatsapp-btn-float { bottom: 88px !important; } .reading-progress-container { top: 70px !important; } } /* ── Nav pill widgets: pressed-in ──────────────────────────────── */ .theme-toggle, .mobile-theme-toggle, .nav-clock { background-color: var(--surface) !important; border-color: transparent !important; box-shadow: var(--shadow-inset-sm) !important; } /* ── Icon chips, markers & circles: extruded ───────────────────── */ .icon-wrap, .icon-wrapper, .expertise-card-icon, .timeline-marker, .mobile-more-icon-wrapper, .social-circle, .value-icon, .skill-icon-box { box-shadow: var(--shadow) !important; border-color: transparent !important; } /* ── Panels: extruded ──────────────────────────────────────────── */ .accordion-item { border-color: transparent !important; box-shadow: var(--shadow) !important; } .timeline-content { background: var(--surface) !important; border-radius: 12px !important; padding: 1rem 1.25rem !important; box-shadow: var(--shadow) !important; } .feature-item { background: var(--surface) !important; border-color: transparent !important; box-shadow: var(--shadow) !important; } .feature-item:hover { transform: translateY(-2px) !important; box-shadow: var(--shadow-md) !important; } .progress { background-color: var(--surface-alt) !important; box-shadow: var(--shadow-inset-sm) !important; } /* Card hover: consistent soft neu shadow */ .card-hover:hover, .project-card:hover, .blog-card:hover, .post-card:hover, .news-card:hover, .testimonial-card:hover, .hover-lift:hover, .stats-item:hover { box-shadow: var(--shadow-lg) !important; } </style> <script> document.addEventListener('DOMContentLoaded', function() { const readerCard = document.getElementById('reader-card'); if (!readerCard) return; // Settings Key constants const THEME_KEY = 'reader_mode_theme'; const FONT_KEY = 'reader_mode_font'; const SIZE_KEY = 'reader_mode_size'; // Available Options const themes = ['light', 'sepia', 'dark', 'oled']; const fonts = ['serif', 'sans']; const sizes = ['xs', 'sm', 'md', 'lg', 'xl']; // Default Configs const siteTheme = document.documentElement.getAttribute('data-theme') || 'light'; const defaultTheme = siteTheme === 'dark' ? 'dark' : 'light'; let currentTheme = localStorage.getItem(THEME_KEY) || defaultTheme; let currentFont = localStorage.getItem(FONT_KEY) || 'serif'; let currentSize = localStorage.getItem(SIZE_KEY) || 'md'; // Apply initial config applySettings(); // Register Action Listeners (Toolbar) document.getElementById('btn-font-serif').addEventListener('click', () => setFont('serif')); document.getElementById('btn-font-sans').addEventListener('click', () => setFont('sans')); document.getElementById('btn-size-decrease').addEventListener('click', () => adjustSize(-1)); document.getElementById('btn-size-increase').addEventListener('click', () => adjustSize(1)); document.getElementById('theme-light').addEventListener('click', () => setTheme('light')); document.getElementById('theme-sepia').addEventListener('click', () => setTheme('sepia')); document.getElementById('theme-dark').addEventListener('click', () => setTheme('dark')); document.getElementById('theme-oled').addEventListener('click', () => setTheme('oled')); // Register Action Listeners (Sidebar widgets, if present) const sidebarSerif = document.getElementById('sidebar-font-serif'); const sidebarSans = document.getElementById('sidebar-font-sans'); const sidebarSizeDec = document.getElementById('sidebar-size-dec'); const sidebarSizeInc = document.getElementById('sidebar-size-inc'); if (sidebarSerif) sidebarSerif.addEventListener('click', () => setFont('serif')); if (sidebarSans) sidebarSans.addEventListener('click', () => setFont('sans')); if (sidebarSizeDec) sidebarSizeDec.addEventListener('click', () => adjustSize(-1)); if (sidebarSizeInc) sidebarSizeInc.addEventListener('click', () => adjustSize(1)); // Action Core Functions function setTheme(theme) { currentTheme = theme; localStorage.setItem(THEME_KEY, theme); applySettings(); } function setFont(font) { currentFont = font; localStorage.setItem(FONT_KEY, font); applySettings(); } function adjustSize(delta) { let index = sizes.indexOf(currentSize); index = Math.max(0, Math.min(sizes.length - 1, index + delta)); currentSize = sizes[index]; localStorage.setItem(SIZE_KEY, currentSize); applySettings(); } function applySettings() { // 1. Reset theme classes and apply selected themes.forEach(t => readerCard.classList.remove('reader-theme-' + t)); readerCard.classList.add('reader-theme-' + currentTheme); // Update Theme circles highlight themes.forEach(t => { const circle = document.getElementById('theme-' + t); if (circle) { if (t === currentTheme) { circle.classList.add('active'); } else { circle.classList.remove('active'); } } }); // 2. Reset Font classes and apply selected fonts.forEach(f => readerCard.classList.remove('reader-font-' + f)); readerCard.classList.add('reader-font-' + currentFont); // Highlight Font buttons const serifBtns = [document.getElementById('btn-font-serif'), document.getElementById('sidebar-font-serif')]; const sansBtns = [document.getElementById('btn-font-sans'), document.getElementById('sidebar-font-sans')]; serifBtns.forEach(btn => { if (btn) { if (currentFont === 'serif') { btn.classList.add('btn-primary-custom'); btn.classList.remove('btn-light'); } else { btn.classList.add('btn-light'); btn.classList.remove('btn-primary-custom'); } } }); sansBtns.forEach(btn => { if (btn) { if (currentFont === 'sans') { btn.classList.add('btn-primary-custom'); btn.classList.remove('btn-light'); } else { btn.classList.add('btn-light'); btn.classList.remove('btn-primary-custom'); } } }); // 3. Reset Sizing classes and apply selected sizes.forEach(s => readerCard.classList.remove('reader-size-' + s)); readerCard.classList.add('reader-size-' + currentSize); } // Reading Scroll progress calculation const progressBar = document.getElementById('reading-progress-bar'); window.addEventListener('scroll', () => { const documentHeight = document.documentElement.scrollHeight - window.innerHeight; if (documentHeight > 0) { const scrollProgress = (window.pageYOffset / documentHeight) * 100; progressBar.style.width = scrollProgress + '%'; } }); }); </script> <script> // ══════════════════════════════════════════════════════════════ // PWA: Service Worker registration + install prompt // ══════════════════════════════════════════════════════════════ (function () { const PWA_VERSION = '20260805-1'; // ---- Service worker ------------------------------------ if ('serviceWorker' in navigator) { window.addEventListener('load', () => { navigator.serviceWorker.register('/sw.js?v=' + PWA_VERSION).then((reg) => { // Apply updates (new deploy) on the next load without disruption reg.addEventListener('updatefound', () => { const newWorker = reg.installing; if (!newWorker) return; newWorker.addEventListener('statechange', () => { if (newWorker.state === 'installed' && navigator.serviceWorker.controller) { newWorker.postMessage({ type: 'SKIP_WAITING' }); } }); }); }).catch(() => {}); }); } // ---- Install prompt -------------------------------------- let deferredPrompt = null; const bannerId = 'pwa-install-banner'; const DISMISS_KEY = 'nesh-install-dismissed'; function showInstallBanner() { if (localStorage.getItem(DISMISS_KEY) || document.getElementById(bannerId)) return; const banner = document.createElement('div'); banner.id = bannerId; banner.setAttribute('role', 'dialog'); banner.setAttribute('aria-label', 'Install app'); banner.innerHTML = '<div class="pwa-install-content">' + '<span class="pwa-install-icon">' + '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"><polyline points="16 18 22 12 16 6"/><polyline points="8 6 2 12 8 18"/></svg>' + '</span>' + '<div class="pwa-install-text">' + '<strong>Install NeshDevTech</strong>' + '<span>Get the app on your device</span>' + '</div>' + '<button type="button" class="pwa-install-btn" id="pwa-install-btn">Install</button>' + '<button type="button" class="pwa-install-close" id="pwa-install-close" aria-label="Dismiss">×</button>' + '</div>'; document.body.appendChild(banner); document.getElementById('pwa-install-btn').addEventListener('click', () => { if (!deferredPrompt) return; deferredPrompt.prompt(); deferredPrompt.userChoice.finally(() => { deferredPrompt = null; hideInstallBanner(); }); }); document.getElementById('pwa-install-close').addEventListener('click', () => { localStorage.setItem(DISMISS_KEY, '1'); hideInstallBanner(); }); requestAnimationFrame(() => banner.classList.add('show')); } function hideInstallBanner() { const banner = document.getElementById(bannerId); if (banner) { banner.classList.remove('show'); setTimeout(() => banner.remove(), 250); } } window.addEventListener('beforeinstallprompt', (e) => { e.preventDefault(); deferredPrompt = e; showInstallBanner(); }); window.addEventListener('appinstalled', () => { deferredPrompt = null; hideInstallBanner(); localStorage.setItem(DISMISS_KEY, '1'); }); })(); </script> <style> .pwa-install-content { display: flex; align-items: center; gap: 12px; padding: 14px 18px; } #pwa-install-banner { position: fixed; left: 50%; transform: translate(-50%, 20px); bottom: 84px; z-index: 1070; background: var(--surface); color: var(--text-primary); border: 1px solid var(--border); border-radius: 18px; box-shadow: var(--shadow-lg); opacity: 0; transition: opacity .25s cubic-bezier(.16, 1, .3, 1), transform .25s cubic-bezier(.16, 1, .3, 1); width: min(420px, calc(100vw - 32px)); } #pwa-install-banner.show { opacity: 1; transform: translate(-50%, 0); } .pwa-install-icon { display: inline-flex; align-items: center; justify-content: center; width: 40px; height: 40px; flex: 0 0 40px; border-radius: 12px; background: rgba(var(--primary-rgb), 0.1); border: 1px solid rgba(var(--primary-rgb), 0.2); color: var(--primary-color); } .pwa-install-icon svg { width: 20px; height: 20px; } .pwa-install-text { display: flex; flex-direction: column; flex: 1; min-width: 0; } .pwa-install-text strong { font-size: 0.92rem; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } .pwa-install-text span { font-size: 0.78rem; color: var(--text-secondary); } .pwa-install-btn { background: var(--primary-color); border: none; color: #fff; font-weight: 600; font-size: 0.85rem; padding: 8px 18px; border-radius: 9999px; cursor: pointer; white-space: nowrap; box-shadow: 0 4px 12px rgba(var(--primary-rgb), 0.25); transition: all .2s ease; } .pwa-install-btn:hover { background: var(--primary-dark); transform: translateY(-1px); } .pwa-install-close { background: transparent; border: none; color: var(--text-muted); font-size: 1.4rem; line-height: 1; cursor: pointer; padding: 2px 6px; border-radius: 50%; transition: background-color .2s ease, color .2s ease; } .pwa-install-close:hover { background: var(--surface-alt); color: var(--text-primary); } @media (min-width: 992px) { #pwa-install-banner { bottom: 24px; } } </style> </div><!-- /.page-wrapper --> </body> </html>