# Project export: SlopShield

This document was generated by HackStack to give an AI agent context about a hackathon project. Sections are labeled with their provenance; content marked as truncated was cut to keep this document small.

## Project metadata

- Hackathon: OpenAI Build Week
- Tagline: Know when a YouTube voice is synthetic. SlopShield combines contextual transcript analysis with targeted audio spoof detection to flag AI-narrated YouTube videos.
- Devpost: https://devpost.com/software/slopshield
- GitHub: https://github.com/chknlittle/slopshield-plugin
- Demo: https://github.com/chknlittle/slopshield-api
- Video: https://www.youtube.com/embed/xijIZwKxm0g?enablejsapi=1&hl=en_US&rel=0&start=&version=3&wmode=transparent
- Team: 3 GitHub contributor(s) — Chicken Little (17 commits), Emil Zimmermann (2 commits), Jaunius (1 commits)

## Devpost submission (written by the team)

### Inspiration

The world cup motivated some of us to watch a lot of football related content before the big games. Especially documentary type videos or explainer videos of players relating their backstories and quirks. What surprised us was how many of the search results that come back on Youtube contain unbearably slopped out videos. That made us think that there must be a way to solve this problem using ChatGPT. That's how we came up with our idea for SlopShield.

### What it does

SlopShield is a Chrome (and Firefox) extension that analyses the transcripts and sound of the Youtube videos that are displayed in your viewport while browsing. It passes the ids down to the backend for analysis, through our engine, that then reports how confident it is that the video has been AI generated. The videos that are flagged are then removed from the Youtube viewport by the browser plugin. Nice and simple.

### How we built it

At first we wanted to train a model to do this but soon we realized that this can be solved for the most part by using very simple heuristics. For one, the transcripts already signal very heavily when a video has been generated by a botfarm. That's because the text will have been generated by, usually, a very old model that by now is easy for AI text detectors to flag. Beyond that, SlopShield also checks the sound characteristics of the video by running it through a synthetic voice detector and uses that information to influence the confidence score as well. These two methods combined resulted in a surprisingly effective filter that gets rid of most of the garbage that you would see in your feed.

### Challenges we ran into

The Youtube api is a little restrictive about just fetching an infinite amount of transcripts and eventually just flagged our backend's IP which made our system run into a slew of 429s. To get around that, we use the session of the user, because they are already logged in, to generate a OTP that the plugin passes to Youtube's backend as if the user click on the transcript button on the bottom of the video. Like this, the IP that is used to fetch the transcript is the same IP as the user watching and searching Youtube, which makes the network traffic behave the way Youtube intends it to. Another complication was dealing with our compute limitations. We have one 3090 that we use for the engine, so we set up a few queues to make the load of multiple users best distributed over that one GPU. In addition we set up a database to cache results so no video has to be processed more than once, which would be wasteful. Lastly, and probably the trickiest problem we ran into, is that the passing of the transcript to the backend via the plugin meant that there was a cache poisoning attack vector. To get around that, we corroborate the transcript that is sent against a transcript that we generate ourselves using whisper for a segment of that video.

### Accomplishments we're proud of

That we overcame the technical hurdles listed above is a large source of pride for sure. That we came managed to cook something of this complexity up within a week is also definitely a source of pride. Even if it was all ChatGPT's merit anyway. What we are also proud of is that this product has the potential to increase the viewing experience of other people. As well as combating botfarms that just manipulate people for views.

### What we learned

We learned a lot about how youtube's API and backend works. We learned a lot about how effective ChatGPT/Codex is at researching, testing and calibrating local models for our use-case. We also learned that an insane amount of videos are AI generated.

### What's next

There is a lot to expand on here. Many user interface improvements can be made. The engine can be made much faster. Which is something that would a very interesting technical challenge for us. We did not implement analysis on Youtube shorts yet, so that would be something that we would love to tackle given more time. Honestly this is a project with a really high polish ceiling that we could definitely make so good that you wouldn't want to use Youtube without it.

## README (from the GitHub repository)

# SlopShield browser extension

SlopShield hides videos that the SlopShield API has classified as AI-generated. It scans normal YouTube cards, reuses cached scores immediately, and fetches missing transcripts through the user's active YouTube browser session. Model inference stays in the backend.

