diff --git a/.gitignore b/.gitignore
index 8a6192a26..697533397 100644
Binary files a/.gitignore and b/.gitignore differ
diff --git a/SLAB/Deadline_Detective_Backend_Webcmd/.env.example b/SLAB/Deadline_Detective_Backend_Webcmd/.env.example
new file mode 100644
index 000000000..2c4f49e40
--- /dev/null
+++ b/SLAB/Deadline_Detective_Backend_Webcmd/.env.example
@@ -0,0 +1,3 @@
+GEMINI_API_KEY=your_gemini_api_key_here
+WEBCMD_PROFILE=default
+MAX_PAGES=5
diff --git a/SLAB/Deadline_Detective_Backend_Webcmd/Dockerfile b/SLAB/Deadline_Detective_Backend_Webcmd/Dockerfile
new file mode 100644
index 000000000..2e3f142f5
--- /dev/null
+++ b/SLAB/Deadline_Detective_Backend_Webcmd/Dockerfile
@@ -0,0 +1,23 @@
+FROM python:3.11-slim
+
+WORKDIR /app
+
+# Install Node.js (required for Webcmd)
+RUN apt-get update && apt-get install -y curl && \
+ curl -fsSL https://deb.nodesource.com/setup_20.x | bash - && \
+ apt-get install -y nodejs && \
+ rm -rf /var/lib/apt/lists/*
+
+# Install Webcmd globally
+RUN npm install -g @agentrhq/webcmd
+
+# Install Python dependencies
+COPY requirements.txt .
+RUN pip install --no-cache-dir -r requirements.txt
+
+# Copy application code
+COPY . .
+
+EXPOSE 8000
+
+CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]
diff --git a/SLAB/Deadline_Detective_Backend_Webcmd/README.md b/SLAB/Deadline_Detective_Backend_Webcmd/README.md
new file mode 100644
index 000000000..040fbc28e
--- /dev/null
+++ b/SLAB/Deadline_Detective_Backend_Webcmd/README.md
@@ -0,0 +1,57 @@
+# Deadline Detective – Backend (Webcmd Version)
+
+AI Browser Agent that finds real college opportunities using **Webcmd** for browser automation.
+
+## What changed
+- Replaced raw Playwright with **Webcmd**
+- Still uses FastAPI + Gemini for planning and analysis
+- Browser control now goes through Webcmd sessions
+
+## Tech Stack
+- FastAPI
+- Google Gemini
+- **Webcmd** (self-learning browser infrastructure)
+- Docker ready
+
+## Prerequisites
+
+1. Node.js 20+
+2. Webcmd installed:
+ ```bash
+ npm install -g @agentrhq/webcmd
+ webcmd doctor
+ ```
+
+## Local Setup
+
+```bash
+python -m venv venv
+source venv/bin/activate # Windows: venv\Scripts\activate
+pip install -r requirements.txt
+
+# Create .env
+cp .env.example .env
+# Edit .env and add your GEMINI_API_KEY
+
+uvicorn main:app --reload --host 0.0.0.0 --port 8000
+```
+
+## API
+
+- `POST /research` → main agent endpoint
+- `GET /health` → health check
+- Docs: http://localhost:8000/docs
+
+## How it works
+
+1. Gemini creates a research plan
+2. Webcmd opens a browser session
+3. Webcmd visits real websites and extracts content
+4. Gemini analyzes eligibility + deadlines
+5. Returns prioritized action plan
+
+## Deploy on Render
+
+- Use the included Dockerfile
+- Add environment variable `GEMINI_API_KEY`
+- Make sure the instance has enough memory (Webcmd + browser needs it)
diff --git a/SLAB/Deadline_Detective_Backend_Webcmd/__pycache__/main.cpython-313.pyc b/SLAB/Deadline_Detective_Backend_Webcmd/__pycache__/main.cpython-313.pyc
new file mode 100644
index 000000000..e1aa20de5
Binary files /dev/null and b/SLAB/Deadline_Detective_Backend_Webcmd/__pycache__/main.cpython-313.pyc differ
diff --git a/SLAB/Deadline_Detective_Backend_Webcmd/app/__init__.py b/SLAB/Deadline_Detective_Backend_Webcmd/app/__init__.py
new file mode 100644
index 000000000..b9ca8ad19
--- /dev/null
+++ b/SLAB/Deadline_Detective_Backend_Webcmd/app/__init__.py
@@ -0,0 +1 @@
+# Deadline Detective Backend
diff --git a/SLAB/Deadline_Detective_Backend_Webcmd/app/__pycache__/__init__.cpython-313.pyc b/SLAB/Deadline_Detective_Backend_Webcmd/app/__pycache__/__init__.cpython-313.pyc
new file mode 100644
index 000000000..565caa69d
Binary files /dev/null and b/SLAB/Deadline_Detective_Backend_Webcmd/app/__pycache__/__init__.cpython-313.pyc differ
diff --git a/SLAB/Deadline_Detective_Backend_Webcmd/app/__pycache__/config.cpython-313.pyc b/SLAB/Deadline_Detective_Backend_Webcmd/app/__pycache__/config.cpython-313.pyc
new file mode 100644
index 000000000..7f0dbcd81
Binary files /dev/null and b/SLAB/Deadline_Detective_Backend_Webcmd/app/__pycache__/config.cpython-313.pyc differ
diff --git a/SLAB/Deadline_Detective_Backend_Webcmd/app/__pycache__/models.cpython-313.pyc b/SLAB/Deadline_Detective_Backend_Webcmd/app/__pycache__/models.cpython-313.pyc
new file mode 100644
index 000000000..3f31abfe7
Binary files /dev/null and b/SLAB/Deadline_Detective_Backend_Webcmd/app/__pycache__/models.cpython-313.pyc differ
diff --git a/SLAB/Deadline_Detective_Backend_Webcmd/app/config.py b/SLAB/Deadline_Detective_Backend_Webcmd/app/config.py
new file mode 100644
index 000000000..ed088e40c
--- /dev/null
+++ b/SLAB/Deadline_Detective_Backend_Webcmd/app/config.py
@@ -0,0 +1,17 @@
+from pydantic_settings import BaseSettings
+from functools import lru_cache
+
+
+class Settings(BaseSettings):
+ gemini_api_key: str
+ webcmd_profile: str = "default"
+ max_pages: int = 5
+
+ class Config:
+ env_file = ".env"
+ env_file_encoding = "utf-8"
+
+
+@lru_cache()
+def get_settings() -> Settings:
+ return Settings()
diff --git a/SLAB/Deadline_Detective_Backend_Webcmd/app/models.py b/SLAB/Deadline_Detective_Backend_Webcmd/app/models.py
new file mode 100644
index 000000000..f7dfa77c6
--- /dev/null
+++ b/SLAB/Deadline_Detective_Backend_Webcmd/app/models.py
@@ -0,0 +1,40 @@
+from pydantic import BaseModel, Field, HttpUrl
+from typing import List, Optional
+from enum import Enum
+
+
+class StudentProfile(BaseModel):
+ year: Optional[str] = Field(None, example="1st Year")
+ branch: Optional[str] = Field(None, example="CSE / AI")
+ interests: Optional[str] = Field(None, example="hackathons, AI, web development")
+ location: Optional[str] = Field(None, example="India")
+
+
+class ResearchRequest(BaseModel):
+ task: str = Field(
+ ...,
+ min_length=10,
+ example="Find currently open opportunities for a first-year CSE/AI student. Check official pages, verify eligibility and deadline, and create a priority list of what I should apply for this week."
+ )
+ profile: Optional[StudentProfile] = None
+
+
+class Opportunity(BaseModel):
+ title: str
+ eligible: bool
+ deadline: Optional[str] = None
+ what_to_do: str
+ source: str
+ reason: Optional[str] = None
+
+
+class ResearchResponse(BaseModel):
+ opportunities: List[Opportunity]
+ next_3_actions: List[str]
+ summary: str
+ sources_checked: List[str] = []
+
+
+class HealthResponse(BaseModel):
+ status: str
+ message: str
diff --git a/SLAB/Deadline_Detective_Backend_Webcmd/app/routers/__init__.py b/SLAB/Deadline_Detective_Backend_Webcmd/app/routers/__init__.py
new file mode 100644
index 000000000..873f7bbbe
--- /dev/null
+++ b/SLAB/Deadline_Detective_Backend_Webcmd/app/routers/__init__.py
@@ -0,0 +1 @@
+# Routers package
diff --git a/SLAB/Deadline_Detective_Backend_Webcmd/app/routers/__pycache__/__init__.cpython-313.pyc b/SLAB/Deadline_Detective_Backend_Webcmd/app/routers/__pycache__/__init__.cpython-313.pyc
new file mode 100644
index 000000000..144311244
Binary files /dev/null and b/SLAB/Deadline_Detective_Backend_Webcmd/app/routers/__pycache__/__init__.cpython-313.pyc differ
diff --git a/SLAB/Deadline_Detective_Backend_Webcmd/app/routers/__pycache__/research.cpython-313.pyc b/SLAB/Deadline_Detective_Backend_Webcmd/app/routers/__pycache__/research.cpython-313.pyc
new file mode 100644
index 000000000..a9459978e
Binary files /dev/null and b/SLAB/Deadline_Detective_Backend_Webcmd/app/routers/__pycache__/research.cpython-313.pyc differ
diff --git a/SLAB/Deadline_Detective_Backend_Webcmd/app/routers/research.py b/SLAB/Deadline_Detective_Backend_Webcmd/app/routers/research.py
new file mode 100644
index 000000000..0f336d643
--- /dev/null
+++ b/SLAB/Deadline_Detective_Backend_Webcmd/app/routers/research.py
@@ -0,0 +1,27 @@
+from fastapi import APIRouter, HTTPException
+from app.models import ResearchRequest, ResearchResponse, HealthResponse
+from app.services.agent import DeadlineDetectiveAgent
+
+router = APIRouter(tags=["Research"])
+
+agent = DeadlineDetectiveAgent()
+
+
+@router.post("/research", response_model=ResearchResponse)
+async def run_research(request: ResearchRequest):
+ """
+ Main endpoint: Run the Deadline Detective browser agent.
+ """
+ if not request.task or len(request.task.strip()) < 10:
+ raise HTTPException(status_code=400, detail="Task must be at least 10 characters long.")
+
+ result = await agent.run(request)
+ return result
+
+
+@router.get("/health", response_model=HealthResponse)
+async def health_check():
+ return HealthResponse(
+ status="ok",
+ message="Deadline Detective backend is running"
+ )
diff --git a/SLAB/Deadline_Detective_Backend_Webcmd/app/services/__init__.py b/SLAB/Deadline_Detective_Backend_Webcmd/app/services/__init__.py
new file mode 100644
index 000000000..453a80dff
--- /dev/null
+++ b/SLAB/Deadline_Detective_Backend_Webcmd/app/services/__init__.py
@@ -0,0 +1 @@
+# Services package - Webcmd version
diff --git a/SLAB/Deadline_Detective_Backend_Webcmd/app/services/__pycache__/__init__.cpython-313.pyc b/SLAB/Deadline_Detective_Backend_Webcmd/app/services/__pycache__/__init__.cpython-313.pyc
new file mode 100644
index 000000000..ad91bb3b8
Binary files /dev/null and b/SLAB/Deadline_Detective_Backend_Webcmd/app/services/__pycache__/__init__.cpython-313.pyc differ
diff --git a/SLAB/Deadline_Detective_Backend_Webcmd/app/services/__pycache__/agent.cpython-313.pyc b/SLAB/Deadline_Detective_Backend_Webcmd/app/services/__pycache__/agent.cpython-313.pyc
new file mode 100644
index 000000000..66a7ec0bf
Binary files /dev/null and b/SLAB/Deadline_Detective_Backend_Webcmd/app/services/__pycache__/agent.cpython-313.pyc differ
diff --git a/SLAB/Deadline_Detective_Backend_Webcmd/app/services/__pycache__/gemini.cpython-313.pyc b/SLAB/Deadline_Detective_Backend_Webcmd/app/services/__pycache__/gemini.cpython-313.pyc
new file mode 100644
index 000000000..37be005e5
Binary files /dev/null and b/SLAB/Deadline_Detective_Backend_Webcmd/app/services/__pycache__/gemini.cpython-313.pyc differ
diff --git a/SLAB/Deadline_Detective_Backend_Webcmd/app/services/__pycache__/webcmd_service.cpython-313.pyc b/SLAB/Deadline_Detective_Backend_Webcmd/app/services/__pycache__/webcmd_service.cpython-313.pyc
new file mode 100644
index 000000000..12e1fbb39
Binary files /dev/null and b/SLAB/Deadline_Detective_Backend_Webcmd/app/services/__pycache__/webcmd_service.cpython-313.pyc differ
diff --git a/SLAB/Deadline_Detective_Backend_Webcmd/app/services/agent.py b/SLAB/Deadline_Detective_Backend_Webcmd/app/services/agent.py
new file mode 100644
index 000000000..6d3281038
--- /dev/null
+++ b/SLAB/Deadline_Detective_Backend_Webcmd/app/services/agent.py
@@ -0,0 +1,69 @@
+from app.services.gemini import GeminiService
+from app.services.webcmd_service import WebcmdService
+from app.models import ResearchRequest, ResearchResponse, Opportunity
+from typing import List
+import traceback
+
+
+class DeadlineDetectiveAgent:
+ def __init__(self):
+ self.gemini = GeminiService()
+ self.webcmd = WebcmdService()
+
+ async def run(self, request: ResearchRequest) -> ResearchResponse:
+ try:
+ # Step 1: Gemini creates the research plan
+ plan = self.gemini.create_research_plan(
+ task=request.task,
+ profile=request.profile
+ )
+
+ # Step 2: Webcmd collects evidence from real websites
+ evidence = await self.webcmd.search_and_collect(plan)
+
+ if not evidence:
+ return ResearchResponse(
+ opportunities=[],
+ next_3_actions=["No opportunities found. Try a more specific query or check if Webcmd is installed."],
+ summary="The agent could not extract useful information. Make sure Webcmd is installed and working (`webcmd doctor`).",
+ sources_checked=[]
+ )
+
+ # Step 3: Gemini analyzes eligibility and ranks
+ analysis = self.gemini.analyze_and_rank(
+ task=request.task,
+ profile=request.profile,
+ extracted_data=evidence
+ )
+
+ # Step 4: Build final structured response
+ opportunities: List[Opportunity] = []
+ for item in analysis.get("opportunities", []):
+ opportunities.append(
+ Opportunity(
+ title=item.get("title", "Unknown"),
+ eligible=bool(item.get("eligible", False)),
+ deadline=item.get("deadline"),
+ what_to_do=item.get("what_to_do", "Check the official page"),
+ source=item.get("source", ""),
+ reason=item.get("reason")
+ )
+ )
+
+ sources = [e["url"] for e in evidence if e.get("url")]
+
+ return ResearchResponse(
+ opportunities=opportunities,
+ next_3_actions=analysis.get("next_3_actions", []),
+ summary=analysis.get("summary", "Research completed using Webcmd browser agent."),
+ sources_checked=sources
+ )
+
+ except Exception as e:
+ traceback.print_exc()
+ return ResearchResponse(
+ opportunities=[],
+ next_3_actions=[],
+ summary=f"An error occurred while running the agent: {str(e)[:300]}",
+ sources_checked=[]
+ )
diff --git a/SLAB/Deadline_Detective_Backend_Webcmd/app/services/gemini.py b/SLAB/Deadline_Detective_Backend_Webcmd/app/services/gemini.py
new file mode 100644
index 000000000..927acf03d
--- /dev/null
+++ b/SLAB/Deadline_Detective_Backend_Webcmd/app/services/gemini.py
@@ -0,0 +1,181 @@
+from google import genai
+from google.genai import types
+from app.config import get_settings
+from app.models import StudentProfile, Opportunity
+from typing import List, Dict, Any
+import json
+import re
+import time
+
+
+class GeminiService:
+ def __init__(self):
+ settings = get_settings()
+ self.client = genai.Client(api_key=settings.gemini_api_key)
+ self.model = "gemini-3.6-flash"
+
+ def _extract_json(self, text: str) -> Any:
+ """Extract JSON from Gemini response (handles markdown code blocks)."""
+ text = text.strip()
+ # Try to find JSON inside ```json ... ```
+ match = re.search(r"```(?:json)?\s*([\s\S]*?)\s*```", text)
+ if match:
+ text = match.group(1).strip()
+ try:
+ return json.loads(text)
+ except json.JSONDecodeError:
+ # Try to find first { ... } or [ ... ]
+ start = text.find("{")
+ end = text.rfind("}") + 1
+ if start != -1 and end > start:
+ try:
+ return json.loads(text[start:end])
+ except json.JSONDecodeError:
+ pass
+ # Try array
+ start = text.find("[")
+ end = text.rfind("]") + 1
+ if start != -1 and end > start:
+ try:
+ return json.loads(text[start:end])
+ except json.JSONDecodeError:
+ pass
+ raise
+
+ def _generate_with_retry(self, prompt: str, max_tokens: int, temperature: float = 0.2, retries: int = 2) -> Any:
+ """Call Gemini with retry logic for transient failures and JSON parsing."""
+ last_error = None
+ for attempt in range(retries + 1):
+ try:
+ response = self.client.models.generate_content(
+ model=self.model,
+ contents=prompt,
+ config=types.GenerateContentConfig(
+ temperature=temperature,
+ max_output_tokens=max_tokens,
+ )
+ )
+ return self._extract_json(response.text)
+ except (json.JSONDecodeError, Exception) as e:
+ last_error = e
+ print(f"[Gemini] Attempt {attempt + 1} failed: {e}")
+ if attempt < retries:
+ time.sleep(1.5 * (attempt + 1))
+ # If all retries failed, try once more without response_mime_type
+ try:
+ print("[Gemini] Trying without response_mime_type constraint...")
+ response = self.client.models.generate_content(
+ model=self.model,
+ contents=prompt,
+ config=types.GenerateContentConfig(
+ temperature=temperature,
+ max_output_tokens=max_tokens,
+ )
+ )
+ return self._extract_json(response.text)
+ except Exception as e:
+ print(f"[Gemini] Final fallback also failed: {e}")
+ raise last_error or e
+
+ def create_research_plan(self, task: str, profile: StudentProfile | None) -> Dict[str, Any]:
+ profile_text = "No specific profile provided."
+ if profile:
+ profile_text = (
+ f"Year: {profile.year or 'Not specified'}\n"
+ f"Branch: {profile.branch or 'Not specified'}\n"
+ f"Interests: {profile.interests or 'Not specified'}\n"
+ f"Location: {profile.location or 'India'}"
+ )
+
+ prompt = f"""
+You are an expert research planner for college students in India.
+
+Student Profile:
+{profile_text}
+
+User Task:
+{task}
+
+Create a focused research plan to find currently open opportunities (hackathons, internships, scholarships, competitions, college events).
+
+Return ONLY valid JSON in this exact format:
+{{
+ "search_queries": ["query1", "query2", "query3"],
+ "target_sites": ["https://example.com", "..."],
+ "extraction_goals": ["title", "eligibility", "deadline", "application link"],
+ "priority": "focus on deadlines within next 14-21 days and first-year / beginner friendly opportunities"
+}}
+
+Rules:
+- Prefer official pages and well-known platforms (Unstop, Devfolio, Internshala, college sites, AICTE, etc.)
+- Maximum 5 search queries
+- Maximum 6 target sites
+- Make queries specific to the student profile
+"""
+
+ return self._generate_with_retry(prompt, max_tokens=4096, temperature=0.2)
+
+ def analyze_and_rank(
+ self,
+ task: str,
+ profile: StudentProfile | None,
+ extracted_data: List[Dict[str, Any]]
+ ) -> Dict[str, Any]:
+ profile_text = "No specific profile provided."
+ if profile:
+ profile_text = (
+ f"Year: {profile.year or 'Not specified'}\n"
+ f"Branch: {profile.branch or 'Not specified'}\n"
+ f"Interests: {profile.interests or 'Not specified'}"
+ )
+
+ data_text = json.dumps(extracted_data, indent=2, ensure_ascii=False)
+
+ prompt = f"""
+You are Deadline Detective – an expert AI that helps college students find real opportunities.
+
+Student Profile:
+{profile_text}
+
+Original Task:
+{task}
+
+Here is the raw data extracted from live websites:
+{data_text}
+
+Your job:
+1. Filter only relevant and currently open opportunities.
+2. Decide eligibility based on the student profile (be strict but fair).
+3. Extract clean deadline (prefer exact date).
+4. Create a clear "what_to_do" action.
+5. Rank by urgency + relevance.
+
+Return ONLY valid JSON in this exact format:
+{{
+ "opportunities": [
+ {{
+ "title": "Name of opportunity",
+ "eligible": true,
+ "deadline": "15 Sept 2025 or null",
+ "what_to_do": "Register on the official page / Apply before deadline",
+ "source": "https://full-url.com",
+ "reason": "Short reason for eligibility decision"
+ }}
+ ],
+ "next_3_actions": [
+ "1. Opportunity Name — deadline XX — do this",
+ "2. ...",
+ "3. ..."
+ ],
+ "summary": "2-3 sentence overall summary for the student"
+}}
+
+Rules:
+- Only include real opportunities that appear in the data.
+- If not eligible, still include it with eligible=false and clear reason.
+- Prefer opportunities with clear deadlines in the near future.
+- next_3_actions should only contain eligible items when possible.
+- Be honest. Do not invent deadlines or opportunities.
+"""
+
+ return self._generate_with_retry(prompt, max_tokens=8192, temperature=0.1)
diff --git a/SLAB/Deadline_Detective_Backend_Webcmd/app/services/webcmd_service.py b/SLAB/Deadline_Detective_Backend_Webcmd/app/services/webcmd_service.py
new file mode 100644
index 000000000..dd67a997b
--- /dev/null
+++ b/SLAB/Deadline_Detective_Backend_Webcmd/app/services/webcmd_service.py
@@ -0,0 +1,211 @@
+import asyncio
+import json
+import subprocess
+import shlex
+from typing import List, Dict, Any, Optional
+from app.config import get_settings
+
+
+class WebcmdService:
+ """
+ Service that controls the browser through Webcmd CLI.
+ Replaces the old Playwright service.
+ """
+
+ def __init__(self):
+ self.settings = get_settings()
+ self.profile = self.settings.webcmd_profile
+ self.session_id: Optional[str] = None
+
+ def _run_cmd(self, args: List[str], input_text: str = None, timeout: int = 60) -> Dict[str, Any]:
+ """Run a webcmd command and return parsed JSON when possible."""
+ webcmd_path = __import__('shutil').which('webcmd') or 'webcmd'
+ cmd = [webcmd_path, "--profile", self.profile] + args
+
+ try:
+ result = subprocess.run(
+ cmd,
+ input=input_text,
+ capture_output=True,
+ text=True,
+ timeout=timeout,
+ )
+
+ stdout = result.stdout.strip()
+ stderr = result.stderr.strip()
+
+ if result.returncode != 0:
+ return {
+ "success": False,
+ "error": stderr or stdout or "Webcmd command failed",
+ "raw": stdout
+ }
+
+ # Try to parse JSON
+ try:
+ data = json.loads(stdout)
+ return {"success": True, "data": data, "raw": stdout}
+ except json.JSONDecodeError:
+ return {"success": True, "data": stdout, "raw": stdout}
+
+ except subprocess.TimeoutExpired:
+ return {"success": False, "error": "Webcmd command timed out"}
+ except FileNotFoundError:
+ return {
+ "success": False,
+ "error": "webcmd command not found. Please install it: npm install -g @agentrhq/webcmd"
+ }
+ except Exception as e:
+ return {"success": False, "error": str(e)}
+
+ async def start_session(self, name: str = "DeadlineDetective") -> bool:
+ """Create a new Webcmd browser session."""
+ result = self._run_cmd(["session", "create", name, "-f", "json"])
+
+ if not result["success"]:
+ print(f"[Webcmd] Failed to create session: {result.get('error')}")
+ return False
+
+ data = result.get("data")
+ if isinstance(data, dict):
+ self.session_id = data.get("id") or data.get("session_id") or data.get("session")
+ elif isinstance(data, str):
+ # Sometimes the ID is printed as plain text
+ self.session_id = data.strip()
+
+ if not self.session_id:
+ # Fallback: try to extract from raw output
+ raw = result.get("raw", "")
+ for line in raw.splitlines():
+ if "id:" in line.lower():
+ self.session_id = line.split(":")[-1].strip()
+ break
+
+ print(f"[Webcmd] Session started: {self.session_id}")
+ return bool(self.session_id)
+
+ async def close_session(self):
+ """Close the current Webcmd session."""
+ if not self.session_id:
+ return
+
+ self._run_cmd(["session", "close", self.session_id])
+ print(f"[Webcmd] Session closed: {self.session_id}")
+ self.session_id = None
+
+ async def run_browser_js(self, js_code: str, timeout: int = 45) -> Dict[str, Any]:
+ """
+ Execute a Playwright-style JavaScript program inside the Webcmd session.
+ """
+ if not self.session_id:
+ return {"success": False, "error": "No active Webcmd session"}
+
+ args = [
+ "--session", self.session_id,
+ "browser", "run",
+ "--stdin",
+ "--no-snapshot-diff",
+ "--timeout", str(timeout),
+ "-f", "json"
+ ]
+
+ result = self._run_cmd(args, input_text=js_code, timeout=timeout + 10)
+ return result
+
+ async def visit_and_extract(self, url: str) -> Dict[str, Any]:
+ """Visit a URL and extract title + readable text."""
+ js_code = f"""
+await page.goto('{url}', {{ waitUntil: 'domcontentloaded', timeout: 30000 }});
+await page.waitForTimeout(1500);
+
+const title = await page.title();
+const text = await page.innerText('body');
+
+return {{
+ url: page.url(),
+ title: title,
+ text: text.slice(0, 10000)
+}};
+"""
+ result = await self.run_browser_js(js_code)
+
+ if not result["success"]:
+ return {
+ "url": url,
+ "title": "",
+ "text": "",
+ "success": False,
+ "error": result.get("error", "Failed to extract")
+ }
+
+ data = result.get("data")
+ if isinstance(data, dict):
+ # Webcmd sometimes wraps the return value
+ content = data.get("result") or data.get("data") or data
+ if isinstance(content, dict):
+ return {
+ "url": content.get("url", url),
+ "title": content.get("title", ""),
+ "text": content.get("text", ""),
+ "success": True,
+ "error": None
+ }
+
+ return {
+ "url": url,
+ "title": "",
+ "text": str(data)[:5000] if data else "",
+ "success": True,
+ "error": None
+ }
+
+ async def search_and_collect(self, plan: Dict[str, Any]) -> List[Dict[str, Any]]:
+ """
+ Execute the research plan using Webcmd.
+ Visits target sites + does simple searches.
+ """
+ if not await self.start_session("DeadlineDetective"):
+ return []
+
+ all_urls = set()
+
+ # Add target sites from plan
+ for site in plan.get("target_sites", [])[:4]:
+ if isinstance(site, str) and site.startswith("http"):
+ all_urls.add(site)
+
+ # Simple search via DuckDuckGo using browser
+ for query in plan.get("search_queries", [])[:2]:
+ try:
+ search_js = f"""
+await page.goto('https://duckduckgo.com/?q={query.replace(" ", "+")}', {{ waitUntil: 'domcontentloaded' }});
+await page.waitForTimeout(2000);
+
+const links = await page.$$eval('a[data-testid="result-title-a"], a.result__a', els =>
+ els.slice(0, 3).map(a => a.href).filter(h => h && h.startsWith('http'))
+);
+return {{ links }};
+"""
+ search_result = await self.run_browser_js(search_js)
+ if search_result["success"]:
+ data = search_result.get("data")
+ if isinstance(data, dict):
+ content = data.get("result") or data.get("data") or data
+ links = content.get("links", []) if isinstance(content, dict) else []
+ for link in links:
+ all_urls.add(link)
+ except Exception as e:
+ print(f"[Webcmd] Search failed for '{query}': {e}")
+
+ urls_to_visit = list(all_urls)[: self.settings.max_pages]
+ results = []
+
+ for url in urls_to_visit:
+ print(f"[Webcmd] Visiting: {url}")
+ page_data = await self.visit_and_extract(url)
+ if page_data.get("success") and len(page_data.get("text", "")) > 80:
+ results.append(page_data)
+ await asyncio.sleep(1)
+
+ await self.close_session()
+ return results
diff --git a/SLAB/Deadline_Detective_Backend_Webcmd/list_models.py b/SLAB/Deadline_Detective_Backend_Webcmd/list_models.py
new file mode 100644
index 000000000..ea6f9ecfa
--- /dev/null
+++ b/SLAB/Deadline_Detective_Backend_Webcmd/list_models.py
@@ -0,0 +1,12 @@
+import os
+from google import genai
+from dotenv import load_dotenv
+
+load_dotenv()
+api_key = os.environ.get("GEMINI_API_KEY")
+client = genai.Client(api_key=api_key)
+
+print("Available models:")
+for m in client.models.list():
+ if "gemini" in m.name:
+ print(m.name)
diff --git a/SLAB/Deadline_Detective_Backend_Webcmd/main.py b/SLAB/Deadline_Detective_Backend_Webcmd/main.py
new file mode 100644
index 000000000..54136e820
--- /dev/null
+++ b/SLAB/Deadline_Detective_Backend_Webcmd/main.py
@@ -0,0 +1,33 @@
+from pathlib import Path
+from fastapi import FastAPI
+from fastapi.middleware.cors import CORSMiddleware
+from fastapi.staticfiles import StaticFiles
+from fastapi.responses import FileResponse
+from app.routers import research
+
+app = FastAPI(
+ title="Deadline Detective",
+ description="AI Browser Agent that finds real college opportunities, verifies eligibility & deadlines, and creates action plans.",
+ version="1.0.0"
+)
+
+# Allow frontend (Vercel / local / Render static) to call the API
+app.add_middleware(
+ CORSMiddleware,
+ allow_origins=["*"], # For hackathon – tighten later
+ allow_credentials=True,
+ allow_methods=["*"],
+ allow_headers=["*"],
+)
+
+app.include_router(research.router)
+
+# Serve frontend static files
+FRONTEND_DIR = Path(__file__).resolve().parent.parent / "frontend"
+
+app.mount("/static", StaticFiles(directory=str(FRONTEND_DIR)), name="static")
+
+
+@app.get("/")
+async def root():
+ return FileResponse(str(FRONTEND_DIR / "index.html"))
diff --git a/SLAB/Deadline_Detective_Backend_Webcmd/requirements.txt b/SLAB/Deadline_Detective_Backend_Webcmd/requirements.txt
new file mode 100644
index 000000000..63be73c8a
--- /dev/null
+++ b/SLAB/Deadline_Detective_Backend_Webcmd/requirements.txt
@@ -0,0 +1,7 @@
+fastapi==0.115.0
+uvicorn[standard]==0.30.6
+google-genai==0.3.0
+pydantic==2.9.2
+pydantic-settings==2.5.2
+python-dotenv==1.0.1
+httpx==0.27.2
diff --git a/SLAB/frontend/index.html b/SLAB/frontend/index.html
new file mode 100644
index 000000000..f667dd5e5
--- /dev/null
+++ b/SLAB/frontend/index.html
@@ -0,0 +1,172 @@
+
+
+
+
+
+Deadline Detective — AI Browser Agent for College Opportunities
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Working the case…
+
+ - Planning research with Gemini…
+ - Opening browser session with Webcmd…
+ - Visiting websites…
+ - Analyzing eligibility & deadlines…
+
+
+
+
+
+
+ The trail went cold
+ Something interrupted the investigation. Please try again.
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/SLAB/frontend/script.js b/SLAB/frontend/script.js
new file mode 100644
index 000000000..7db7e8fea
--- /dev/null
+++ b/SLAB/frontend/script.js
@@ -0,0 +1,251 @@
+(() => {
+ const form = document.getElementById('research-form');
+ const submitBtn = document.getElementById('submit-btn');
+ const btnLabel = submitBtn.querySelector('.btn-label');
+
+ const intakePanel = document.getElementById('intake-panel');
+ const loadingPanel = document.getElementById('loading-panel');
+ const errorPanel = document.getElementById('error-panel');
+ const errorMessage = document.getElementById('error-message');
+ const resultsSection = document.getElementById('results-section');
+ const retryBtn = document.getElementById('retry-btn');
+
+ const progressItems = Array.from(document.querySelectorAll('#progress-list li'));
+
+ const summaryText = document.getElementById('summary-text');
+ const actionsList = document.getElementById('actions-list');
+ const resultsTbody = document.getElementById('results-tbody');
+ const sourcesList = document.getElementById('sources-list');
+
+ const API_ENDPOINT = '/research';
+
+ let progressTimer = null;
+
+ function showPanel(el) { el.hidden = false; }
+ function hidePanel(el) { el.hidden = true; }
+
+ function resetProgressUI() {
+ progressItems.forEach(li => li.classList.remove('active', 'done'));
+ }
+
+ // Step through the progress list while the request is in flight.
+ // If the response comes back before the animation finishes, we
+ // immediately mark everything done in advance() on completion.
+ function startProgressAnimation() {
+ resetProgressUI();
+ let index = 0;
+ const advance = () => {
+ progressItems.forEach((li, i) => {
+ li.classList.toggle('active', i === index);
+ li.classList.toggle('done', i < index);
+ });
+ if (index < progressItems.length - 1) {
+ index += 1;
+ progressTimer = setTimeout(advance, 1600);
+ }
+ };
+ advance();
+ }
+
+ function stopProgressAnimation() {
+ if (progressTimer) {
+ clearTimeout(progressTimer);
+ progressTimer = null;
+ }
+ progressItems.forEach(li => {
+ li.classList.add('done');
+ li.classList.remove('active');
+ });
+ }
+
+ function setSubmitting(isSubmitting) {
+ submitBtn.disabled = isSubmitting;
+ btnLabel.textContent = isSubmitting ? 'Investigating…' : 'Start Research';
+ }
+
+ function escapeHtml(str) {
+ return String(str ?? '')
+ .replace(/&/g, '&')
+ .replace(//g, '>')
+ .replace(/"/g, '"');
+ }
+
+ function isLikelyUrl(str) {
+ if (!str) return false;
+ try {
+ const u = new URL(str);
+ return u.protocol === 'http:' || u.protocol === 'https:';
+ } catch {
+ return false;
+ }
+ }
+
+ function renderEligibilityPill(value) {
+ const v = String(value ?? '').toLowerCase();
+ if (v === 'true' || v === 'yes' || v === 'eligible') {
+ return 'Eligible';
+ }
+ if (v === 'false' || v === 'no' || v === 'not eligible' || v === 'ineligible') {
+ return 'Not eligible';
+ }
+ return `${escapeHtml(value || 'Unclear')}`;
+ }
+
+ function isEligibleValue(value) {
+ const v = String(value ?? '').toLowerCase();
+ return v === 'true' || v === 'yes' || v === 'eligible';
+ }
+
+ function renderResults(data) {
+ // Summary
+ summaryText.textContent = data.summary || 'The investigation is complete. See the findings below.';
+
+ // Next actions
+ const actions = data.next_3_actions || data.next_actions || data.nextActions || data.actions || [];
+ actionsList.innerHTML = '';
+ if (actions.length === 0) {
+ const li = document.createElement('li');
+ li.innerHTML = 'No specific actions were identified. Review the opportunities table below.';
+ actionsList.appendChild(li);
+ } else {
+ actions.slice(0, 3).forEach(action => {
+ const li = document.createElement('li');
+ li.innerHTML = `${escapeHtml(action)}`;
+ actionsList.appendChild(li);
+ });
+ }
+
+ // Opportunities table
+ const opportunities = data.opportunities || data.results || [];
+ resultsTbody.innerHTML = '';
+ if (opportunities.length === 0) {
+ const tr = document.createElement('tr');
+ tr.innerHTML = 'No opportunities were found for this task. Try broadening the search. | ';
+ resultsTbody.appendChild(tr);
+ } else {
+ opportunities.forEach(op => {
+ const name = op.name || op.opportunity || op.title || 'Untitled opportunity';
+ const eligible = op.eligible ?? op.eligibility;
+ const deadline = op.deadline || 'Not specified';
+ const action = op.action || op.what_to_do || op.next_step || '—';
+ const source = op.source || op.source_url || op.url || '';
+
+ const tr = document.createElement('tr');
+ if (isEligibleValue(eligible)) tr.classList.add('eligible-row');
+
+ const sourceCell = isLikelyUrl(source)
+ ? `${escapeHtml(source)}`
+ : escapeHtml(source || 'Not provided');
+
+ tr.innerHTML = `
+ ${escapeHtml(name)} |
+ ${renderEligibilityPill(eligible)} |
+ ${escapeHtml(deadline)} |
+ ${escapeHtml(action)} |
+ ${sourceCell} |
+ `;
+ resultsTbody.appendChild(tr);
+ });
+ }
+
+ // Sources checked
+ const sources = data.sources || data.sources_checked || data.sourcesChecked || [];
+ sourcesList.innerHTML = '';
+ if (sources.length === 0) {
+ const li = document.createElement('li');
+ li.textContent = 'No source list was returned.';
+ sourcesList.appendChild(li);
+ } else {
+ sources.forEach(src => {
+ const li = document.createElement('li');
+ if (isLikelyUrl(src)) {
+ li.innerHTML = `${escapeHtml(src)}`;
+ } else {
+ li.textContent = src;
+ }
+ sourcesList.appendChild(li);
+ });
+ }
+ }
+
+ function showError(message) {
+ errorMessage.textContent = message || 'Something interrupted the investigation. Please try again.';
+ hidePanel(loadingPanel);
+ hidePanel(resultsSection);
+ showPanel(errorPanel);
+ showPanel(intakePanel);
+ }
+
+ async function runResearch(payload) {
+ stopProgressAnimation(); // clear any stale timers first
+ resetProgressUI();
+ hidePanel(errorPanel);
+ hidePanel(resultsSection);
+ hidePanel(intakePanel);
+ showPanel(loadingPanel);
+ setSubmitting(true);
+ startProgressAnimation();
+
+ try {
+ const response = await fetch(API_ENDPOINT, {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify(payload),
+ });
+
+ if (!response.ok) {
+ let detail = '';
+ try {
+ const errJson = await response.json();
+ detail = errJson.detail || errJson.message || '';
+ } catch {
+ /* ignore parse errors on error body */
+ }
+ throw new Error(detail || `The research service returned an error (${response.status}).`);
+ }
+
+ const data = await response.json();
+ stopProgressAnimation();
+
+ // brief pause so the final "done" state is visible before switching views
+ await new Promise(r => setTimeout(r, 350));
+
+ renderResults(data);
+ hidePanel(loadingPanel);
+ showPanel(resultsSection);
+ } catch (err) {
+ stopProgressAnimation();
+ showError(
+ err && err.message
+ ? err.message
+ : 'Could not reach the research service. Check your connection and try again.'
+ );
+ } finally {
+ setSubmitting(false);
+ }
+ }
+
+ form.addEventListener('submit', (e) => {
+ e.preventDefault();
+
+ const task = document.getElementById('task').value.trim();
+ if (!task) return;
+
+ const payload = {
+ task,
+ profile: {
+ year: document.getElementById('year').value || null,
+ branch: document.getElementById('branch').value.trim() || null,
+ interests: document.getElementById('interests').value.trim() || null,
+ },
+ };
+
+ runResearch(payload);
+ });
+
+ retryBtn.addEventListener('click', () => {
+ hidePanel(errorPanel);
+ showPanel(intakePanel);
+ });
+})();
diff --git a/SLAB/frontend/style.css b/SLAB/frontend/style.css
new file mode 100644
index 000000000..7c8d4a024
--- /dev/null
+++ b/SLAB/frontend/style.css
@@ -0,0 +1,497 @@
+/* -------------------------------------------------------
+ Deadline Detective — Design tokens
+ Color:
+ --ink #12172A (base background, deep ink navy)
+ --ink-soft #1B2140 (raised panels on dark)
+ --paper #F6F1E4 (case-file paper cream)
+ --paper-line #E4DBC4 (paper rules / borders)
+ --brass #C69A3C (accent — desk-lamp brass)
+ --brass-dark #9C7626 (accent, pressed/hover)
+ --ok #3F7D58 (eligible green, muted)
+ --no #A24B3F (not eligible, muted rust)
+ --ink-text #21241C (body text on paper)
+ Type:
+ Display / headings: 'Fraunces' (warm slab-serif, case-file stamp feel)
+ Body / UI: 'Inter'
+ Layout: single column, centered, max-width 880px, generous vertical rhythm
+------------------------------------------------------- */
+
+:root {
+ --ink: #12172A;
+ --ink-soft: #1B2140;
+ --ink-line: #2C335A;
+ --paper: #F6F1E4;
+ --paper-raised: #FBF8EF;
+ --paper-line: #E4DBC4;
+ --brass: #C69A3C;
+ --brass-dark: #9C7626;
+ --ok: #3F7D58;
+ --ok-bg: #E7F0E7;
+ --no: #A24B3F;
+ --no-bg: #F5E7E3;
+ --ink-text: #21241C;
+ --muted-text: #6B6455;
+ --radius-panel: 6px;
+ --shadow-panel: 0 18px 40px -20px rgba(0,0,0,0.55);
+ --font-display: 'Fraunces', Georgia, serif;
+ --font-body: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
+}
+
+* { box-sizing: border-box; }
+
+html { scroll-behavior: smooth; }
+
+body {
+ margin: 0;
+ background: var(--ink);
+ color: var(--paper);
+ font-family: var(--font-body);
+ line-height: 1.55;
+ -webkit-font-smoothing: antialiased;
+}
+
+.case-backdrop {
+ position: fixed;
+ inset: 0;
+ z-index: -1;
+ background:
+ radial-gradient(ellipse 900px 500px at 15% -10%, rgba(198,154,60,0.10), transparent 60%),
+ radial-gradient(ellipse 700px 500px at 100% 10%, rgba(198,154,60,0.06), transparent 55%),
+ var(--ink);
+}
+
+a { color: var(--brass); }
+
+:focus-visible {
+ outline: 2px solid var(--brass);
+ outline-offset: 3px;
+}
+
+/* ---------- Header ---------- */
+
+.site-header {
+ padding: 2.4rem 1.5rem 1.6rem;
+ border-bottom: 1px solid var(--ink-line);
+}
+
+.header-inner {
+ max-width: 880px;
+ margin: 0 auto;
+ display: flex;
+ align-items: flex-end;
+ justify-content: space-between;
+ gap: 1.5rem;
+ flex-wrap: wrap;
+}
+
+.brand {
+ display: flex;
+ align-items: center;
+ gap: 0.85rem;
+}
+
+.brand-mark {
+ color: var(--brass);
+ flex-shrink: 0;
+}
+
+.brand-text h1 {
+ font-family: var(--font-display);
+ font-weight: 600;
+ font-size: clamp(1.7rem, 3.4vw, 2.3rem);
+ margin: 0 0 0.15rem;
+ letter-spacing: -0.01em;
+ color: var(--paper);
+}
+
+.tagline {
+ margin: 0;
+ color: #B9BEDB;
+ font-size: 0.95rem;
+}
+
+.case-stamp {
+ font-family: var(--font-body);
+ font-size: 0.78rem;
+ color: var(--brass);
+ border: 1px solid var(--ink-line);
+ padding: 0.4rem 0.75rem;
+ border-radius: 999px;
+ white-space: nowrap;
+}
+
+/* ---------- Layout ---------- */
+
+main {
+ max-width: 880px;
+ margin: 0 auto;
+ padding: 2.5rem 1.5rem 4rem;
+ display: flex;
+ flex-direction: column;
+ gap: 2rem;
+}
+
+.panel {
+ background: var(--paper);
+ color: var(--ink-text);
+ border-radius: var(--radius-panel);
+ box-shadow: var(--shadow-panel);
+ padding: 2rem;
+ border: 1px solid var(--paper-line);
+}
+
+.panel-header { margin-bottom: 1.4rem; }
+
+.panel-index {
+ display: inline-block;
+ font-size: 0.72rem;
+ letter-spacing: 0.02em;
+ color: var(--brass-dark);
+ font-weight: 600;
+ margin-bottom: 0.5rem;
+}
+
+.panel-header h2 {
+ font-family: var(--font-display);
+ font-size: 1.5rem;
+ font-weight: 600;
+ margin: 0 0 0.4rem;
+ color: var(--ink-text);
+}
+
+.panel-sub {
+ margin: 0;
+ color: var(--muted-text);
+ max-width: 60ch;
+}
+
+/* ---------- Intake form ---------- */
+
+.field { margin-bottom: 1.3rem; }
+
+.field label {
+ display: block;
+ font-size: 0.85rem;
+ font-weight: 600;
+ margin-bottom: 0.45rem;
+ color: var(--ink-text);
+}
+
+textarea, input[type="text"], select {
+ width: 100%;
+ font-family: var(--font-body);
+ font-size: 0.96rem;
+ color: var(--ink-text);
+ background: var(--paper-raised);
+ border: 1px solid var(--paper-line);
+ border-radius: 4px;
+ padding: 0.75rem 0.9rem;
+ resize: vertical;
+ transition: border-color 0.15s ease;
+}
+
+textarea:focus, input[type="text"]:focus, select:focus {
+ border-color: var(--brass);
+}
+
+.field-grid {
+ display: grid;
+ grid-template-columns: repeat(3, 1fr);
+ gap: 1rem;
+ margin-bottom: 0.5rem;
+}
+
+.btn-primary {
+ margin-top: 0.6rem;
+ display: inline-flex;
+ align-items: center;
+ gap: 0.6rem;
+ background: var(--ink);
+ color: var(--paper);
+ border: none;
+ font-family: var(--font-body);
+ font-weight: 600;
+ font-size: 1rem;
+ padding: 0.85rem 1.6rem;
+ border-radius: 4px;
+ cursor: pointer;
+ transition: background 0.15s ease, transform 0.1s ease;
+}
+
+.btn-primary:hover:not(:disabled) { background: #23294A; }
+.btn-primary:active:not(:disabled) { transform: translateY(1px); }
+
+.btn-primary:disabled {
+ opacity: 0.55;
+ cursor: not-allowed;
+}
+
+.btn-icon { flex-shrink: 0; }
+
+.btn-secondary {
+ background: transparent;
+ color: var(--ink-text);
+ border: 1px solid var(--paper-line);
+ font-family: var(--font-body);
+ font-weight: 600;
+ font-size: 0.95rem;
+ padding: 0.7rem 1.4rem;
+ border-radius: 4px;
+ cursor: pointer;
+}
+
+.btn-secondary:hover { border-color: var(--brass); color: var(--brass-dark); }
+
+/* ---------- Loading ---------- */
+
+.loading-panel { text-align: center; }
+
+.loading-inner {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ padding: 1rem 0;
+}
+
+.magnifier-spinner {
+ color: var(--brass);
+ margin-bottom: 1.2rem;
+ animation: spin-sweep 1.8s ease-in-out infinite;
+}
+
+@keyframes spin-sweep {
+ 0% { transform: rotate(0deg); }
+ 50% { transform: rotate(25deg); }
+ 100% { transform: rotate(0deg); }
+}
+
+.loading-panel h2 {
+ font-family: var(--font-display);
+ font-size: 1.4rem;
+ margin: 0 0 1.4rem;
+}
+
+.progress-list {
+ list-style: none;
+ margin: 0;
+ padding: 0;
+ text-align: left;
+ display: inline-flex;
+ flex-direction: column;
+ gap: 0.75rem;
+ min-width: min(420px, 100%);
+}
+
+.progress-list li {
+ display: flex;
+ align-items: center;
+ gap: 0.7rem;
+ color: var(--muted-text);
+ font-size: 0.95rem;
+ transition: color 0.2s ease;
+}
+
+.step-marker {
+ width: 9px;
+ height: 9px;
+ border-radius: 50%;
+ border: 1.5px solid var(--paper-line);
+ flex-shrink: 0;
+ transition: background 0.2s ease, border-color 0.2s ease;
+}
+
+.progress-list li.active {
+ color: var(--ink-text);
+ font-weight: 600;
+}
+
+.progress-list li.active .step-marker {
+ background: var(--brass);
+ border-color: var(--brass);
+ box-shadow: 0 0 0 4px rgba(198,154,60,0.18);
+}
+
+.progress-list li.done .step-marker {
+ background: var(--ok);
+ border-color: var(--ok);
+}
+
+.progress-list li.done { color: var(--muted-text); }
+
+/* ---------- Error ---------- */
+
+.error-panel {
+ text-align: left;
+ border-left: 4px solid var(--no);
+}
+
+.error-panel h2 {
+ font-family: var(--font-display);
+ font-size: 1.4rem;
+ margin: 0 0 0.5rem;
+}
+
+.error-panel p { color: var(--muted-text); margin: 0 0 1.2rem; }
+
+/* ---------- Results ---------- */
+
+.results-wrap {
+ display: flex;
+ flex-direction: column;
+ gap: 1.6rem;
+}
+
+.summary-text {
+ margin: 0;
+ font-size: 1.02rem;
+ color: var(--ink-text);
+ max-width: 68ch;
+}
+
+/* Next 3 actions — most prominent card */
+.actions-panel {
+ background: var(--ink-soft);
+ color: var(--paper);
+ border: 1px solid var(--ink-line);
+ box-shadow: 0 22px 48px -18px rgba(0,0,0,0.65);
+}
+
+.actions-panel .panel-index { color: var(--brass); }
+.actions-panel .panel-header h2 { color: var(--paper); }
+
+.actions-list {
+ list-style: none;
+ counter-reset: action-counter;
+ margin: 0;
+ padding: 0;
+ display: flex;
+ flex-direction: column;
+ gap: 0.9rem;
+}
+
+.actions-list li {
+ counter-increment: action-counter;
+ display: flex;
+ gap: 1rem;
+ align-items: flex-start;
+ background: rgba(246,241,228,0.05);
+ border: 1px solid var(--ink-line);
+ border-radius: 5px;
+ padding: 0.95rem 1.1rem;
+}
+
+.actions-list li::before {
+ content: counter(action-counter);
+ font-family: var(--font-display);
+ font-weight: 600;
+ font-size: 1.1rem;
+ color: var(--ink);
+ background: var(--brass);
+ width: 1.9rem;
+ height: 1.9rem;
+ border-radius: 50%;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ flex-shrink: 0;
+}
+
+.actions-list .action-text { padding-top: 0.15rem; font-size: 0.98rem; }
+
+/* Table */
+
+.table-scroll { overflow-x: auto; }
+
+table {
+ width: 100%;
+ border-collapse: collapse;
+ font-size: 0.92rem;
+ min-width: 640px;
+}
+
+thead th {
+ text-align: left;
+ font-size: 0.75rem;
+ letter-spacing: 0.02em;
+ color: var(--muted-text);
+ font-weight: 600;
+ padding: 0.6rem 0.8rem;
+ border-bottom: 2px solid var(--paper-line);
+}
+
+tbody td {
+ padding: 0.85rem 0.8rem;
+ border-bottom: 1px solid var(--paper-line);
+ vertical-align: top;
+}
+
+tbody tr:last-child td { border-bottom: none; }
+
+tbody tr.eligible-row {
+ background: var(--ok-bg);
+}
+
+.eligible-pill, .not-eligible-pill, .unclear-pill {
+ display: inline-block;
+ font-size: 0.78rem;
+ font-weight: 600;
+ padding: 0.25rem 0.6rem;
+ border-radius: 999px;
+}
+
+.eligible-pill { background: var(--ok); color: #fff; }
+.not-eligible-pill { background: var(--no); color: #fff; }
+.unclear-pill { background: #D8CBA0; color: #5C4A1E; }
+
+td.opportunity-cell { font-weight: 600; }
+
+td a.source-link {
+ color: var(--brass-dark);
+ text-decoration: underline;
+ text-underline-offset: 2px;
+ word-break: break-word;
+}
+
+/* Sources */
+
+.sources-list {
+ list-style: none;
+ margin: 0;
+ padding: 0;
+ display: flex;
+ flex-direction: column;
+ gap: 0.55rem;
+}
+
+.sources-list li {
+ font-size: 0.9rem;
+ padding-bottom: 0.55rem;
+ border-bottom: 1px dashed var(--paper-line);
+}
+
+.sources-list li:last-child { border-bottom: none; padding-bottom: 0; }
+
+.sources-list a { word-break: break-word; }
+
+/* ---------- Footer ---------- */
+
+.site-footer {
+ text-align: center;
+ padding: 2rem 1.5rem 3rem;
+ color: #8890B8;
+ font-size: 0.85rem;
+}
+
+/* ---------- Responsive ---------- */
+
+@media (max-width: 640px) {
+ .field-grid { grid-template-columns: 1fr; }
+ .panel { padding: 1.4rem; }
+ .header-inner { align-items: flex-start; }
+ .case-stamp { order: 3; }
+}
+
+@media (prefers-reduced-motion: reduce) {
+ .magnifier-spinner { animation: none; }
+ html { scroll-behavior: auto; }
+}