From 4bdb7bfc510d4a848a08fbc7248e8bd4a370db91 Mon Sep 17 00:00:00 2001
From: ares <285551516+New1Direction@users.noreply.github.com>
Date: Sun, 14 Jun 2026 20:21:20 -0700
Subject: [PATCH] =?UTF-8?q?feat(site):=20"The=20Case=20File"=20redesign=20?=
=?UTF-8?q?=E2=80=94=20drop=20the=20AI-terminal=20look?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Re-skin the marketing site around a detective/case-file identity that
ties the lens-detective mascot, the magnifying-glass name, and the
investigate→verdict product framing together — and away from the generic
dark + mono + gradient-text "AI terminal" template.
Design
- Warm manila/ink/amber palette, light-default; warm "night stakeout"
dark. Removed the multi-palette (data-palette) machinery entirely —
one intentional pair of themes instead of swatch soup.
- Fraunces serif for display type (was Space Grotesk); real pairing
against Inter/JetBrains Mono.
- Verdict fit-call rendered as a rotated rubber stamp; killed gradient
text (.grad-text, health number) — both AI tells — for solid accent.
- Detective-framed section eyebrows: the verdict / the case file / the
procedure / the detective. Mascot expressions relabelled to case
beats (On the case, Reading, Deep dive, Cleared, Suspicious, Off duty).
- All token references (accent/warm weak+line, shadows) defined in both
:root and .dark — verified no transparent surfaces.
Docs & housekeeping
- README: v3.0.0 + 730+ tests badges, detective tagline, the v2/v3
surfaces (Triage, Evaluate/compare, Discover) and a v3-forward
"What's new" replacing the stale v1.x wall.
- Move audit report to docs/audits/; delete dead PaletteSwatches; ignore
Playwright MCP scratch output.
Build + lint green; both themes pass WCAG AA on body text.
---
.gitignore | 4 +
README.md | 11 +-
docs/audits/audit-v3.0.0.html | 412 ++++++++++++++++++++
website/app/(home)/styles/home.css | 33 +-
website/app/(home)/styles/shell.css | 6 +-
website/app/global.css | 248 ++++++------
website/app/layout.tsx | 31 +-
website/components/home/FeatureBento.tsx | 2 +-
website/components/home/HowItWorks.tsx | 2 +-
website/components/home/ThemeShowcase.tsx | 28 +-
website/components/home/VerdictDemo.tsx | 2 +-
website/components/site/PaletteSwatches.tsx | 65 ---
12 files changed, 586 insertions(+), 258 deletions(-)
create mode 100644 docs/audits/audit-v3.0.0.html
delete mode 100644 website/components/site/PaletteSwatches.tsx
diff --git a/.gitignore b/.gitignore
index 1461fed..aeb294f 100644
--- a/.gitignore
+++ b/.gitignore
@@ -7,3 +7,7 @@ coverage/
# superpowers skill state
.superpowers/
+
+# playwright/chrome-devtools MCP scratch output
+.playwright-mcp/
+*-screenshot.png
diff --git a/README.md b/README.md
index fd76479..ce31730 100644
--- a/README.md
+++ b/README.md
@@ -2,15 +2,15 @@
# 🔭 RepoLens
-### One click turns any repo into a plain-English briefing.
+### One click opens the case file on any repo.
-**What it is · whether it's a good fit · how it's actually built · what it connects to.**
+**The verdict · the evidence · the red flags · how it's actually built — in plain English, before the README's pitch.**



-
-
+
+