This fork uses the real [`slopshield-api`](https://github.com/chknlittle/slopshield-api). The old mock scores and strictness slider have been removed. Filtering is simply on or off; switching it off reveals already-classified AI cards with provenance badges.

## Built with OpenAI Codex and GPT-5.6

We used OpenAI Codex with GPT-5.6 to build and review the browser extension.

Codex helped connect the extension to the real API.

It helped move YouTube caption retrieval into the user's browser session.

It helped design batching, caching, cooldowns, and fail-open behavior.

It helped harden trust boundaries and validate API messages.

It helped improve consent, privacy text, status badges, and result provenance.

It also helped test the Chrome and Firefox flows.

## Install in Chrome

1. Open `chrome://extensions`.
2. Enable **Developer mode**.
3. Click **Load unpacked** and select this repository.
4. Refresh any YouTube tabs that were already open.

## Install temporarily in Firefox

Firefox uses a background script rather than Chrome's Manifest V3 service worker, so it needs its own manifest. Install build dependencies and build the Firefox directory with Node.js 18 or newer:

```bash
npm install
npm run build:firefox
```

Then:

1. Open `about:debugging#/runtime/this-firefox`.
2. Click **Load Temporary Add-on**.
3. Select `dist/firefox/manifest.json`.
4. Refresh any YouTube tabs that were already open.

Firefox removes temporary add-ons when the browser closes. Everyday installation will require a Mozilla-signed XPI.

The popup reports API/engine health plus truthful current-page coverage: videos cleaned, AI videos hidden, checks still running, unavailable or failed checks, and lightweight scan timing.

## Behavior

The extension extracts each video's immutable `UC…` channel ID from YouTube renderer data (with no handle/name fallback), then sends discovered videos to `POST /v1/analyses` in batches of up to 50. Cached video or channel classifications are applied immediately. For a new channel, the API selects one evidence video; its transcript is fetched only after its card enters the viewport. Other videos from that channel wait and inherit the evidence verdict without fetching transcripts.

- `completed` with `is_ai: true`: hide the video card.
- `completed` with `is_ai: false`: leave it visible.
- `queued` or `running`: leave it visible and check again later.
- `missing` with `needs_transcript: true`: fetch captions in Firefox and submit them.
- `failed` or invalid: leave it visible.

For missing videos that enter the viewport, the page bridge runs YouTube's BotGuard challenge once, reuses its WebPO minter, mints a content-bound token per video, and fetches timestamped captions through the user's IP/session. Transcript jobs run one at a time with at least 1 second between starts. Transient YouTube/network failures trigger a shared exponential cooldown (30 seconds up to 5 minutes) so one rate limit cannot cause a retry storm. No hidden tabs or video navigation are used.

Results are cached by the backend. Thumbnail badges show `Checking…` while a result is pending and `✓ No AI detected` after either a direct non-AI result or a non-AI verdict inherited from the channel's evidence video. AI-classified cards are hidden while filtering is enabled. Turning filtering off reveals direct AI results as `AI detected` and inherited results as `AI channel`, making the disabled state useful as a preview. Failures remain visible with a `Check failed` badge. Videos without an immutable channel ID or usable transcript remain visible with an `Unavailable` badge.

YouTube Shorts and Shorts shelves are intentionally ignored.

## Source layout

```text
manifest.json            Chrome Manifest V3 configuration
manifest.firefox.json    Firefox Manifest V3 configuration
scripts/build-firefox.mjs  Creates the loadable Firefox directory
src/background.js        SlopShield API requests and health checks
src/content.js           Card discovery, two-phase queueing, and filtering
src/webpo-page-entry.js   Browser transcript/WebPO source
src/webpo-page.js         Generated page-context browser bundle
src/content.css          Hidden-card and API-offline styles
popup/                   On/off switch, health, and hidden count
```

## API configuration

The API URL is defined in `src/background.js`. Its origin must also appear under `host_permissions` in both browser manifests.

See [`docs/API_CONTRACT.md`](docs/API_CONTRACT.md) for the API fields used by the extension.

See [`PRIVACY.md`](PRIVACY.md) for the concise hackathon privacy disclosure mirrored in the popup.


## Detected evidence (automated analysis)

Indexed codebase: 13 recognized source files, 74 KB.
- CSS (language) — detected in the code
- HTML (language) — detected in the code
- JavaScript (language) — detected in the code
- Python (language) — claimed on Devpost, not found in the code

## Codebase structure (from repository index)

### Files (19 of 19)

```
.gitignore
docs/API_CONTRACT.md
manifest.firefox.json
manifest.json
package.json
popup/popup.css
popup/popup.html
popup/popup.js
PRIVACY.md
README.md
scripts/build-firefox.mjs
scripts/build-webpo.mjs
src/background.js
src/content-helpers.js
src/content.css
src/content.js
src/execute-interpreter.js
src/webpo-page-entry.js
src/webpo-page.js
```

### Dependencies

- package.json: bgutils-js@4.0.0, esbuild@0.25.6

### Recent commits (newest first)

- Merge pull request #2 from WELIO-Account/codex/document-codex-gpt56-usage
- Document Codex and GPT-5.6 usage
- Hide videos when analysis checks fail
- Prevent status badges from retriggering scans
- Merge pull request #1 from KalnuErelis/agent/align-extension-experience
- Condense mirrored verified statuses
- Remove temporary API domain from docs
- Remove tests
- Align extension experience and scan feedback
- Document channel confirmation flow
- Show unavailable and failed checks
- Resolve watch recommendation channel IDs
- Bridge YouTube channel IDs into content script
- Reuse channel classifications across videos
- Throttle viewport transcript checks and show status badges
- Remove obsolete extension bookkeeping
- Fetch missing transcripts in Firefox
- Add Firefox extension support
- Connect extension to SlopShield API
- Initial SlopShield plugin prototype

## Key source files (fetched from GitHub, selected and truncated for size)

### PRIVACY.md

```markdown
# SlopShield privacy summary

SlopShield reads YouTube video URLs and immutable channel IDs so it can check whether videos have already been classified. When a classification needs evidence, it also reads the video's available captions through the user's active YouTube session.

The extension sends those video URLs, channel IDs, and requested caption text to the SlopShield API solely to classify AI-generated videos and return the filtering result. It does not send the user's YouTube account credentials, cookies, watch history, or the contents of unrelated pages.

SlopShield stores the on/off preference in browser-synced extension storage. Classification results and evidence submitted to the SlopShield API may be cached so future checks do not repeat the same work.

```

### docs/API_CONTRACT.md

```markdown
# SlopShield API contract used by the extension

The extension talks directly to [`slopshield-api`](https://github.com/chknlittle/slopshield-api) in two phases.

## 1. Look up visible videos

```http
POST /v1/analyses
Content-Type: application/json

{
  "videos": [
    {
      "url": "https://www.youtube.com/watch?v=dQw4w9WgXcQ",
      "channel_id": "UCxxxxxxxxxxxxxxxxxxxxxx",
      "evidence_candidate": true
    }
  ]
}
```

Each response entry is one of:

- `completed`: apply the cached `is_ai` result immediately.
- `queued` or `running`: leave visible and poll later. This may mean the video is waiting for its channel's evidence video.
- `missing` with `needs_transcript: true`: fetch captions in the browser. The API returns this only for a channel's selected primary or confirmation evidence video.
- `failed`: leave visible and stop retrying automatically.

If the API has a stored transcript but no score for the active engine version, it queues analysis itself and returns `queued`; the browser does not fetch the transcript again.

## 2. Submit browser-fetched transcripts

For missing videos, Firefox obtains the transcript through the user's YouTube session and submits:

```http
POST /v1/analyses
Content-Type: application/json

{
  "videos": [
    {
      "url": "https://www.youtube.com/watch?v=dQw4w9WgXcQ",
      "channel_id": "UCxxxxxxxxxxxxxxxxxxxxxx",
      "evidence_candidate": false,
      "transcript": "[0.00 -> 3.20] Timestamped caption text"
    }
  ]
}
```

The API persists the transcript and returns `queued`. Subsequent polls omit the transcript. `evidence_candidate` is true only when a card is in the viewport; once the API has claimed an evidence video, its transcript submission is accepted regardless of that flag. A benign primary result verifies the channel immediately. A positive channel verdict requires positive results from two different evidence videos.

A response entry has this shape:

```json
{
  "input_url": "https://www.youtube.com/watch?v=dQw4w9WgXcQ",
  "video_id": "dQw4w9WgXcQ",
  "channel_id": "UCxxxxxxxxxxxxxxxxxxxxxx",
  "engine_version": "v1",
  "status": "completed",
  "cached": true,
  "needs_transcript": false,
  "is_ai": false,
  "classification_source": "channel",
  "evidence_video_id": "anotherVid1",
  "result": {},
  "error": null
}
```

Only a completed `is_ai: true` entry hides a card. `classification_source` distinguishes a direct `video` result from a `channel`-inherited result. Inferred results always identify the directly analyzed `evidence_video_id`. All failures remain visible.

## Health

```http
GET /health
```

The extension reads:

```json
{
  "ok": true,
  "engine": {
    "version": "v1",
    "reachable": true
  }
}
```

```

### package.json

```
{
  "name": "slopshield-plugin",
  "version": "0.5.0",
  "private": true,
  "type": "module",
  "scripts": {
    "build:webpo": "node scripts/build-webpo.mjs",
    "build:firefox": "node scripts/build-firefox.mjs",
    "test": "node --test"
  },
  "devDependencies": {
    "bgutils-js": "4.0.0",
    "esbuild": "0.25.6"
  }
}

```

### src/execute-interpreter.js

```javascript
export function executeInterpreter(document, interpreter) {
  const script = document.createElement("script");
  script.type = "text/javascript";

  const pageNonce = document.querySelector("script[nonce]")?.nonce;
  if (pageNonce) script.nonce = pageNonce;

  // Keep TrustedScript values intact. Coercing one through Function() turns it
  // back into a string, which YouTube rejects under Trusted Types enforcement.
  script.text = interpreter;

  const parent = document.head ?? document.documentElement;
  if (!parent) throw new Error("Cannot execute BotGuard before the document root exists");

  try {
    parent.appendChild(script);
  } finally {
    script.remove();
  }
}

```

### src/content-helpers.js

```javascript
(function exposeSlopShieldHelpers(root) {
  const VIDEO_ID = /^[A-Za-z0-9_-]{11}$/;

  function indexAnalysisResults(results) {
    const indexed = new Map();
    for (const result of Array.isArray(results) ? results : []) {
      if (VIDEO_ID.test(result?.videoId ?? "")) indexed.set(result.videoId, result);
    }
    return indexed;
  }

  function summarizeVideoStates({ videoIds, classifications, failures, hiddenVideoIds }) {
    const uniqueVideoIds = new Set(videoIds);
    let checkedCount = 0;
    let safeCount = 0;
    let aiCount = 0;
    let unavailableCount = 0;
    let failedCount = 0;

    for (const videoId of uniqueVideoIds) {
      const classification = classifications.get(videoId);
      if (classification) {
        checkedCount += 1;
        if (classification.isAi) aiCount += 1;
        else safeCount += 1;
        continue;
      }

      const failure = failures.get(videoId);
      if (failure === "unavailable") unavailableCount += 1;
      else if (failure === "check-failed") failedCount += 1;
    }

    return {
      scannedCount: uniqueVideoIds.size,
      checkedCount,
      cleanedCount: checkedCount,
      safeCount,
      aiCount,
      hiddenCount: new Set(hiddenVideoIds).size,
      checkingCount: Math.max(
        0,
        uniqueVideoIds.size - checkedCount - unavailableCount - failedCount,
      ),
      unavailableCount,
      failedCount,
    };
  }

  root.__slopShieldHelpers = Object.freeze({ indexAnalysisResults, summarizeVideoStates });
})(globalThis);

```

### popup/popup.html

```html
<!doctype html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>SlopShield</title>
    <link rel="stylesheet" href="popup.css" />
  </head>
  <body>
    <main class="shell">
      <header class="masthead">
        <div class="brand-lockup">
          <span class="brand-mark" aria-hidden="true">
            <i></i><i></i><i></i>
          </span>
          <div>
            <h1>SlopShield</h1>
            <p class="subtitle">/ YouTube signal filter</p>
          </div>
        </div>
        <label class="switch" aria-label="Enable SlopShield">
          <input id="enabled" type="checkbox" />
          <span class="switch-track"><span class="switch-knob"></span></span>
        </label>
      </header>

      <section class="status-panel" aria-live="polite">
        <div class="status-row">
          <div class="status-copy">
            <span id="statusDot" class="status-dot"></span>
            <span id="statusText">Checking engine…</span>
          </div>
          <span id="latency" class="latency">—</span>
        </div>
        <div class="signal-track" aria-hidden="true">
          <i></i><i></i><i></i><i></i><i></i><i></i><i></i><i></i><i></i><i></i><i></i><i></i>
        </div>
      </section>

      <section class="counter-panel">
        <div>
          <p class="counter-kicker">Page result / live</p>
          <p class="counter-label">Videos cleaned</p>
          <p id="coverageText" class="coverage-text">Waiting for YouTube…</p>
        </div>
        <p id="cleanedCount" class="counter">00</p>
      </section>

      <section class="stats-grid" aria-label="Page protection summary">
        <div><strong id="blockedCount">00</strong><span>AI hidden</span></div>
        <div><strong id="checkingCount">00</strong><span>Checking</span></div>
        <div><strong id="issueCount">00</strong><span>Issues</span></div>
      </section>

      <footer>
        <p class="privacy-note">Video metadata and captions are analyzed by the SlopShield API.</p>
        <div class="footer-meta">
          <span id="scanTiming">Text signal × voice signal</span>
          <span class="version">v0.5</span>
        </div>
      </footer>
    </main>
    <script src="popup.js"></script>
  </body>
</html>

```

### src/background.js

```javascript
const extensionApi = globalThis.browser ?? globalThis.chrome;
const API_BASE_URL = "https://slopshield-api.chkn.computer";
const DEFAULT_SETTINGS = Object.freeze({ enabled: true });

extensionApi.runtime.onInstalled.addListener(async () => {
  const current = await extensionApi.storage.sync.get(DEFAULT_SETTINGS);
  await extensionApi.storage.sync.set(current);
});

extensionApi.runtime.onMessage.addListener((message, _sender, sendResponse) => {
  if (message?.type === "ANALYZE_VIDEOS") {
    analyzeVideos(message.videos)
      .then(sendResponse)
      .catch((error) => sendResponse({ ok: false, error: error.message }));
    return true;
  }

  if (message?.type === "HEALTH_CHECK") {
    checkHealth()
      .then(sendResponse)
      .catch((error) => sendResponse({ ok: false, error: error.message }));
    return true;
  }

  return false;
});

async function analyzeVideos(videos) {
  if (!Array.isArray(videos) || videos.length === 0) return { ok: true, results: [] };

  const settings = await extensionApi.storage.sync.get(DEFAULT_SETTINGS);
  if (!settings.enabled) return { ok: true, results: [] };

  const batch = videos.slice(0, 100);
  const response = await fetch(`${API_BASE_URL}/v1/analyses`, {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({
      videos: batch.map((video) => ({
        url: video.url,
        channel_id: video.channelId,
        evidence_candidate: video.evidenceCandidate === true,
        ...(typeof video.transcript === "string" ? { transcript: video.transcript } : {}),
      })),
    }),
  });

  if (!response.ok) throw new Error(`SlopShield API returned HTTP ${response.status}`);

  const payload = await response.json();
  if (!Array.isArray(payload.analyses)) {
    throw new Error("SlopShield API response is missing analyses");
  }

  return {
    ok: true,
    results: payload.analyses.map((analysis) => ({
      videoId: analysis.video_id,
      status: analysis.status,
      needsTranscript: analysis.needs_transcript === true,
      isAi: analysis.is_ai,
      classificationSource: analysis.classification_source,
    })),
  };
}

async function checkHealth() {
  const startedAt = performance.now();
  const response = await fetch(`${API_BASE_URL}/health`);

  if (!response.ok) throw new Error(`SlopShield API returned HTTP ${response.status}`);

  const health = await response.json();
  return {
    ok: health.ok === true,
    engineReachable: health.engine?.reachable === true,
    engineVersion: health.engine?.version ?? "unknown",
    latencyMs: Math.round(performance.now() - startedAt),
  };
}

```

### src/content.css

```css
.slopshield-hidden { display: none !important; }
.slopshield-thumbnail { position: relative !important; }

.slopshield-status-badge {
  position: absolute;
  z-index: 20;
  top: 8px;
  left: 8px;
  display: inline-flex;
  align-items: center;
  min-height: 23px;
  padding: 4px 8px;
  border: 1px solid rgba(88, 230, 220, 0.55);
  border-radius: 0;
  color: #eef6f3;
  background: rgba(8, 12, 13, 0.94);
  box-shadow: 0 6px 20px rgba(0, 0, 0, 0.34);
  font: 500 10px/1.2 "SFMono-Regular", "Cascadia Code", Menlo, monospace;
  letter-spacing: 0.07em;
  text-transform: uppercase;
  pointer-events: none;
  white-space: nowrap;
  backdrop-filter: blur(5px);
}

.slopshield-status-badge::before {
  content: "";
  width: 5px;
  height: 5px;
  margin-right: 6px;
  border-radius: 50%;
  background: #58e6dc;
  box-shadow: 0 0 8px rgba(88, 230, 220, 0.65);
}

.slopshield-status-badge[data-status="verified"],
.slopshield-status-badge[data-status="verified-channel"] {
  border-color: #b7f34a;
  color: #071000;
  background: #b7f34a;
  box-shadow: 0 7px 22px rgba(183, 243, 74, 0.22), 0 0 0 1px rgba(8, 12, 13, 0.28) inset;
  font-weight: 700;
}
.slopshield-status-badge[data-status="verified"]::before,
.slopshield-status-badge[data-status="verified-channel"]::before {
  width: 6px;
  height: 6px;
  background: #071000;
  box-shadow: none;
}

.slopshield-status-badge[data-status="processing"] {
  border-color: rgba(88, 230, 220, 0.58);
  color: #c8fffb;
}

.slopshield-status-badge[data-status="would-hide"],
.slopshield-status-badge[data-status="would-hide-channel"],
.slopshield-status-badge[data-status="check-failed"] {
  border-color: rgba(255, 114, 98, 0.72);
  color: #ffc1ba;
  background: rgba(38, 17, 16, 0.96);
}
.slopshield-status-badge[data-status="would-hide"]::before,
.slopshield-status-badge[data-status="would-hide-channel"]::before,
.slopshield-status-badge[data-status="check-failed"]::before {
  background: #ff7262;
  box-shadow: 0 0 8px rgba(255, 114, 98, 0.56);
}

.slopshield-status-badge[data-status="unavailable"] {
  border-color: rgba(145, 160, 158, 0.48);
  color: #aab7b5;
}
.slopshield-status-badge[data-status="unavailable"]::before { background: #586765; box-shadow: none; }

.slopshield-page-notice {
  position: fixed;
  z-index: 2147483647;
  left: 16px;
  bottom: 16px;
  max-width: 360px;
  padding: 11px 13px;
  border: 1px solid rgba(255, 114, 98, 0.62);
  border-left: 2px solid #ff7262;
  border-radius: 0;
  color: #eef6f3;
  background: rgba(8, 12, 13, 0.96);
  box-shadow: 0 10px 32px rgba(0, 0, 0, 0.36);
  font: 500 11px/1.4 "SFMono-Regular", "Cascadia Code", Menlo, monospace;
  letter-spacing: 0.03em;
  text-transform: uppercase;
}

```

### popup/popup.js

```javascript
const extensionApi = globalThis.browser ?? globalThis.chrome;
const DEFAULT_SETTINGS = { enabled: true };

const enabledInput = document.querySelector("#enabled");
const cleanedCount = document.querySelector("#cleanedCount");
const blockedCount = document.querySelector("#blockedCount");
const checkingCount = document.querySelector("#checkingCount");
const issueCount = document.querySelector("#issueCount");
const coverageText = document.querySelector("#coverageText");
const scanTiming = document.querySelector("#scanTiming");
const statusDot = document.querySelector("#statusDot");
const statusText = document.querySelector("#statusText");
const latency = document.querySelector("#latency");

void initialize();

async function initialize() {
  const settings = await extensionApi.storage.sync.get(DEFAULT_SETTINGS);
  enabledInput.checked = settings.enabled;
  enabledInput.addEventListener("change", async () => {
    await extensionApi.storage.sync.set({ enabled: enabledInput.checked });
  });

  await Promise.all([refreshHealth(), refreshPageStats()]);
}

async function refreshHealth() {
  try {
    const health = await extensionApi.runtime.sendMessage({ type: "HEALTH_CHECK" });
    if (!health?.ok) throw new Error(health?.error || "Unavailable");

    statusDot.className = health.engineReachable ? "status-dot online" : "status-dot offline";
    statusText.textContent = health.engineReachable
      ? `Engine ${health.engineVersion} online`
      : "API online · engine unavailable";
    latency.textContent = `${health.latencyMs}ms`;
  } catch {
    statusDot.className = "status-dot offline";
    statusText.textContent = "SlopShield API offline";
    latency.textContent = "offline";
  }
}

async function refreshPageStats() {
  try {
    const [activeTab] = await extensionApi.tabs.query({ active: true, currentWindow: true });
    if (!activeTab?.id) throw new Error("No active tab");

    const stats = await extensionApi.tabs.sendMessage(activeTab.id, { type: "GET_PAGE_STATS" });
    renderStats(stats);
  } catch {
    renderStats(null);
  }
}

function renderStats(stats) {
  const format = (value) => String(Math.max(0, Number(value) || 0)).padStart(2, "0");
  cleanedCount.textContent = format(stats?.cleanedCount);
  blockedCount.textContent = format(stats?.hiddenCount);
  checkingCount.textContent = format(stats?.checkingCount);
  issueCount.textContent = format((stats?.failedCount ?? 0) + (stats?.unavailableCount ?? 0));
  coverageText.textContent = stats
    ? `${stats.checkedCount} of ${stats.scannedCount} checked`
    : "Open a YouTube feed to scan";
  scanTiming.textContent = stats?.scanCount
    ? `Scan ${stats.lastScanMs}ms · peak ${stats.maxScanMs}ms`
    : "Text signal × voice signal";
}

```

### popup/popup.css

```css
:root {
  color-scheme: dark;
  --ink: #eef6f3;
  --muted: #91a09e;
  --faint: #586765;
  --background: #080c0d;
  --surface: #0d1314;
  --surface-raised: #11191a;
  --line: #263232;
  --cyan: #58e6dc;
  --lime: #b7f34a;
  --danger: #ff7262;
  --mono: "SFMono-Regular", "Cascadia Code", "Liberation Mono", Menlo, monospace;
  --sans: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
}

* { box-sizing: border-box; }

body {
  width: 360px;
  margin: 0;
  color: var(--ink);
  background:
    linear-gradient(rgba(88, 230, 220, 0.035) 1px, transparent 1px),
    linear-gradient(90deg, rgba(88, 230, 220, 0.035) 1px, transparent 1px),
    var(--background);
  background-size: 32px 32px;
  font: 14px/1.4 var(--sans);
  -webkit-font-smoothing: antialiased;
}

.shell { padding: 18px; }
.masthead, .brand-lockup, .status-row, .counter-panel, .footer-meta { display: flex; align-items: center; justify-content: space-between; }
.masthead { min-height: 44px; margin-bottom: 18px; }
.brand-lockup { justify-content: flex-start; gap: 11px; }
.brand-mark {
  width: 36px;
  height: 36px;
  padding: 8px;
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  align-items: end;
  gap: 3px;
  border: 1px solid var(--cyan);
  background: rgba(13, 19, 20, 0.9);
}
.brand-mark i { display: block; background: var(--cyan); }
.brand-mark i:nth-child(1) { height: 7px; opacity: 0.55; }
.brand-mark i:nth-child(2) { height: 18px; }
.brand-mark i:nth-child(3) { height: 12px; opacity: 0.8; }
h1, p { margin: 0; }
h1 { font-size: 17px; font-weight: 680; line-height: 1.1; letter-spacing: -0.025em; }
.subtitle { margin-top: 4px; color: var(--muted); font: 9px/1 var(--mono); letter-spacing: 0.08em; text-transform: uppercase; }

.switch { position: relative; cursor: pointer; }
.switch input { position: absolute; width: 1px; height: 1px; opacity: 0; }
.switch-track {
  display: block;
  width: 42px;
  height: 24px;
  padding: 3px;
  border: 1px solid #3b4746;
  background: #192222;
  transition: border-color 150ms ease, background 150ms ease;
}
.switch-knob {
  display: block;
  width: 16px;
  height: 16px;
  background: var(--faint);
  transition: transform 150ms ease, background 150ms ease;
}
.switch input:checked + .switch-track { border-color: var(--cyan); background: rgba(88, 230, 220, 0.12); }
.switch input:checked + .switch-track .switch-knob { transform: translateX(18px); background: var(--cyan); box-shadow: 0 0 12px rgba(88, 230, 220, 0.3); }
.switch input:focus-visible + .switch-track { outline: 2px solid var(--lime); outline-offset: 3px; }

.status-panel { padding: 12px 13px 11px; border: 1px solid var(--line); background: rgba(13, 19, 20, 0.94); }
.status-row { min-height: 18px; }
.status-copy { display: flex; align-items: center; gap: 8px; min-width: 0; }
.status-copy span:last-child { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font: 10px/1.2 var(--mono); letter-spacing: 0.03em; text-transform: uppercase; }
.status-dot { flex: 0 0 auto; width: 6px; height: 6px; border-radius: 50%; background: var(--faint); }
.status-dot.online { background: var(--lime); box-shadow: 0 0 9px rgba(183, 243, 74, 0.55); }
.status-dot.offline { background: var(--danger); box-shadow: 0 0 9px rgba(255, 114, 98, 0.45); }
.latency { margin-left: 12px; color: var(--muted); font: 9px/1 var(--mono); }
.signal-track { height: 30px; margin-top: 10px; display: flex; align-items: center; gap: 5px; border-top: 1px dashed var(--line); border-bottom: 1px dashed var(--line); overflow: hidden; }
.signal-track i { flex: 1; min-width: 2px; background: var(--cyan); opacity: 0.55; }
.signal-track i:nth-child(1) { height: 20%; }
.signal-track i:nth-child(2) { height: 55%; }
.signal-track i:nth-child(3) { height: 85%; }
.signal-track i:nth-child(4) { height: 35%; }
.signal-track i:nth-child(5) { height: 68%; }
.signal-track i:nth-child(6) { height: 95%; }
.signal-track i:nth-child(7) { height: 45%; }
.signal-track i:nth-child(8) { height: 76%; }
.signal-track i:nth-child(9) { height: 30%; }
.signal-track i:nth-child(10) { height: 62%; }
.signal-track i:nth-child(11) { height: 88%; }
.signal-track i:nth-child(12) { height: 42%; }

.counter-panel { min-height: 100px; margin-top: 10px; padding: 16px 14px; border-left: 2px solid var(--lime); background: linear-gradient(110deg, rgba(183, 243, 74, 0.08), transparent 55%), var(--surface-raised); }
.counter-kicker { margin-bottom: 7px; color: var(--cyan); font: 8px/1 var(--mono); letter-spacing: 0.12em; text-transform: uppercase; }
.counter-label { color: var(--ink); font: 11px/1.25 var(--mono); text-transform: uppercase; }
.coverage-text { margin-top: 5px; color: var(--muted); font: 9px/1.2 var(--mono); }
.counter { color: var(--lime); text-shadow: 0 0 24px rgba(183, 243, 74, 0.18); font: 52px/0.9 var(--mono); font-weight: 500; letter-spacing: -0.08em; }
.stats-grid { display: grid; grid-template-columns: repeat(3, 1fr); margin-top: 1px; border: 1px solid var(--line); background: var(--surface); }
.stats-grid div { min-width: 0; padding: 10px 11px; border-right: 1px solid var(--line); }
.stats-grid div:last-child { border-right: 0; }
.stats-grid strong { display: block; color: var(--ink); font: 16px/1 var(--mono); font-weight: 500; }
.stats-grid span { display: block; margin-top: 5px; overflow: hidden; color: var(--muted); font: 7px/1 var(--mono); letter-spacing: 0.08em; text-overflow: ellipsis; text-transform: uppercase; white-space: nowrap; }
footer { margin-top: 13px; padding-top: 11px; border-top: 1px solid var(--line); color: var(--faint); font: 8px/1.35 var(--mono); letter-spacing: 0.07em; text-transform: uppercase; }
.privacy-note { max-width: 285px; color: var(--muted); text-transform: none; letter-spacing: 0; }
.footer-meta { margin-top: 8px; }
.version { color: var(--muted); }

@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after { transition-duration: 0.01ms !important; }
}

```

[3 more indexed source files omitted to keep this export small. The full file list is in the Codebase structure section above.]