@@ -32,6 +32,9 @@ A scan opens to a **verdict landing** and fans out into focused tabs:
| ⚖️ | **Verdict** | Fit call (strong / solid / care / risky), a one-line bottom line, measured facts, and the top things worth noting — first thing you see. |
| 🧠 | **Deep Dive** | The core concepts → how they build on each other → a plain-English ("explain it like I'm five") walkthrough. Optionally grounded by **measured facts** from the local runner. |
| 📚 | **Library** | Every repo you've analyzed, as a sortable / filterable triage grid with fit chips, a stats bar, **bulk multi-select delete**, and one-click **Export / Import / Backup**. |
+| 🗂️ | **Triage & decide** | Keyboard-first **Adopt / Trial / Hold / Reject**, a Tech Radar, Boards, fit-delta tracking, notes, and daily **drift alerts** when repos go stale. |
+| ★ | **Evaluate & compare** | Score repos **1–5** against your own rubric, grade docs **A–F**, and put any **2–10** side-by-side in a decision matrix (CSV / Markdown export). |
+| 🔍 | **Discover** | Search GitHub from inside the extension, or get **recommendations** from the repos you've already adopted. |
| 🕸️ | **Connections** | A walkable map centred on the current repo, showing how it relates to the others you've scanned. |
| 🤝 | **Synergies** · **Versus** · **Combinator** | Complements, head-to-heads, and fused project ideas — grounded in *your* library. |
diff --git a/docs/audits/audit-v3.0.0.html b/docs/audits/audit-v3.0.0.html
new file mode 100644
index 0000000..3787ff6
--- /dev/null
+++ b/docs/audits/audit-v3.0.0.html
@@ -0,0 +1,412 @@
+
+
+
A full audit of the v3.0.0 extension and the redesigned website across security, code & architecture, tests, and accessibility. The short version: zero critical issues, the security fundamentals that matter most are verified clean, and the work to do is concentrated and mostly known — starting with a one-line fix that turns CI green.
+
+ 2026-06-15
+ 4 parallel specialist passes
+ ~18.8k LoC extension · Next.js site
+ adversarially verified
+
+
+
+
+
Bottom line
+
0 CRITICAL across all four dimensions. Security is genuinely sound for a key-handling extension — keys are never logged, the settings export is provably allowlist-driven, PKCE + OAuth state are correct, there's no eval, and the content script is minimal. Test coverage is high on pure logic (57 of ~75 modules), accessibility intent is above average, and the website's motion/perf hygiene is exemplary. The issues cluster in five places: a red CI from a known flaky test (trivial fix), maintainability debt in three oversized files, accessibility polish (focus lifecycle + a few low-contrast tokens), attack-surface hardening (endpoint validation), and basic SEO (sitemap / OG image).
+
+
+
+
Security
2H5M4L
+
Code & Architecture
5H5M3L
+
Tests & Coverage
3H3M1L
+
Accessibility
5H6M3L
+
Website
3H3M6L
+
+
+
+
+
00
The one to fix first
+
A single thread connects three of the four passes — and it's a one-line fix.
+
+
HIGH
CI is red on main — caused by a time-flaky test, not a missing script
tests/maintenance.test.js · maintenance.js:15,25
+
CI run #19 failed while #18 (24 min earlier) passed — exactly when daysSincePush(daysAgo(30)) tipped from 31→32 as the UTC date rolled to the 15th. The website pass flagged this as "no npm test script," but that was verified wrong: ci.yml runs at the repo root where npm test = vitest run exists. The real cause: bandFromSignals and daysSincePush call real Date.now() with no injection point, while the test pins NOW=2026-06-13 to build inputs — so measured age drifts +1 day/day. A latent sibling exists in diff-analysis.test.js (loose assertions hide it for now), and the band-boundary tests will break later (≈Aug 12 and Jan 13).
+
Fix: give bandFromSignals/daysSincePush an injectable today = Date.now() param (mirroring buildMaintenancePrompt, which already does this) and pass NOW in the test — or vi.useFakeTimers() + vi.setSystemTime('2026-06-13'). vi.useFakeTimers is currently used in zero test files; adopt it for any time-based test. This turns CI green.
+
+
+
+
+
+
01
Security
+
An extension that holds 20+ provider keys + OAuth tokens and feeds untrusted README content to an LLM. The core handling is sound; the gaps are attack-surface and log hygiene.
+
+
+
HIGH
Custom-endpoint save can grant <all_urls> to any origin
manifest.json:41 · options-providers.js:51
+
Saving a custom endpoint calls chrome.permissions.request({ origins: [origin + '/*'] }) against the optional https://*/* grant. A social-engineered URL (e.g. https://company-intranet.corp/) could hand the extension persistent cross-origin fetch to an internal network.
+
Fix: validate the endpoint is a public host before requesting — block RFC-1918 ranges, localhost, and .internal/.local TLDs (Ollama already has static localhost perms).
+
+
+
+
HIGH
Unsanitized stored endpoint → localhost SSRF
manifest.json:38 · background.js (callCompat)
+
Static http://localhost/* + 127.0.0.1/* permissions plus a user/import-writable endpoint string mean a tampered endpoint (http://localhost:8080/internal-admin) is fetched with no further consent.
+
Fix: validate stored endpoints against the provider's registered host pattern before fetch; pin Ollama to port 11434.
+
+
+
+
Medium & low
+
+
[MED] Prompt-injection homoglyph gap — prompt.js:7 ASCII keyword filter is bypassable by small-caps/Cyrillic lookalikes; the structural delimiters are the real (probabilistic) guard. The changelog's "hardened" claim overstates it — soften the wording.
+
[MED] OAuth log hygiene — background.js:447,498,502,1557 log callback URLs + JSON.stringify(err) provider bodies; truncate/strip token-ish fields, gate behind a DEBUG flag. (Also flagged by the code pass.)
+
[MED] whats-new.html is web-accessible to <all_urls> — enables extension fingerprinting from any page; scope to chrome-extension://*/* or drop it.
+
[MED] xAI token stored in redundant flat keys — oauth-xai.js:61; a partial clear leaves a stale xaiKey. Consolidate to structured creds after a migration window.
+
[MED] Backup import has no max string length — backup.js enforces MAX_ROWS but a 1 MB repoId passes; cap field lengths (~256).
+
[LOW] esc() doesn't escape single quotes (format.js:4) — latent for single-quoted attributes; align with safe-html.js's escapeHtml.
+
[LOW] Gemini key in URL query string (background.js:1435) — Google's REST design, not a bug, but more exposed than header auth.
+
[LOW] tabs permission broader than needed — the tabs.onUpdated OAuth listener duplicates the webNavigation one; dropping it could remove the tabs perm.
+
[LOW] No per-message rate limiting — mitigated by same-origin sendMessage + the aiChain serializer.
+
+
+
+
+
+
+
02
Code & Architecture
+
Three files hold most of the logic — library.js (2,593), output-tab.js (2,576), background.js (1,567), all ~3× the size guideline. The HIGH findings are duplication that has already silently diverged.
+
+
+
HIGH
render() and getVisibleRows() duplicate the filter/sort pipeline — and have diverged
library.js:177 vs 1834
+
The export path re-implements the filter→sort pipeline and has already dropped the eval sort, the NL-filter dedup, and parts of the collection/decision filters — so exports can return the wrong rows today.
+
Fix: extract one pure applyFilters(rows, state, maps) that both call. Highest-leverage refactor in the codebase.
+
+
+
+
HIGH
11 near-identical session-patch helpers with a read-modify-write race
background.js:771–1169
+
Every runXxx redefines a 3-line read-merge-write against a session sub-key. Two concurrent lens writers to the same key can clobber each other.
+
Fix: a makeSessionPatcher(key, subKey) factory — removes 33 lines and gives one place to serialize writes.
+
+
+
+
HIGH
Duplicated LANG_COLORS and FIT_ORDER — both already drifting
library.js:25,113… · output-tab.js:617
+
LANG_COLORS is copied in both big files (the copies diverge in palette); FIT_ORDER is re-declared 6× inside functions, one of which adds 'unrated' so ordering is inconsistent.
+
Fix: a shared lang-colors.js; hoist FIT_ORDER to module scope once.
+
+
+
+
HIGH
Global callAI queue serializes unrelated flows
background.js:1201
+
One module-level promise chain throttles all AI calls — so a multi-minute deep-dive blocks a quick ASK_CACHED for its full duration; the keep-alive .catch(()=>{}) also swallows rejections.
+
Fix: exempt low-latency parts (ask) from the deep-dive queue; document the throttling intent at the call site.
+
+
+
+
Medium & low
+
+
[MED] Silent catch {} at output-tab.js:2573 swallows a whole render block with no signal — add a console.warn + visible fallback.
+
[MED] RERUN set has no .catch (background.js:198) — a storage reject leaves the output tab polling to its 90s timeout; add error propagation to sendResponse.
+
[MED] Listener leaks — openNote/openQuickAsk (library.js:353,465) re-add keydown handlers on every open (the comment says it's fixed; it isn't). Use { once: true }.
+
[MED] Redundant storage reads — each runXxx re-reads PROVIDER_KEYS (~10 IPC round-trips on "Run All"); pass keys down from one read.
+
[MED] 14 mutable module globals in library.js — the root cause of the render/getVisibleRows drift; consolidate into one state object incrementally.
+
[LOW] window.prompt fallback (library.js:1318) — blocked in some extension contexts; remove the dead path.
+
[LOW] Implicit message contract — 20+ msg.type string literals with no shared enum; a typo is a silent no-op. Export a MSG_TYPES constant.
+
+
+
+
+
+
+
03
Tests & Coverage
+
Strong by design — ~60 vitest files cover 57 of ~75 modules, targeting pure logic (the model: extract library-data.js out of library.js and test it). The three big files are untested deliberately (zero exports, explicitly excluded) — not fake coverage. The gaps are real pure logic still trapped inside, plus two security-relevant modules.
+
+
+
HIGH
evaluations.js → computeScore has zero tests
evaluations.js
+
A pure weighted-average with null-guards, range-filtering and zero-weight fallback — used by both the eval panel and the N-way compare matrix, yet untested. The single highest-value, lowest-effort add.
+
Fix: unit-test computeScore; mock the chrome.storage CRUD exactly as oauth-openai.test.js does.
+
+
+
+
HIGH
OAuth helpers oauth-pkce.js & oauth-xai.js are untested
oauth-pkce.js · oauth-xai.js
+
Security-sensitive: base64url/createPkcePair are pure/deterministic, and the full xAI device-code flow (request/poll/store/refresh) has no coverage — while the sibling oauth-openai.js is tested.
+
Fix: add deterministic tests for the PKCE primitives and the xAI token lifecycle.
+
+
+
+
Also
+
+
[HIGH] The maintenance flake — see "the one to fix first." Root cause + the latent diff-analysis.test.js sibling; adopt vi.useFakeTimers (used in 0 files today).
+
[MED] Extractable pure logic trapped in library.js — radarToMarkdown, exportCompareMatrix, exportDecisionMatrix, exportDigest are pure builders that could move to a library-export.js and be tested like library-data.js.
+
[MED] The E2E driver isn't enforced — .verify/drive.mjs is a real Playwright harness (~20 assertions, loads the unpacked MV3 extension, 12 screenshot stages) but it's gitignored, playwright isn't a dep, and CI never runs it. Promote it: add the dep + an npm script + non-zero exit + CI wiring.
+
[LOW] Minor hygiene — store.test.js doesn't uniformly clear collections; vitest.config.js sets no clearMocks/restoreMocks.
+
+
+
+
+
+
+
04
Accessibility
+
Above-average intent — global :focus-visible, single-key handlers that guard typing fields, an exemplary aria-hidden mascot, real live regions on the library, and strong command-palette ARIA. Two consistent gaps: focus lifecycle on modals and announcement on the main output surface. Plus a cross-product contrast theme (the website pass found the same class of issue in its tokens).
+
+
+
HIGH
The output tab never announces scan progress or completion
output-tab.html:763 · 27 tab panels
+
The primary analysis surface cycles loading copy and injects into 27 panels with no aria-live/role="status" — a screen-reader user gets no signal a scan is running or done. (WCAG 4.1.3)
+
Fix:role="status" aria-live="polite" on #loading-msg + one polite sr-only region announcing start/complete/error.
+
+
+
+
HIGH
The 27-tab strip has no tab semantics; j/k nav is visual-only
output-tab.html:832 · library.js:2066
+
Tabs are plain buttons with an .active class (no role="tablist/tab/tabpanel", no aria-selected). And library setJkFocus toggles a class + scrolls but never calls .focus() on the <div> cards — SRs announce nothing, and Tab jumps away. (WCAG 4.1.2, 2.4.3)
+
Fix: add tablist roles + aria-selected; give cards tabindex="-1"/role="article" and move real focus on j/k.
+
+
+
+
HIGH
Command palette & modals: no focus trap or restoration
palette.js:95 · library.js (compare/boards)
+
Modals have good role="dialog"+aria-modal+Escape, but none trap Tab or restore focus to the trigger on close, and the palette listbox lacks aria-activedescendant (arrowing is silent to SRs). (WCAG 2.1.2, 4.1.3)
+
Fix: a shared open/close focus-lifecycle helper (move focus in, trap Tab, restore on close) applied to every popover/modal; add aria-activedescendant to the palette.
+
+
+
+
HIGH
Low-contrast text fails WCAG AA — extension themes and the website
Extension:--text-muted (descriptions/hints, 65 uses) fails AA on 5 of 13 themes — including the default midnight (4.15:1) and worst rosepine (3.41:1); --accent-as-text fails on solarized/claude/bmw. Website:--faint body text fails AA in both themes (2.20 latte / 2.69 dark — .hero-foot, footer), and small --accent/--muted labels are AA-large-only.
+
Fix: bump --text-muted/--faint to ≥4.5:1 per theme (darken light, lighten dark); route --accent-as-text through the darker --accent-deep/--accent-2.
+
+
+
+
Medium & low
+
+
[MED] No reduced-motion guard on batch.html & stack-tab.html — the only two pages missing it; both run infinite pulse loaders. Add the same @media (prefers-reduced-motion: reduce) reset (better: one shared a11y.css).
+
[MED] :focus-visible suppressed on focusable widgets — decision-popover buttons, eval badge, palette input get programmatic focus but show no ring (library.html:481, palette.css:46). (WCAG 2.4.11)
+
[MED] Icon-only buttons rely on title, not aria-label — card actions (pin/compare/note/copy/remove) + header buttons; inconsistent with #settings which has a label.
+
[MED] Boards popover has no role/label — a bare <div> (its rows are real buttons, so still operable).
+
[LOW] Inline ♥/★/? symbols without text alternatives in a few spots.
+
[LOW] Output-tab loading bar animates width over 25s — perf nit vs the team's own rules; disabled under reduced-motion, so no a11y issue.
+
+
+
+
+
+
+
05
Website
+
The fresh redesign's motion/perf hygiene is genuinely exemplary (GSAP fully lazy + 100% behind prefers-reduced-motion, correct reduced-motion video fallback, clean heading order, CLS well-defended with explicit dimensions, the case-sensitive /RepoLens basePath handled). The gaps are SEO basics and a couple of perf nits.
+
+
+
HIGH
No sitemap.xml or robots.txt
add app/sitemap.ts · app/robots.ts
+
Nothing tells crawlers the canonical URL set for a freshly launched marketing site. Next 15 emits both under output: export.
+
Fix: add app/sitemap.ts + app/robots.ts with absolute /RepoLens URLs enumerating /, /changelog, and the 10 docs routes.
+
+
+
+
Medium & low
+
+
[MED] OG/Twitter card declared but no image — layout.tsx sets summary_large_image with no images; link unfurls render bare. Add public/og.png (1200×630) or app/opengraph-image.tsx.
+
[MED] Hero video/poster not fetch-prioritized — the poster is the likely LCP but gets no preload; the 185 KB MP4 autoloads. Add preload="metadata" to the video + a poster <link rel=preload>.
+
[MED] Atmosphere paint cost — 3 fixed radial gradients + starfield + header backdrop-filter: blur; trace on a throttled device.
+
[LOW] Font-swap CLS on large display headings (no size-adjust; next/font mitigates); focus-ring contrast weak in latte; warm tab underline 1.88:1 (decorative card); fumadocs lazy-files coupling in lib/source.ts is version-fragile; the RSC .txt 404 stays cosmetic; stray untracked whats-new.html at repo root (doesn't ship).
+
+
+
+
+
+
+
06
What's solid
+
An audit is only honest if it says what's done right. A lot is.
+
+
+
API keys are never logged — passed straight to fetch headers.
+
Settings export is provably allowlist-driven — pickSafe() excludes keys/tokens on export and import.
+
PKCE is correct — CSPRNG verifier/state, SHA-256, base64url; OAuth state verified before exchange.
+
No eval / dynamic code anywhere.
+
Content script is minimal — sends one message, reads only the URL.
+
57 of ~75 modules tested — pure logic extracted and tested deliberately.
+
Mascot a11y is exemplary — always aria-hidden, meaning carried by text/color.
+
Reduced-motion done right — extension and website both gate all motion; static fallbacks.
Inject today into bandFromSignals/daysSincePush (or vi.setSystemTime in the test); fix the latent diff-analysis sibling. One change, main goes green.
+
+
+
Extract applyFilters()HIGH
+
Collapse the render()/getVisibleRows() divergence — it's an active wrong-export bug source — then lift FIT_ORDER/LANG_COLORS to shared modules.
+
+
+
Add tests: computeScore + the OAuth helpers HIGH
+
High-value, low-effort, security-relevant — the biggest coverage holes that are trivially testable.
+
+
+
Accessibility: announce + focus HIGH
+
Add the output-tab live region + tab semantics, make j/k move real focus, and add a shared modal focus-trap/restore helper.
+
+
+
Fix contrast tokens HIGH
+
Bump --text-muted/--faint to AA on the failing themes (incl. default midnight) across both extension and site; stop suppressing focus rings on focusable widgets; add the reduced-motion reset to batch.html/stack-tab.html.
Add sitemap.ts + robots.ts + an OG image; preload the hero poster.
+
+
+
Pay down the big-file debt MED · ongoing
+
Split library.js/output-tab.js/background.js along their seams (export builders, session patcher, render vs. state) as growth demands. Promote .verify/drive.mjs to enforced E2E.
+
+
+
+
+
+
+
+
+
diff --git a/website/app/(home)/styles/home.css b/website/app/(home)/styles/home.css
index 0564cca..473c0bc 100644
--- a/website/app/(home)/styles/home.css
+++ b/website/app/(home)/styles/home.css
@@ -218,12 +218,9 @@
flex-shrink: 0;
}
.vd-health-n {
- font: 800 2rem/1 var(--sans);
+ font: 800 2.1rem/1 var(--font-display, var(--sans));
letter-spacing: -0.02em;
- background: var(--grad);
- -webkit-background-clip: text;
- background-clip: text;
- color: transparent;
+ color: var(--accent);
}
.vd-health-l {
font: 600 9px/1 var(--mono);
@@ -253,22 +250,28 @@
border-bottom-color: var(--warm);
}
.vd-verdict {
- margin: 16px 20px 0;
+ margin: 18px 20px 0;
display: flex;
align-items: center;
- gap: 12px;
+ gap: 16px;
padding: 14px 16px;
- border-radius: 12px;
- background: color-mix(in srgb, var(--ok) 12%, transparent);
- border: 1px solid color-mix(in srgb, var(--ok) 34%, transparent);
+ border-radius: 10px;
+ background: var(--bg-2);
+ border: 1px solid var(--border);
}
+/* The verdict — a rubber stamp, slammed at an angle, not a chip. */
.vd-fit {
- font: 800 12px/1 var(--mono);
+ font: 800 13px/1 var(--mono);
+ letter-spacing: 0.12em;
+ text-transform: uppercase;
color: var(--ok);
- background: color-mix(in srgb, var(--ok) 18%, transparent);
- border: 1px solid color-mix(in srgb, var(--ok) 45%, transparent);
- border-radius: 7px;
- padding: 7px 10px;
+ background: transparent;
+ border: 2.5px solid var(--ok);
+ border-radius: 5px;
+ padding: 6px 11px;
+ transform: rotate(-7deg);
+ opacity: 0.9;
+ box-shadow: inset 0 0 0 2px color-mix(in srgb, var(--ok) 20%, transparent);
}
.vd-verdict-meta {
font-size: 0.84rem;
diff --git a/website/app/(home)/styles/shell.css b/website/app/(home)/styles/shell.css
index fe8e71a..111e06e 100644
--- a/website/app/(home)/styles/shell.css
+++ b/website/app/(home)/styles/shell.css
@@ -348,11 +348,9 @@
max-width: 60ch;
font-size: 1.02rem;
}
+/* No gradient text (an AI-template tell) — the key word just goes amber ink. */
.site-root .grad-text {
- background: var(--grad);
- -webkit-background-clip: text;
- background-clip: text;
- color: transparent;
+ color: var(--accent);
}
/* ── Vee mascot (token-aware SVG; expression = class swap) ────────────── */
diff --git a/website/app/global.css b/website/app/global.css
index a0e8feb..4f55598 100644
--- a/website/app/global.css
+++ b/website/app/global.css
@@ -4,151 +4,141 @@
@source '../app';
@source '../components';
-/* ────────────────────────────────────────────────────────────────────────
- RepoLens brand tokens.
- Two themes drive everything: latte (:root, light) and midnight (.dark).
- next-themes (wired by fumadocs RootProvider, `class` strategy) flips .dark,
- so both the marketing pages and the docs re-skin from a single toggle.
- Accent palettes layer on top via [data-palette] — accent ramp only.
+/* ════════════════════════════════════════════════════════════════════════
+ "The Case File" — a detective/inspector identity for RepoLens.
+ Leads LIGHT (warm manila + ink), with an amber/brass lens-glint accent and a
+ detective red for risk. Dark survives as a warm "night stakeout" (.dark), not
+ a cold terminal. next-themes flips .dark; tokens drive marketing + docs.
──────────────────────────────────────────────────────────────────────── */
:root {
- /* type + motion (theme-independent) */
- --mono: ui-monospace, "SF Mono", SFMono-Regular, Menlo, monospace;
- --sans: "SF Pro Display", system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
+ /* type + motion */
+ --mono: "JetBrains Mono", ui-monospace, "SF Mono", SFMono-Regular, Menlo, monospace;
+ --sans: "Inter", system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
+ /* --font-display is supplied by next/font (Fraunces); falls back to a serif. */
+ --serif-fallback: "Fraunces", "Iowan Old Style", Georgia, "Times New Roman", serif;
--dur-fast: 120ms;
--dur: 200ms;
--dur-slow: 320ms;
--ease-out: cubic-bezier(0.16, 1, 0.3, 1);
--ease-spring: cubic-bezier(0.34, 1.56, 0.64, 1);
+ /* the stamp's heavy "slam" */
+ --ease-stamp: cubic-bezier(0.5, 1.8, 0.4, 0.95);
- /* Inspector accent ramp — royal blue → cyan, tied to the mascot.
- [data-palette] overrides these; nothing else should redefine them. */
- --accent: #3b82f6;
- --accent-2: #2563eb;
- --accent-3: #38bdf8;
- --grad: linear-gradient(120deg, #60a5fa 0%, #3b82f6 55%, #22d3ee 120%);
- --grad-soft: linear-gradient(120deg, #3b82f6, #22d3ee);
- --accent-glow: rgba(59, 130, 246, 0.4);
- --accent-weak: rgba(59, 130, 246, 0.12);
- --accent-line: rgba(59, 130, 246, 0.3);
+ /* Amber/brass lens-glint accent + detective red. One ramp tuned for paper;
+ .dark brightens it for the night-stakeout surfaces. */
+ --accent: #9e530e;
+ --accent-2: #934a0c;
+ --accent-3: #d9822b;
+ --grad: linear-gradient(120deg, #d9822b 0%, #9e530e 60%, #934a0c 120%);
+ --grad-soft: linear-gradient(120deg, #d9822b, #9e530e);
+ --accent-glow: rgba(179, 94, 16, 0.35);
+ --accent-weak: rgba(179, 94, 16, 0.1);
+ --accent-line: rgba(179, 94, 16, 0.28);
- /* Warm signature — the lens rim. Complementary to the blue; used sparingly
- (the primary CTA, select highlights, the verdict tab underline). */
- --warm: #f59e0b;
- --warm-2: #fb923c;
+ /* warm = the amber CTA; red = the RISKY stamp / red flags */
+ --warm: #c2691c;
+ --warm-2: #d9822b;
--warm-ink: #2a1605;
- --warm-glow: rgba(245, 158, 11, 0.5);
- --warm-weak: rgba(245, 158, 11, 0.14);
- --warm-line: rgba(245, 158, 11, 0.36);
+ --warm-glow: rgba(194, 105, 28, 0.4);
+ --warm-weak: rgba(194, 105, 28, 0.12);
+ --warm-line: rgba(194, 105, 28, 0.34);
+ --red: #c0392b;
+ --red-weak: rgba(192, 57, 43, 0.12);
- /* latte — warm paper, light */
- --bg: #f3f1ec;
- --bg-2: #eae6dd;
- --surface: #ffffff;
- --surface-2: #faf8f3;
- --border: #e4ded1;
- --border-2: #d3ccba;
- --text: #34322c;
- --text-strong: #1a1813;
- --sub: #5f5a4d;
- --muted: #857f6d;
- --faint: #aaa392;
- --ok: #0f9d6a;
- --info: #0284c7;
- --warn: #b45309;
- --bad: #dc2647;
- --shadow-lg: 0 30px 80px -40px rgba(40, 34, 20, 0.28);
- --shadow-md: 0 18px 40px -24px rgba(40, 34, 20, 0.22);
- --starfield-opacity: 0;
+ /* Case File — warm manila paper, sepia ink */
+ --bg: #f4ead4;
+ --bg-2: #ecdfc2;
+ --surface: #fbf5e7;
+ --surface-2: #f5ecd6;
+ --border: #ddccab;
+ --border-2: #c8b285;
+ --text: #2a2114;
+ --text-strong: #17110a;
+ --sub: #5d5034;
+ --muted: #756544;
+ --faint: #a89571;
+ --ok: #2c7631;
+ --info: #1d6fa3;
+ --warn: #b4690a;
+ --bad: #c0392b;
+ --shadow-lg: 0 26px 70px -34px rgba(60, 42, 16, 0.38);
+ --shadow-md: 0 16px 38px -22px rgba(60, 42, 16, 0.3);
+ --paper-grain: 0.5;
- /* fumadocs remap → latte */
- --color-fd-background: #f3f1ec;
- --color-fd-foreground: #34322c;
- --color-fd-muted: #eae6dd;
- --color-fd-muted-foreground: #6f6a5b;
- --color-fd-popover: #ffffff;
- --color-fd-popover-foreground: #1a1813;
- --color-fd-card: #faf8f3;
- --color-fd-card-foreground: #1a1813;
- --color-fd-border: #e2ddd1;
- --color-fd-primary: #2563eb;
- --color-fd-primary-foreground: #ffffff;
- --color-fd-secondary: #eee9df;
- --color-fd-secondary-foreground: #34322c;
- --color-fd-accent: #ece7dc;
- --color-fd-accent-foreground: #1a1813;
- --color-fd-ring: #3b82f6;
+ /* fumadocs remap → Case File light */
+ --color-fd-background: #f4ead4;
+ --color-fd-foreground: #2a2114;
+ --color-fd-muted: #ecdfc2;
+ --color-fd-muted-foreground: #6a5c3d;
+ --color-fd-popover: #fbf5e7;
+ --color-fd-popover-foreground: #17110a;
+ --color-fd-card: #fbf5e7;
+ --color-fd-card-foreground: #17110a;
+ --color-fd-border: #ddccab;
+ --color-fd-primary: #9e530e;
+ --color-fd-primary-foreground: #fbf5e7;
+ --color-fd-secondary: #efe3ca;
+ --color-fd-secondary-foreground: #2a2114;
+ --color-fd-accent: #ecdfc2;
+ --color-fd-accent-foreground: #17110a;
+ --color-fd-ring: #9e530e;
}
.dark {
- /* midnight — blue-black observatory, dark */
- --bg: #0a0e1a;
- --bg-2: #0c1220;
- --surface: #111a2e;
- --surface-2: #16203a;
- --border: #223149;
- --border-2: #32486e;
- --text: #eaeefb;
- --text-strong: #ffffff;
- --sub: #9aa6c2;
- --muted: #6b7794;
- --faint: #4b5570;
- --ok: #34d399;
- --info: #38bdf8;
- --warn: #fbbf24;
- --bad: #fb7185;
- --shadow-lg: 0 30px 80px -40px rgba(0, 0, 0, 0.8);
- --shadow-md: 0 20px 50px -28px rgba(0, 0, 0, 0.7);
- --starfield-opacity: 0.5;
+ /* Night Stakeout — warm noir, not a cold terminal */
+ --bg: #1b1610;
+ --bg-2: #221b12;
+ --surface: #2a2116;
+ --surface-2: #32281a;
+ --border: #43361f;
+ --border-2: #5e4b2c;
+ --text: #ece1cd;
+ --text-strong: #fff8ec;
+ --sub: #c3b094;
+ --muted: #9c8a6a;
+ --faint: #6f6045;
- /* fumadocs remap → midnight */
- --color-fd-background: #0a0e1a;
- --color-fd-foreground: #eaeefb;
- --color-fd-muted: #111a2e;
- --color-fd-muted-foreground: #9aa6c2;
- --color-fd-popover: #0c1220;
- --color-fd-popover-foreground: #eaeefb;
- --color-fd-card: #111a2e;
- --color-fd-card-foreground: #eaeefb;
- --color-fd-border: #223149;
- --color-fd-primary: #60a5fa;
- --color-fd-primary-foreground: #0a0e1a;
- --color-fd-secondary: #16203a;
- --color-fd-secondary-foreground: #eaeefb;
- --color-fd-accent: #18233c;
- --color-fd-accent-foreground: #ffffff;
- --color-fd-ring: #3b82f6;
-}
+ /* brighter amber + red read better on the warm-dark surfaces */
+ --accent: #e0934a;
+ --accent-2: #cf7a2a;
+ --accent-3: #f0aa63;
+ --grad: linear-gradient(120deg, #f0aa63 0%, #e0934a 60%, #cf7a2a 120%);
+ --grad-soft: linear-gradient(120deg, #f0aa63, #e0934a);
+ --accent-glow: rgba(224, 147, 74, 0.45);
+ --accent-weak: rgba(224, 147, 74, 0.14);
+ --accent-line: rgba(224, 147, 74, 0.32);
+ --warm: #e0934a;
+ --warm-2: #f0aa63;
+ --warm-ink: #2a1605;
+ --warm-glow: rgba(224, 147, 74, 0.45);
+ --warm-weak: rgba(224, 147, 74, 0.16);
+ --warm-line: rgba(224, 147, 74, 0.36);
+ --red: #e06a5c;
+ --red-weak: rgba(224, 106, 92, 0.16);
+ --ok: #5cc05f;
+ --info: #56b6e8;
+ --warn: #e0a341;
+ --bad: #e06a5c;
+ --shadow-lg: 0 26px 70px -34px rgba(0, 0, 0, 0.72);
+ --shadow-md: 0 16px 44px -26px rgba(0, 0, 0, 0.66);
+ --paper-grain: 0.7;
-/* ── Accent palettes (showcase the theme engine) — accent ramp only ─────── */
-:root[data-palette="terminal"] {
- --accent: #34d399;
- --accent-2: #10b981;
- --accent-3: #6ee7b7;
- --grad: linear-gradient(120deg, #6ee7b7 0%, #10b981 55%, #059669 120%);
- --grad-soft: linear-gradient(120deg, #34d399, #10b981);
- --accent-glow: rgba(16, 185, 129, 0.5);
- --accent-weak: rgba(16, 185, 129, 0.12);
- --accent-line: rgba(16, 185, 129, 0.3);
-}
-:root[data-palette="nord"] {
- --accent: #88c0d0;
- --accent-2: #5e81ac;
- --accent-3: #8fbcbb;
- --grad: linear-gradient(120deg, #8fbcbb 0%, #88c0d0 50%, #5e81ac 120%);
- --grad-soft: linear-gradient(120deg, #88c0d0, #5e81ac);
- --accent-glow: rgba(94, 129, 172, 0.5);
- --accent-weak: rgba(136, 192, 208, 0.14);
- --accent-line: rgba(136, 192, 208, 0.32);
-}
-:root[data-palette="claude"] {
- --accent: #d97757;
- --accent-2: #cc785c;
- --accent-3: #e8a87c;
- --grad: linear-gradient(120deg, #e8a87c 0%, #d97757 55%, #c15f3c 120%);
- --grad-soft: linear-gradient(120deg, #d97757, #c15f3c);
- --accent-glow: rgba(217, 119, 87, 0.5);
- --accent-weak: rgba(217, 119, 87, 0.13);
- --accent-line: rgba(217, 119, 87, 0.3);
+ /* fumadocs remap → night stakeout */
+ --color-fd-background: #1b1610;
+ --color-fd-foreground: #ece1cd;
+ --color-fd-muted: #2a2116;
+ --color-fd-muted-foreground: #c3b094;
+ --color-fd-popover: #221b12;
+ --color-fd-popover-foreground: #ece1cd;
+ --color-fd-card: #2a2116;
+ --color-fd-card-foreground: #ece1cd;
+ --color-fd-border: #43361f;
+ --color-fd-primary: #e0934a;
+ --color-fd-primary-foreground: #1b1610;
+ --color-fd-secondary: #32281a;
+ --color-fd-secondary-foreground: #ece1cd;
+ --color-fd-accent: #322619;
+ --color-fd-accent-foreground: #fff8ec;
+ --color-fd-ring: #e0934a;
}
diff --git a/website/app/layout.tsx b/website/app/layout.tsx
index 1227222..fbb4e02 100644
--- a/website/app/layout.tsx
+++ b/website/app/layout.tsx
@@ -1,22 +1,22 @@
import type { ReactNode } from 'react';
import type { Metadata } from 'next';
-import { Space_Grotesk } from 'next/font/google';
+import { Fraunces } from 'next/font/google';
import { RootProvider } from 'fumadocs-ui/provider';
import './global.css';
-// Self-hosted at build time (works with output: export). Display face for
-// headings only; body stays the system stack. Exposed as --font-display.
-const display = Space_Grotesk({
+// On GitHub Pages the static export is served under //, so the static search
+// index lives at /api/search. Empty when served at root (local dev).
+const basePath = process.env.NEXT_PUBLIC_BASE_PATH ?? '';
+
+// Fraunces — a characterful display serif (self-hosted at build; works with
+// output: export). Display headings only; body stays the system/Inter stack.
+const display = Fraunces({
subsets: ['latin'],
weight: ['500', '600', '700'],
variable: '--font-display',
display: 'swap',
});
-// On GitHub Pages the static export is served under //, so the static search
-// index lives at /api/search. Empty when served at root (local dev).
-const basePath = process.env.NEXT_PUBLIC_BASE_PATH ?? '';
-
export const metadata: Metadata = {
metadataBase: new URL('https://new1direction.github.io/RepoLens/'),
title: {
@@ -24,33 +24,24 @@ export const metadata: Metadata = {
template: '%s · RepoLens',
},
description:
- 'A Chrome extension that turns any GitHub, GitLab, npm, or PyPI page into a plain-English, verdict-first briefing: what it is, whether it fits, and how it is actually built.',
+ 'A Chrome extension that investigates any GitHub, GitLab, npm, or PyPI repo and hands down a verdict — what it is, whether it fits, and how it is actually built. The case file on any dependency.',
applicationName: 'RepoLens',
openGraph: {
title: 'RepoLens — read code before you trust it',
description:
- 'One click turns any repo into a verdict-first briefing — fit, health, and the real shape of the thing. Bring your own model. Nothing leaves your browser.',
+ 'One click opens the case file on any repo: a verdict, the evidence, the red flags. Bring your own model. Nothing leaves your browser.',
type: 'website',
siteName: 'RepoLens',
},
twitter: { card: 'summary_large_image', title: 'RepoLens', description: 'Read code before you trust it.' },
};
-/**
- * Applies the saved accent palette (data-palette) before first paint so the
- * named palettes don't flash. Light/dark is handled by next-themes' own script.
- */
-const PALETTE_SCRIPT = `(function(){try{var p=localStorage.getItem('repolens-palette');if(p&&p!=='inspector')document.documentElement.setAttribute('data-palette',p);}catch(e){}})();`;
-
export default function RootLayout({ children }: { children: ReactNode }) {
return (
-
-
-
{children}
diff --git a/website/components/home/FeatureBento.tsx b/website/components/home/FeatureBento.tsx
index f04656b..534c4ad 100644
--- a/website/components/home/FeatureBento.tsx
+++ b/website/components/home/FeatureBento.tsx
@@ -78,7 +78,7 @@ export function FeatureBento() {
return (
- One token-aware lens, thirteen themes, one accent swap away. Try a palette — the whole
- page and the mascot re-skin live. Every expression maps to a real scan moment, and all of
- it folds to a static glyph under reduced-motion.
+ The lens mascot reacts to what it finds — wide-open on a clean repo, narrowed and
+ skeptical on a risky one. Every expression maps to a real scan moment, and all of it
+ folds to a static glyph under reduced-motion. Works the day shift or the night stakeout.
-
- Accent
-
-
-
{EXPRESSIONS.map((e) => (
diff --git a/website/components/home/VerdictDemo.tsx b/website/components/home/VerdictDemo.tsx
index c54dfb4..e539544 100644
--- a/website/components/home/VerdictDemo.tsx
+++ b/website/components/home/VerdictDemo.tsx
@@ -9,7 +9,7 @@ export function VerdictDemo() {