← back to Norma Sdcc Pitch
add: top-5 priorities, reluctant-adopter, companies-are-databases, wix integration + UX/UI principles, glossary (80 terms across 10 categories), 2 new vocab badges
290711480f992e6c1ec8fe67ceef5028d834bb78 · 2026-05-20 08:31:19 -0700 · SteveStudio2
- priorities section ranks the 5 SDCC needs (email coordination / inbound / graphics / wix site / wix UX-UI) with pain → solution → before/after → agent-pill links
- reluctant_adopter answers 7 common objections + Tuesday-2pm before/after narrative
- companies_are_databases callout with 6-row shift table (Website → API, etc)
- wix_integration with 5 ranked integration paths (forms→webhook, velo fetch, iframe embed, structured intake, CMS write-back) + 6 UX/UI principles
- glossary with 10 categories, 80 terms, search, category chips, expand-collapse, mark-as-learned XP, 2 new badges (Vocab Starter, Vocab Champ)
Files touched
M data/features.jsonM public/app.jsM public/index.htmlM public/styles.css
Diff
commit 290711480f992e6c1ec8fe67ceef5028d834bb78
Author: SteveStudio2 <stevestudio2@SteveStacStudio.lan>
Date: Wed May 20 08:31:19 2026 -0700
add: top-5 priorities, reluctant-adopter, companies-are-databases, wix integration + UX/UI principles, glossary (80 terms across 10 categories), 2 new vocab badges
- priorities section ranks the 5 SDCC needs (email coordination / inbound / graphics / wix site / wix UX-UI) with pain → solution → before/after → agent-pill links
- reluctant_adopter answers 7 common objections + Tuesday-2pm before/after narrative
- companies_are_databases callout with 6-row shift table (Website → API, etc)
- wix_integration with 5 ranked integration paths (forms→webhook, velo fetch, iframe embed, structured intake, CMS write-back) + 6 UX/UI principles
- glossary with 10 categories, 80 terms, search, category chips, expand-collapse, mark-as-learned XP, 2 new badges (Vocab Starter, Vocab Champ)
---
data/features.json | 757 ++++++++++++++++++++++++++++++++++++++++++++++++++++-
public/app.js | 233 +++++++++++++++++
public/index.html | 100 ++++++-
public/styles.css | 248 ++++++++++++++++++
4 files changed, 1328 insertions(+), 10 deletions(-)
diff --git a/data/features.json b/data/features.json
index 53943cb..d285b2a 100644
--- a/data/features.json
+++ b/data/features.json
@@ -1,4 +1,259 @@
{
+ "companies_are_databases": {
+ "headline": "The big mental shift — your org is now a database.",
+ "kicker": "If you only remember one thing from this pitch",
+ "body": "Until recently, an organization was a website + an email account + people. The website was a brochure. The email was where work happened. The people held the knowledge in their heads.\n\nThat era is ending. In the AI era, every organization is a DATABASE — a structured store of who you know, what you've said, who you've helped, what worked, what failed — that humans AND AI agents can query, update, and act on.\n\nThe organizations that win in the next 5 years are the ones that treat themselves as databases first, websites second. Your knowledge stops dying when staff leaves. Your past statements become searchable. Your borrower stories become assets, not buried email threads. Your coalition map becomes living, not a stale Google Doc.",
+ "implication_for_sdcc": "SDCC's most valuable asset is not the studentdebtcrisis.org website. It's not even Natalia's Rolodex. It's the structured database of every borrower SDCC has ever helped, every statement SDCC has ever published, every coalition relationship SDCC has ever built — IF it exists in a form that can be searched, queried, and acted on. Right now most of that is stuck in Gmail, Google Docs, and people's memories. Norma's job is to extract it, structure it, and let SDCC USE it.",
+ "shift_table": [
+ { "before": "Website (brochure)", "after": "API (queryable, updatable)" },
+ { "before": "Email (work happens here)", "after": "Triage layer (work GETS DECIDED here, but happens in the database)" },
+ { "before": "People (hold knowledge in heads)", "after": "People (decide; the database remembers)" },
+ { "before": "PDFs of past reports", "after": "Searchable archive with citations" },
+ { "before": "Coalition Google Doc, stale", "after": "Live power map, updated by agents" },
+ { "before": "Borrower stories in email threads", "after": "Structured database — searchable by program, geography, urgency" }
+ ]
+ },
+
+ "priorities": {
+ "headline": "Top 5 priorities — what SDCC needs FIRST",
+ "intro": "These are the highest-leverage problems we solve first. Not 'cool AI features' — actual SDCC pains, in order of impact.",
+ "items": [
+ {
+ "id": "p1",
+ "rank": 1,
+ "title": "Email coordination across the team",
+ "icon": "📬",
+ "the_pain": "Three staffers, one info@ inbox. Who replied to which borrower? Who's already followed up with which journalist? Who's the lead on the Lumina application? Right now SDCC tracks this in shared mental models — i.e. you guess, you check Slack, you ask Sabrina.",
+ "the_solution": "Norma is the source of truth for every conversation. When a message comes in, it's claimed by a staffer (or auto-assigned). Every reply is logged. Every follow-up is scheduled. Anyone on the team can see who's handling what, when last touched, and what was said.",
+ "how_norma_helps": [
+ "Auto-assignment by topic — borrower questions → Sabrina; press inquiries → Natalia; grants questions → board ops.",
+ "'Claim' button on every conversation so two people don't reply to the same journalist.",
+ "Follow-up reminders 3/7/14 days after a sent reply.",
+ "Single search across every conversation — 'what did we tell journalist X last month?'"
+ ],
+ "agents_skills": ["gmail-triage", "audit-trail", "pulse-briefing"],
+ "before_after": {
+ "before": "Sabrina replied to a journalist Tuesday. Natalia replied to the SAME journalist Wednesday with a slightly different stance. The journalist published the inconsistency.",
+ "after": "Wednesday Natalia opens the thread, sees 'Sabrina replied 22h ago' with the full reply quoted. Natalia adds a follow-up that builds on it."
+ }
+ },
+ {
+ "id": "p2",
+ "rank": 2,
+ "title": "Inbound emails — the 400-message inbox",
+ "icon": "📥",
+ "the_pain": "info@studentdebtcrisis.org gets 200-400 messages a day. Some are borrower help requests, some are press inquiries, some are coalition partners, some are spam. Triage takes hours. Important messages get buried.",
+ "the_solution": "Every inbound message gets classified the moment it arrives. By topic (borrower help, press, vendor, coalition), urgency (default crisis, garnishment notice, casual question), and required action. A draft reply is pre-written. The 400 messages become 20-30 priority items by 7am.",
+ "how_norma_helps": [
+ "Classifier reads every message and tags it: type / urgency / who-should-handle.",
+ "Drafts a first-pass reply in SDCC's voice — staffer reviews, edits, sends.",
+ "Routes high-urgency to a Slack channel (default-crisis borrower stories, journalists on deadline, etc).",
+ "Surfaces every email containing a borrower story → tags by debt type, geography, anonymizability — building the structured story database."
+ ],
+ "agents_skills": ["gmail-triage", "borrower-story-classifier", "pulse-briefing"],
+ "before_after": {
+ "before": "Sabrina at 9am: 'I have 387 unread. I'll start with the most recent and hope.'",
+ "after": "Sabrina at 9am: 'I have 22 priority drafts. Norma triaged the other 365 — 240 into FAQ, 80 into newsletter signup, 45 into vendor pitches I'll never read.'"
+ }
+ },
+ {
+ "id": "p3",
+ "rank": 3,
+ "title": "Graphics — social cards, press graphics, mockups",
+ "icon": "🎨",
+ "the_pain": "Every campaign needs visuals. SDCC doesn't have a designer on staff. Canva works but takes hours. Hiring a designer per campaign is expensive and slow. Most petitions launch with no graphic and underperform.",
+ "the_solution": "Norma generates campaign-ready graphics from a topic prompt: social cards (Instagram + X + Bluesky), petition heroes, press release banners, infographics. SDCC's brand colors + typography baked in. Generated in 30 seconds per asset. SDCC reviews + tweaks (or kicks back for regeneration) before publish.",
+ "how_norma_helps": [
+ "AI image generation tuned to SDCC's visual brand (royal blue + civic, no piggy-bank clipart).",
+ "Templates for: petition launch card, press release banner, social media quote card, infographic, action alert.",
+ "One-click generate 5 variants per asset, pick the best.",
+ "Auto-resize for every platform: 1080x1080 IG, 1200x628 X header, 1080x1920 stories, etc."
+ ],
+ "agents_skills": ["graphic-designer", "petition-generator", "reels-producer"],
+ "before_after": {
+ "before": "Sabrina spends 90 minutes in Canva making a social card. Or skips it. Or asks a volunteer who delivers in 4 days.",
+ "after": "Sabrina types '/social-card SAVE rollback'. Five variants in 30 seconds. Picks one. Tweaks the headline. Published in under 2 minutes."
+ }
+ },
+ {
+ "id": "p4",
+ "rank": 4,
+ "title": "studentdebtcrisis.org on Wix — making it work harder",
+ "icon": "🌐",
+ "the_pain": "The Wix site is a brochure. It tells people who SDCC is but doesn't capture borrower stories systematically, doesn't auto-route press inquiries, doesn't surface live data (signature counts, recent wins), doesn't integrate with the rest of SDCC's stack.",
+ "the_solution": "Wix becomes a smart front door. Forms route to Norma for classification + draft replies. Live signature counts pulled from Norma. Recent wins surface automatically. Borrower-story intake form structures the data on the way in (vs unstructured email threads). Pulse brief embedded for transparency.",
+ "how_norma_helps": [
+ "Wix forms POST to Norma's `/api/wix-webhook` → message arrives pre-classified in info@ inbox.",
+ "Wix Velo (Wix's code IDE) calls Norma's read APIs to embed live data — petition counts, recent press hits.",
+ "Norma's Pulse dashboard embeddable as an iframe on a private '/team' page only staff see.",
+ "Donation thank-you triggers personalized follow-up via Norma's outreach pipeline."
+ ],
+ "agents_skills": ["wix-integration", "borrower-story-classifier", "gmail-triage"],
+ "before_after": {
+ "before": "Wix contact form → emails info@ as a generic 'You have a new submission' → buried in 400 messages → forgotten.",
+ "after": "Wix contact form → POSTs to Norma → classified (borrower help / press / etc) → draft reply ready → tagged by debt type → in Sabrina's priority queue by 7am."
+ }
+ },
+ {
+ "id": "p5",
+ "rank": 5,
+ "title": "Adding UX + UI capability INTO Wix",
+ "icon": "✨",
+ "the_pain": "Wix's default templates look fine but generic. SDCC's site doesn't feel as urgent or distinctive as the work demands. Hiring a Wix designer per change is slow and expensive.",
+ "the_solution": "Wix lets you customize via the Editor (no-code) for visual changes and Velo (Wix's JavaScript dev environment) for interactive UX. Norma's UI patterns can be lifted into Wix piece by piece — Pulse-style daily brief blocks, urgency-tagged story cards, accessible WCAG-AA buttons, the lot.",
+ "wix_specific_steps": [
+ "Step 1 — Enable Wix Velo (Dev Mode → Enable Velo). Gives you a code IDE inside the Wix Editor.",
+ "Step 2 — Add data collections in Wix's CMS. Treat them like Postgres tables — 'BorrowerStories', 'Petitions', 'PressMentions'. Each row is structured, queryable.",
+ "Step 3 — Build Repeater components in Wix that pull from those collections — basically Norma-style grids inside Wix.",
+ "Step 4 — Add Velo code blocks that call Norma's read APIs via fetch(). E.g. live signature count on a petition page.",
+ "Step 5 — Replace generic Wix templates with custom layouts using Wix's Grid + Flexbox containers. Borrow Norma's spacing/typography tokens.",
+ "Step 6 — Add accessibility: every image gets alt text, every button has focus state, color contrast WCAG AA minimum. Wix has a built-in a11y checker — use it."
+ ],
+ "how_norma_helps": [
+ "Norma's design system (colors, typography, spacing) ships as a single CSS file you can paste into Wix's Custom CSS panel.",
+ "Norma exposes read-only API endpoints Wix Velo can call — no DB credentials needed in Wix.",
+ "Wix MCP server (planned) lets Claude Code edit Wix pages from terminal — same workflow as editing Norma."
+ ],
+ "agents_skills": ["wix-integration", "ui-ux-designer", "graphic-designer"],
+ "before_after": {
+ "before": "Wix site looks like every other Wix template. Generic typography, default spacing, no live data.",
+ "after": "Wix site has SDCC's actual brand, lives data from Norma (signature counts, recent press), and matches the team workflow. Visitors feel the urgency."
+ }
+ }
+ ]
+ },
+
+ "wix_integration": {
+ "headline": "Wix ↔ Norma — concrete integration guide",
+ "intro": "Wix is fine. It's where SDCC's site lives. Here's exactly how to make it work with Norma's data + workflows.",
+ "options": [
+ {
+ "name": "Forms → Norma webhook (start here)",
+ "difficulty": "Easy",
+ "time": "30 minutes",
+ "steps": [
+ "In Wix Editor, open any contact form → Settings → Submission tab.",
+ "Set 'Webhook URL' to `https://norma.studentdebtcrisis.org/api/wix-webhook?form=contact` (replace with your Norma URL).",
+ "Test the form. The submission lands in Norma's inbound queue pre-classified.",
+ "In Norma, view incoming Wix submissions at /pulse → Inbound tab."
+ ]
+ },
+ {
+ "name": "Live data → Wix Velo fetch (read-only widgets)",
+ "difficulty": "Medium",
+ "time": "1-2 hours per widget",
+ "steps": [
+ "In Wix Editor, enable Dev Mode (top bar → 'Dev Mode' → Enable Velo).",
+ "Add a Text or Repeater element to your page. Give it an ID like #petitionCount.",
+ "Open the Code Panel (right side). Add: `import {fetch} from 'wix-fetch'; $w.onReady(async () => { const r = await fetch('https://norma.../api/petitions/topics?limit=5'); const data = await r.json(); $w('#petitionCount').text = String(data.topics.length); });`",
+ "Publish. Wix now pulls live counts from Norma on every page load."
+ ]
+ },
+ {
+ "name": "Embed Norma's Pulse dashboard (private team page)",
+ "difficulty": "Easy",
+ "time": "15 minutes",
+ "steps": [
+ "Create a new Wix page at /team. Set page permissions: 'Members only' + restricted to SDCC staff emails.",
+ "Add an HTML Embed element (Wix Editor → Add → Embed → HTML iFrame).",
+ "Set src to `https://norma.studentdebtcrisis.org/pulse` and add cookie passthrough.",
+ "Publish. Staff visit studentdebtcrisis.org/team and see live Pulse dashboard inside Wix."
+ ]
+ },
+ {
+ "name": "Borrower story intake — structured from the start",
+ "difficulty": "Medium",
+ "time": "2 hours",
+ "steps": [
+ "In Wix CMS, create a Collection called 'BorrowerStories' with fields: debtType (dropdown: federal/private/parent-PLUS), urgency (dropdown: low/medium/crisis), state, story (long text), consentForPress (boolean), email.",
+ "Build a form in Wix that writes to this collection.",
+ "In Velo, on form submit, ALSO POST the structured data to Norma's `/api/borrower-stories/intake`.",
+ "Norma de-duplicates by email + classifies further (PSLF eligible? In default? Anonymizable?).",
+ "Result: every story enters SDCC's database structured + with email triage already started."
+ ]
+ },
+ {
+ "name": "Wix CMS as a write-back target",
+ "difficulty": "Harder",
+ "time": "1 day",
+ "steps": [
+ "Generate a Wix API key (Wix Dashboard → Settings → Headless Settings).",
+ "Store the key in Norma's env: `WIX_API_KEY=...`.",
+ "When Norma generates a new petition, it can POST to Wix CMS to create a corresponding Wix page automatically.",
+ "Risk: Wix changes are slow to propagate. Only do this for non-time-sensitive content."
+ ]
+ }
+ ],
+ "wix_ux_ui_principles": [
+ { "title": "Stop using stock photos of stressed students", "detail": "Every other debt nonprofit uses them. SDCC's distinctiveness comes from REAL borrower stories with real names + faces (when consented). Wix supports custom photo galleries." },
+ { "title": "Live counters > static hero text", "detail": "'12,847 borrowers helped this year' updating in real-time beats '15 years of advocacy.' Velo + Norma API makes this trivial." },
+ { "title": "Mobile-first or you're invisible", "detail": "60%+ of debt-distressed users hit the site from phone. Wix's mobile editor is separate — actually test it." },
+ { "title": "Accessibility is non-negotiable", "detail": "Many SDCC supporters have disabilities or are under cognitive load (debt stress). WCAG AA, every page. Wix has an a11y checker — use it before publish." },
+ { "title": "Three primary actions, not seventeen", "detail": "Every page should have a clear hierarchy of 'what we want you to do here.' Most Wix templates have too many CTAs." },
+ { "title": "Show, don't tell", "detail": "Don't write 'we help borrowers' — show a borrower story with name + photo + outcome. Stories convert; abstractions don't." }
+ ]
+ },
+
+ "reluctant_adopter": {
+ "headline": "If you're skeptical of AI — this section is for you.",
+ "intro": "You don't have to love AI. You don't have to believe the hype. You just need to know what it does for your Tuesday at 2pm when the inbox is at 400 and a journalist is calling.",
+ "objections": [
+ {
+ "objection": "\"I don't trust AI to write in my voice.\"",
+ "answer": "You shouldn't — yet. The first month is AI proposes, you edit, you approve, you send. Every reply, every statement, every social post passes through your hands before it goes out. By the time you trust it (week 3-4), it's because you've personally approved hundreds of drafts and watched it learn your voice. Trust is earned, not assumed.",
+ "before_after": { "before": "Sabrina writes 40 replies/day from a blank cursor. Each takes 4-7 min.", "after": "Sabrina reviews 40 drafts that already exist. Each takes 60 seconds — most get sent as-is, some get a 10-second edit." }
+ },
+ {
+ "objection": "\"I'd rather hire a person.\"",
+ "answer": "If SDCC has the money for a full-time hire, do that AND this. A part-time hire costs $30K+/year. Norma costs $200-800/month. They solve different problems — hires bring judgment + relationships; AI absorbs typing + research. The orgs that win do both.",
+ "before_after": { "before": "Hire $40K/year part-timer to triage inbox. Takes 6 weeks to onboard.", "after": "Spend $400/month on Norma. Live in 2 days. Hire the part-timer for borrower 1:1s instead." }
+ },
+ {
+ "objection": "\"What if it hallucinates and embarrasses us?\"",
+ "answer": "It will hallucinate sometimes. That's why every outbound message goes through a human. The risk isn't AI making a mistake — it's a tired staffer at 11pm hitting send without reading carefully. Norma's audit trail and citations actually make THAT mistake LESS likely.",
+ "before_after": { "before": "Sabrina at 11pm sends an email with a typo'd statistic.", "after": "Sabrina at 11pm sees the draft has a citation footer; clicks the cited source; catches the typo before send." }
+ },
+ {
+ "objection": "\"I'm not a tech person.\"",
+ "answer": "You don't have to be. The Norma interface is Gmail-shaped — read, click, approve, send. If you can use Gmail, you can use Norma. Steve handles the technical side. SDCC handles the borrower-centered judgment side.",
+ "before_after": { "before": "\"I'd love to use AI but I wouldn't know where to start.\"", "after": "\"Open Norma in the morning, read the Pulse brief, approve the drafts that look right, edit the ones that don't, send.\"" }
+ },
+ {
+ "objection": "\"Won't this make us less authentic?\"",
+ "answer": "Authenticity isn't who types the first draft — it's who decides what gets said and why. SDCC's authentic voice is Natalia + Sabrina's judgment. AI absorbs the typing. The voice is yours, the strategy is yours, the borrower relationships are yours.",
+ "before_after": { "before": "Natalia writes statements at 2am because she's the only one who can find the voice.", "after": "Natalia approves drafts at 7am over coffee. The voice is still hers — it just doesn't cost her sleep." }
+ },
+ {
+ "objection": "\"What if donors think we're cutting corners?\"",
+ "answer": "Frame it the opposite way: SDCC is investing in capacity-building so every dollar of donor money reaches 5× more borrowers. AI is leverage on impact, not a corner-cut. Funders LOVE this story when told correctly.",
+ "before_after": { "before": "SDCC reaches 10,000 borrowers/year with current staff.", "after": "Same staff reaches 50,000 borrowers/year. Donor reports write themselves." }
+ },
+ {
+ "objection": "\"This is going to fail and I'll have wasted 3 months.\"",
+ "answer": "Three-month pilot, kill switch in writing, no annual commitment until SDCC says yes. If it doesn't work, you walk away with: (1) your data, exported, (2) a better understanding of what your team actually needs, (3) $1,500 ish spent — less than one staff retreat. The downside is tiny. The upside is enormous.",
+ "before_after": { "before": "12-month enterprise contract, paid upfront, locked in.", "after": "Month-to-month pilot. Cancel anytime. Steve eats the risk." }
+ }
+ ],
+ "your_tuesday": {
+ "label": "Your Tuesday at 2pm",
+ "before": [
+ "Inbox at 387 unread. Three journalists in your Slack DMs.",
+ "ED just put out a press release. You're trying to draft a response while three other things bleed.",
+ "Someone on the team asks 'do we have a borrower in Texas with garnishment?' You think you do but you'd have to dig through six months of emails to find them.",
+ "Tomorrow's grants deadline you forgot until just now.",
+ "You leave at 8pm with the response statement still half-drafted."
+ ],
+ "after": [
+ "Inbox shows 12 priority items — Norma triaged the other 375 into folders.",
+ "Press response draft was waiting at 4:05pm when the ED announcement dropped at 4:00. You spent 6 minutes editing it; sent at 4:30.",
+ "'Borrower in Texas with garnishment' search returns 3 stories in 5 seconds, anonymized for press use.",
+ "Norma reminded you about the grants deadline yesterday at 9am. Application submitted.",
+ "You leave at 5:30. Response is live. Statement is shared. Grants is in. Inbox is closed."
+ ]
+ },
+ "the_promise": "The point of AI here isn't 'cool technology.' It's that Tuesday should not destroy you anymore."
+ },
+
"lecture": {
"why_ai": {
"headline": "Why AI for a Nonprofit?",
@@ -504,10 +759,508 @@
}
],
+ "glossary": {
+ "headline": "Onboarding glossary — every term a new grad needs",
+ "intro": "Written for someone with a fresh CS/policy degree from a good university but zero practical exposure to this stack. Tap any card to expand. Mark as learned to earn XP toward the Vocabulary Champ badge.",
+ "categories": [
+ {
+ "id": "computer-basics",
+ "label": "Your computer (the basics)",
+ "icon": "💻",
+ "terms": [
+ {
+ "term": "Terminal",
+ "tldr": "The text-based interface to your computer. Where you type commands instead of clicking buttons.",
+ "detail": "Every command-line task — installing software, running servers, deploying code, talking to git — happens through a terminal. The terminal itself is just an app that hosts a 'shell.' On macOS the default app is Terminal.app. On Windows it's PowerShell or Windows Terminal.",
+ "example": "Open Spotlight (⌘ + Space), type 'Terminal', hit enter. You'll see a prompt like `you@laptop ~ %`. Type `pwd` and hit enter — it prints your current directory."
+ },
+ {
+ "term": "iTerm2",
+ "tldr": "A better terminal app for macOS than the default Terminal.app. Tabs, split panes, mouse support, search.",
+ "detail": "Free, open-source. Install at iterm2.com or via Homebrew (`brew install --cask iterm2`). Steve uses iTerm2 because it supports parallel tabs (one per Claude session) with distinct colors per tab so they don't blur together.",
+ "example": "After install: iTerm2 → Preferences → Profiles → set 'Working Directory' to 'Reuse previous session's directory'. Cmd+T for new tab, Cmd+D to split."
+ },
+ {
+ "term": "Shell (zsh, bash)",
+ "tldr": "The program running inside the terminal that interprets the commands you type.",
+ "detail": "zsh is macOS's default since 2019. Older Macs use bash. Different shells = slightly different syntax for things like aliases and scripts. You'll see `.zshrc` and `.bashrc` files in your home directory — those are the configuration files for your shell.",
+ "example": "Echo your current shell: `echo $SHELL`. Usually returns `/bin/zsh`."
+ },
+ {
+ "term": "Command line / CLI",
+ "tldr": "Synonym for 'using the terminal'. CLI = Command-Line Interface, the opposite of GUI (graphical).",
+ "detail": "Most developer tools ship a CLI. You'll spend half your day in CLIs once you're past the GUI stage. The fastest path to productivity at any new job is becoming fluent in the team's CLIs.",
+ "example": "`git status`, `npm install`, `claude --help` are all CLI commands."
+ },
+ {
+ "term": "Git",
+ "tldr": "The version-control system every modern team uses. Tracks every change to every file.",
+ "detail": "Git is the tool. GitHub is the website that hosts git repositories. They're different — you can use git without GitHub (Steve does for local versioning), but GitHub adds collaboration features. Critical commands: `git status`, `git add`, `git commit`, `git log`, `git diff`.",
+ "example": "After making a code change: `git add -A && git commit -m 'fix typo in petition generator'`. Now your change is locked into git history forever."
+ },
+ {
+ "term": "GitHub",
+ "tldr": "A website (microsoft-owned) that hosts git repositories and adds collaboration tools — pull requests, issues, actions (CI/CD).",
+ "detail": "SDCC's repos will likely live on GitHub eventually. You don't NEED GitHub to use git, but you need GitHub to share code with teammates."
+ },
+ {
+ "term": "Node.js + npm",
+ "tldr": "Node.js runs JavaScript on the server. npm is its package manager — the tool that installs JS libraries from a global registry.",
+ "detail": "Node.js is the runtime. npm (Node Package Manager) is the installer. Norma is a Node.js app. Install Node from nodejs.org or via Homebrew (`brew install node`). After install you have both `node` and `npm` commands.",
+ "example": "`npm install express` adds the express web framework to the current project. `npm run dev` starts whatever the project defined as its dev command."
+ },
+ {
+ "term": "localhost",
+ "tldr": "Your own computer. When code runs locally, it talks to itself via the hostname 'localhost' (which always means 127.0.0.1).",
+ "detail": "When you see `http://localhost:7400`, that's a server running ON YOUR machine, listening on port 7400. Other people on the internet cannot reach it. To make it reachable you'd need to deploy it to a real server."
+ },
+ {
+ "term": "Port (e.g. :7400)",
+ "tldr": "A number that identifies which service on a machine you're talking to. Like a phone extension.",
+ "detail": "A single computer can run many services. Each grabs a different port number so requests don't collide. Norma uses :7400. The pitch viewer uses :9876. Web servers usually use :80 (HTTP) or :443 (HTTPS) in production. Anything 1024+ is fair game in dev.",
+ "example": "Norma is at http://localhost:7400 — the :7400 is the port."
+ },
+ {
+ "term": "Environment variable (env var)",
+ "tldr": "A named value your code reads from the environment, instead of hardcoding it in the source.",
+ "detail": "Secrets like API keys go in env vars (never in source code that gets committed to git). A `.env.local` file is a common pattern — list `KEY=value` pairs there, the app reads them at startup. NEVER commit `.env*` files to git.",
+ "example": "`ANTHROPIC_API_KEY=sk-ant-...` goes in `.env.local`. Code reads it via `process.env.ANTHROPIC_API_KEY`."
+ }
+ ]
+ },
+ {
+ "id": "ai-fundamentals",
+ "label": "AI fundamentals",
+ "icon": "🧠",
+ "terms": [
+ {
+ "term": "LLM (Large Language Model)",
+ "tldr": "A neural network trained on huge amounts of text that can predict the next word — well enough to write essays, code, answer questions.",
+ "detail": "LLMs don't 'know' things the way humans do. They have statistical patterns over text. They CAN be wrong with high confidence (see: hallucination). Examples: Claude, GPT-4, Gemini, Llama, Hermes. Different LLMs differ in size, training data, RLHF (post-training tuning), and what they're best at.",
+ "example": "When you type a question into ChatGPT, the LLM reads your prompt and generates a reply one token at a time."
+ },
+ {
+ "term": "Token",
+ "tldr": "The smallest unit an LLM processes. Usually 3-4 characters of English, roughly ¾ of a word.",
+ "detail": "LLMs charge by tokens (input + output). Counting tokens matters for cost and context-window management. Rule of thumb: 1000 tokens ≈ 750 English words.",
+ "example": "'Hello, world!' is about 4 tokens. A 2,000-word essay is ~2,700 tokens."
+ },
+ {
+ "term": "Context window",
+ "tldr": "The maximum amount of text (in tokens) an LLM can read at once.",
+ "detail": "Claude Sonnet 4 has a 200K-token context window — about 150,000 words. Claude Opus 4.7 (1M context, what's running this pitch viewer) is 1 million tokens — entire codebases fit inside. Bigger windows = more powerful tasks but more expensive per call."
+ },
+ {
+ "term": "Prompt",
+ "tldr": "The text you send the LLM. Includes your question and any context/instructions.",
+ "detail": "A 'system prompt' is hidden instructions the LLM sees first ('You are a helpful student-debt advocate'). The 'user prompt' is what you type. Prompts are the primary way to control LLM behavior — better prompts = better outputs."
+ },
+ {
+ "term": "Inference",
+ "tldr": "When you ask the LLM a question and it generates an answer. 'Running inference' = 'using the model'.",
+ "detail": "Separate from training. Training is how the model was built (months, expensive). Inference is each call to the model (seconds, much cheaper). Norma runs inference every time it drafts a statement or classifies an email."
+ },
+ {
+ "term": "Hallucination",
+ "tldr": "When an LLM makes up something that sounds plausible but is false.",
+ "detail": "Biggest risk of using LLMs in advocacy. The model might invent a Supreme Court case, misquote a politician, or fabricate a statistic — all with high confidence. Mitigation: citations on every claim, human review before send, RAG (next term)."
+ },
+ {
+ "term": "RAG (Retrieval Augmented Generation)",
+ "tldr": "Instead of relying only on what the LLM knows from training, RAG looks up real source documents at query time and gives them to the LLM as context.",
+ "detail": "Pattern: user asks a question → system searches a database (e.g. SDCC's petition archive) for relevant snippets → puts those snippets in the prompt → LLM answers based on the snippets. This is how Norma's 'ask the knowledge base' feature avoids hallucination.",
+ "example": "Question: 'What did SDCC say about parent PLUS in 2024?' → RAG retrieves SDCC's 2024 parent PLUS statements → LLM summarizes with citations."
+ },
+ {
+ "term": "Embedding",
+ "tldr": "A numeric vector that represents a piece of text's 'meaning'. Used to find semantically-similar documents.",
+ "detail": "Every document gets converted into a 768- or 1536-dimensional vector. Two documents with similar meaning have vectors that are close in that 1536-dim space. That's how RAG finds relevant docs — by vector similarity, not keyword match."
+ },
+ {
+ "term": "Vector database",
+ "tldr": "A database optimized for storing + searching embeddings.",
+ "detail": "Examples: Pinecone, Weaviate, pgvector (Postgres extension). Norma uses pgvector (a Postgres extension) so vectors live alongside relational data."
+ },
+ {
+ "term": "Agent",
+ "tldr": "An LLM that can use tools (call APIs, run code, search the web) in a loop to accomplish a goal, not just answer one question.",
+ "detail": "Instead of 'answer this question', an agent gets 'classify all incoming emails, draft replies, route urgent ones to Slack' — and figures out the steps itself. Agents are LLMs + tools + a loop. Norma's inbox triage is an agent."
+ },
+ {
+ "term": "Tool use / Function calling",
+ "tldr": "When you let the LLM call functions you defined — search the database, send an email, look up a contact.",
+ "detail": "You hand the LLM a list of available tools ('search_petitions', 'draft_email', 'get_borrower_story'). The LLM decides which to call based on the user request and returns a structured call. Your code executes the call and returns results to the LLM."
+ },
+ {
+ "term": "Temperature",
+ "tldr": "A knob 0-1 controlling randomness. 0 = deterministic. 1 = creative/varied.",
+ "detail": "Press statements: temperature 0.3-0.5 (consistent, professional). Brainstorming: 0.7-0.9 (creative). Norma usually defaults to 0.7."
+ },
+ {
+ "term": "Multi-modal",
+ "tldr": "An LLM that handles more than just text — also images, audio, video.",
+ "detail": "Claude is multi-modal: you can paste a screenshot of a press release and ask the model to extract key claims. Useful for SDCC when reviewing graphics, PDFs, news photos."
+ },
+ {
+ "term": "Fine-tuning vs RAG vs Prompting",
+ "tldr": "Three ways to make an LLM better at your specific task. Prompting is cheapest. RAG is more accurate. Fine-tuning is most powerful but expensive.",
+ "detail": "Prompting: just write better instructions (start here). RAG: feed the LLM your data at query time (default for knowledge bases). Fine-tuning: actually retrain the model on your data (rare, expensive, last resort)."
+ }
+ ]
+ },
+ {
+ "id": "anthropic-claude",
+ "label": "Anthropic + Claude",
+ "icon": "🟧",
+ "terms": [
+ {
+ "term": "Anthropic",
+ "tldr": "The AI safety company that builds Claude. Founded 2021 by ex-OpenAI researchers including Dario and Daniela Amodei.",
+ "detail": "Anthropic's pitch is 'AI safety as a science'. They publish on alignment, scaling laws, constitutional AI. Norma uses Anthropic's Claude API for all its inference. Anthropic does not see SDCC's borrower data unless you explicitly opt into their feedback program (Norma does not)."
+ },
+ {
+ "term": "Claude (the model)",
+ "tldr": "Anthropic's LLM family. Three sizes — Haiku (fast/cheap), Sonnet (balanced), Opus (most capable).",
+ "detail": "Versions are dated/numbered (Claude 4.6, Claude 4.7). Each newer model is generally smarter but pricier. Norma uses Sonnet for most tasks and Opus for the hard ones (long-document analysis, multi-step reasoning).",
+ "example": "Claude Opus 4.7 (running THIS session) costs ~$15/$75 per million input/output tokens. Claude Haiku is ~$1/$5 per million."
+ },
+ {
+ "term": "Claude.ai",
+ "tldr": "The chat website. claude.ai is Anthropic's consumer product — the place a non-developer goes to chat with Claude.",
+ "detail": "Free tier exists. Pro ($20/mo) and Max ($100/mo or $200/mo) get bigger context, file uploads, Claude Code access. SDCC staff could use claude.ai for ad-hoc research without any setup."
+ },
+ {
+ "term": "Claude API",
+ "tldr": "The developer interface for Claude. Apps like Norma call this to do inference programmatically.",
+ "detail": "Pay-as-you-go pricing per token. Console at console.anthropic.com — where you get API keys. The key looks like `sk-ant-api03-...`. NEVER commit API keys to git."
+ },
+ {
+ "term": "Claude Code",
+ "tldr": "Anthropic's official command-line tool for Claude. Runs in your terminal, edits files, runs commands, builds whole projects.",
+ "detail": "What Steve uses to build Norma + this pitch viewer. Install with: `npm install -g @anthropic-ai/claude-code`, then run `claude` in any project directory. Authenticate via Claude Max subscription or API key.",
+ "example": "Type `claude` in a terminal → opens an interactive prompt → 'add a search bar to this page' → Claude reads the files, makes edits, runs tests."
+ },
+ {
+ "term": "Anthropic Console",
+ "tldr": "console.anthropic.com — where developers manage API keys, see usage, set up billing.",
+ "detail": "Also has a 'workbench' for trying prompts against the API without writing code. SDCC's API key would be issued from here."
+ },
+ {
+ "term": "Claude Projects (the feature)",
+ "tldr": "A feature inside claude.ai where you attach documents + give Claude persistent instructions for a recurring task.",
+ "detail": "Not the same as 'a Norma project'. Claude Projects = lightweight chat workspaces. Norma = structured, audited, integrated platform. Both can coexist — staff use claude.ai Projects for ad-hoc research; Norma handles the structured work."
+ },
+ {
+ "term": "Constitutional AI",
+ "tldr": "Anthropic's approach to making models safer — training them to follow a set of principles ('the constitution') instead of relying purely on human feedback.",
+ "detail": "Means Claude tends to be more cautious about harmful, dishonest, or extremist outputs. For SDCC: Claude won't help draft something defamatory or generate misleading content even if asked. Good for advocacy: less risk of producing material that backfires."
+ },
+ {
+ "term": "Anthropic responsible-use policy",
+ "tldr": "The legal/usage terms you sign when using Anthropic's API. Bans certain use cases (mass surveillance, deepfakes, election interference).",
+ "detail": "Norma's design respects these — no impersonation of public officials, no manipulating elections, full audit trail of who used what. Worth reviewing at anthropic.com/legal/aup before SDCC commits to scaled use."
+ }
+ ]
+ },
+ {
+ "id": "sdcc-llm",
+ "label": "The 'SDCC LLM' — what's actually under the hood",
+ "icon": "🎯",
+ "terms": [
+ {
+ "term": "The 'SDCC LLM'",
+ "tldr": "There isn't one — and that's intentional. 'SDCC's LLM' = Anthropic's Claude + SDCC's prompts + SDCC's data + SDCC's integrations.",
+ "detail": "The model itself is the same Claude that any other Anthropic customer uses. What makes SDCC's experience unique is the LAYER ON TOP — Norma. Norma carries SDCC's voice library (prior statements, talking points), SDCC's knowledge base (petition archive, borrower stories), SDCC's prompts (tuned for advocacy + nonprofit voice), and SDCC's integrations (Gmail, Slack, EveryAction if you connect it).",
+ "example": "When SDCC asks 'draft a press response to ED's announcement', Norma sends Claude: (1) a system prompt with SDCC's voice rules, (2) the top 5 most-relevant prior SDCC statements (retrieved via RAG), (3) the user's specific ask. Claude generates a response that sounds like SDCC because the prompt is loaded with SDCC context."
+ },
+ {
+ "term": "Why this matters",
+ "tldr": "SDCC keeps the upside (custom voice, knowledge moat, integrations) without the downside (training your own model = $millions and months).",
+ "detail": "Training an actual SDCC-specific model would cost $1M+ and take 6+ months. Plus you'd need to retrain when the world changes. The 'context layer' approach gives 90% of the benefit at 0.1% of the cost. As Claude improves, SDCC gets the improvements for free."
+ },
+ {
+ "term": "Voice library",
+ "tldr": "The collection of prior SDCC writing (statements, op-eds, petitions) Norma feeds Claude as style examples.",
+ "detail": "Curated by Natalia + Sabrina. Updated quarterly. Every draft passes through it so output sounds like SDCC, not generic-nonprofit-bot."
+ },
+ {
+ "term": "Knowledge base",
+ "tldr": "The structured data Norma can query — petitions, borrower stories, coalition org info, policy moves, news mentions.",
+ "detail": "Lives in Postgres on SDCC's tenancy. Indexed for fast search + embedded for semantic search. Grows over time as staff use Norma."
+ },
+ {
+ "term": "Tenant / Tenancy",
+ "tldr": "SDCC's isolated workspace inside Norma. SDCC's data is fully separate from any other org using Norma.",
+ "detail": "Multi-tenant architecture: one platform, many orgs, complete data separation. SDCC's borrower stories never appear in another org's search results — guaranteed at the database level, not just the UI."
+ }
+ ]
+ },
+ {
+ "id": "install-hookup",
+ "label": "Install + hook up Claude on your computer",
+ "icon": "🔌",
+ "terms": [
+ {
+ "term": "Step 1 — Install Node.js",
+ "tldr": "Norma + Claude Code both need Node.js 18+ to run.",
+ "detail": "Download from nodejs.org (LTS version) OR `brew install node` on macOS. Verify after install: `node --version` (should print v18+ or v20+), `npm --version` (should print 10+).",
+ "example": "$ node --version\nv20.10.0\n$ npm --version\n10.2.3"
+ },
+ {
+ "term": "Step 2 — Install Claude Code (the CLI)",
+ "tldr": "One command. Globally installed so you can run `claude` from any directory.",
+ "detail": "`npm install -g @anthropic-ai/claude-code`. The `-g` makes it global. After install, type `claude --version` to confirm. First time you run `claude` in a project, it'll ask you to sign in.",
+ "example": "$ npm install -g @anthropic-ai/claude-code\n$ claude --version\n2.1.145"
+ },
+ {
+ "term": "Step 3 — Authenticate Claude Code",
+ "tldr": "Two options: sign in with Claude Max (subscription) or paste an API key (pay-per-use).",
+ "detail": "Run `claude` in a terminal. First time: browser opens for Claude Max sign-in, OR it prompts for an API key. Pick Claude Max if you have a Max subscription ($100/mo gets you a lot of Claude Code usage). Pick API key if you want metered billing only."
+ },
+ {
+ "term": "Step 4 — Get an Anthropic API key",
+ "tldr": "If you're going API-key route: get one from console.anthropic.com.",
+ "detail": "Sign up at console.anthropic.com. Add billing info (need a credit card). API Keys → Create Key. Copy the `sk-ant-...` string. Paste it when Claude Code asks, OR set it as env var: `export ANTHROPIC_API_KEY='sk-ant-...'`."
+ },
+ {
+ "term": "Step 5 — Hook Claude to a project",
+ "tldr": "cd into the project directory. Run `claude`. That's the whole step.",
+ "detail": "Claude Code reads the current directory's CLAUDE.md (if any) for context, ignores files in .gitignore, and inherits your shell environment. No project-level setup beyond optionally writing a CLAUDE.md.",
+ "example": "$ cd ~/Projects/norma\n$ claude\nWelcome to Claude Code 2.1.145\n>"
+ },
+ {
+ "term": "Step 6 — Hook Norma to Anthropic",
+ "tldr": "Norma needs ANTHROPIC_API_KEY in its .env.local file.",
+ "detail": "Edit ~/Projects/Norma/.env.local. Add the line: `ANTHROPIC_API_KEY=sk-ant-...`. Restart the dev server (`npm run dev`). Norma will now use that key for every Claude call.",
+ "example": "$ echo 'ANTHROPIC_API_KEY=sk-ant-api03-...' >> ~/Projects/Norma/.env.local\n$ pkill -f 'next dev' && cd ~/Projects/Norma && npm run dev"
+ },
+ {
+ "term": "Step 7 — Verify it's hooked up",
+ "tldr": "Visit a route that uses Claude. If it returns real content, you're connected.",
+ "detail": "Try POSTing to `/api/agents/petition/generate` with a topic. If it generates real petition copy, the key is valid + Norma is talking to Anthropic. If it 500s with 'invalid API key' — your key is wrong. If it 503s — check rate limits/billing."
+ }
+ ]
+ },
+ {
+ "id": "mcp-protocol",
+ "label": "MCP — the protocol that connects Claude to tools",
+ "icon": "🔗",
+ "terms": [
+ {
+ "term": "MCP (Model Context Protocol)",
+ "tldr": "Anthropic's open protocol for connecting LLMs to external tools, data sources, and services. Think USB for AI.",
+ "detail": "Before MCP, every AI app reinvented its own way to give the model access to (Gmail, Slack, Postgres, file systems, etc). MCP standardizes that. Any 'MCP server' can plug into any 'MCP client' — Claude.ai, Claude Code, Norma, third-party AI apps. Open spec at modelcontextprotocol.io.",
+ "example": "If SDCC wanted to give Claude direct read access to its EveryAction CRM, you'd run an EveryAction MCP server and Claude would automatically discover its tools (lookup_donor, recent_donations, etc)."
+ },
+ {
+ "term": "MCP Server",
+ "tldr": "A program that exposes a set of tools/data to MCP clients. Runs on your machine or a remote host.",
+ "detail": "Steve has dozens of these — a Gmail MCP server, a domain-suite MCP server, a Cloudflare MCP server. Each exposes a few related tools (e.g. `gmail_send`, `gmail_list_messages`). Claude Code automatically loads them at startup."
+ },
+ {
+ "term": "MCP Client",
+ "tldr": "The app that USES MCP servers. Claude.ai, Claude Code, and Norma can all be MCP clients.",
+ "detail": "When a client starts, it connects to its configured MCP servers, discovers what tools each offers, and presents them to the LLM. The LLM can then decide to invoke them mid-conversation."
+ },
+ {
+ "term": "Tool (in MCP terms)",
+ "tldr": "A single function an MCP server exposes. Has a name, description, parameters, and a way to execute.",
+ "detail": "Example tool: `search_petitions(query: string, limit: int) -> Petition[]`. The MCP server defines it, the LLM decides when to call it."
+ },
+ {
+ "term": "Resource (in MCP terms)",
+ "tldr": "A piece of data an MCP server exposes (rather than a function). Like 'the latest news brief' or 'this Postgres table'.",
+ "detail": "Tools are verbs, resources are nouns. The LLM can read resources without 'calling' them."
+ }
+ ]
+ },
+ {
+ "id": "models-other",
+ "label": "Other LLMs you'll hear about",
+ "icon": "🤖",
+ "terms": [
+ {
+ "term": "Hermes (Nous Research)",
+ "tldr": "An open-weight LLM family from Nous Research. Run locally via Ollama as a free alternative to commercial APIs.",
+ "detail": "Hermes 3 ships in 8B / 70B / 405B parameter sizes. Smaller versions run on a laptop GPU; larger ones need real hardware. Used by some teams for sensitive data they don't want to send to commercial APIs. Norma DOES NOT use Hermes — Norma uses Claude. But you'll see it in the broader ecosystem and Steve's debate-team agents sometimes route through local Ollama models.",
+ "example": "Install Ollama (ollama.ai), then `ollama pull hermes3:8b`, then `ollama run hermes3:8b`. Runs entirely on your machine."
+ },
+ {
+ "term": "GPT (OpenAI)",
+ "tldr": "OpenAI's LLM family — GPT-4, GPT-4o, GPT-5. The biggest direct competitor to Claude.",
+ "detail": "Norma doesn't use GPT by default. Some Steve workflows do (the 'debate-team' agents put Claude + GPT-5-codex + Qwen in adversarial review). For SDCC, sticking to one provider (Anthropic/Claude) is the simpler default."
+ },
+ {
+ "term": "Gemini (Google)",
+ "tldr": "Google's LLM family. Strong on multi-modal (images, video) and long context.",
+ "detail": "Norma uses Gemini ONLY for image analysis (vision tasks where it's notably good). Text inference all goes through Claude."
+ },
+ {
+ "term": "Llama (Meta)",
+ "tldr": "Meta's open-weight LLM family. Llama 3, Llama 3.3.",
+ "detail": "Free to download + run locally. Foundation many other open models build on. Same caveats as Hermes — Norma uses Claude, but you'll see Llama in the wider ecosystem."
+ },
+ {
+ "term": "Qwen (Alibaba)",
+ "tldr": "Alibaba's open-weight LLM family. Strong on coding + multi-language.",
+ "detail": "Steve runs Qwen3:14B locally via Ollama as the 'free third opinion' in adversarial debate panels. Not part of SDCC's stack."
+ },
+ {
+ "term": "Ollama",
+ "tldr": "A tool for running open-weight LLMs (Llama, Hermes, Qwen, etc) locally on your machine.",
+ "detail": "Free. Install at ollama.ai. After install: `ollama pull llama3.3` then `ollama run llama3.3`. The model runs entirely offline on your machine. Slower than Claude API, but no per-token cost and no data leaves your computer."
+ }
+ ]
+ },
+ {
+ "id": "web-backend",
+ "label": "Web + backend basics",
+ "icon": "🌐",
+ "terms": [
+ {
+ "term": "API (Application Programming Interface)",
+ "tldr": "A contract that lets one program talk to another over the network. Send a request, get a response.",
+ "detail": "Modern APIs are usually REST or GraphQL. You send HTTP requests (GET, POST, PUT, DELETE) to URLs. Responses come back as JSON. Norma exposes 200+ API endpoints internally."
+ },
+ {
+ "term": "REST / RESTful",
+ "tldr": "A pattern for APIs based on HTTP verbs + URL paths. The default for most modern web APIs.",
+ "detail": "`GET /api/petitions` = list petitions. `POST /api/petitions` = create one. `GET /api/petitions/123` = fetch petition 123. `DELETE /api/petitions/123` = remove it. Predictable, simple, well-tooled."
+ },
+ {
+ "term": "HTTP / HTTPS",
+ "tldr": "The protocol the web is built on. HTTP = plaintext. HTTPS = encrypted (TLS). Always use HTTPS in production.",
+ "detail": "Status codes you'll see: 200 (ok), 301/307 (redirect), 400 (bad request), 401 (unauthenticated), 403 (forbidden), 404 (not found), 500 (server crash), 503 (overloaded)."
+ },
+ {
+ "term": "JSON",
+ "tldr": "The text format APIs send data in. Looks like JavaScript objects.",
+ "detail": "`{\"name\":\"SDCC\",\"founded\":2013}`. Universal across languages — every modern language can parse and produce JSON. Norma's APIs all send/receive JSON."
+ },
+ {
+ "term": "Endpoint",
+ "tldr": "A single URL on an API. Each endpoint usually does one thing.",
+ "detail": "`/api/petitions/topics` is an endpoint. `/api/agents/petition/generate` is another. Together they form 'the Norma API'."
+ },
+ {
+ "term": "Database",
+ "tldr": "Where your data persists between requests. The thing that survives server restarts.",
+ "detail": "Norma uses Postgres (Postgres = PostgreSQL = the same database). SDCC's borrower stories, petition data, audit logs all live in Postgres on the SDCC tenant."
+ },
+ {
+ "term": "PostgreSQL (Postgres)",
+ "tldr": "The relational database Norma uses. Open-source, battle-tested, fast.",
+ "detail": "Stores data in tables with rows and columns (like a spreadsheet, but with strict schemas and SQL queries). The `sdcc` database in this dev environment holds SDCC's data."
+ },
+ {
+ "term": "SQL (Structured Query Language)",
+ "tldr": "The language you use to ask the database questions.",
+ "detail": "`SELECT title FROM petitions WHERE status='active' ORDER BY created_at DESC LIMIT 5;` — that's SQL. Returns the 5 most recent active petitions."
+ },
+ {
+ "term": "Cookie / Session",
+ "tldr": "A bit of data the browser holds that proves you're logged in.",
+ "detail": "When you log into Norma, the server sends back a `Set-Cookie: norma-auth=...` header. Your browser sends that cookie back on every subsequent request. The server reads it to know you're admin / staff / pulse."
+ },
+ {
+ "term": "OAuth",
+ "tldr": "A standard protocol for 'sign in with Google' (or Gmail, or X, or anything else).",
+ "detail": "Norma uses OAuth for Gmail integration. Instead of asking for SDCC's Gmail password, it redirects to Google, has the user grant permission, and gets back a token. SDCC's password is never seen by Norma."
+ }
+ ]
+ },
+ {
+ "id": "norma-specifics",
+ "label": "Norma platform glossary",
+ "icon": "🛠️",
+ "terms": [
+ {
+ "term": "Norma (the platform)",
+ "tldr": "The multi-tenant nonprofit intelligence platform SDCC would license. Three tiers of access: admin, staff, pulse.",
+ "detail": "Built on Next.js 16 + Postgres + Tailwind. Runs at localhost:7400 in dev. In production it runs on cloud servers (e.g., Kamatera, AWS, GCP — SDCC picks)."
+ },
+ {
+ "term": "Pulse (the page)",
+ "tldr": "Norma's daily-briefing landing page at /pulse. The morning standup view.",
+ "detail": "Surfaces: today's petitions, active news cycles, urgent borrower stories, coalition activity. The 'open this first' page."
+ },
+ {
+ "term": "Tier credentials",
+ "tldr": "The three login roles in Norma. admin = full access. staff = SDCC team. pulse = end users (borrowers, public).",
+ "detail": "Stored in the `tier_credentials` table. Each row has username, hashed password, role, optional org_id. Steve uses bcrypt for new passwords; older ones get auto-upgraded."
+ },
+ {
+ "term": "Loop (Norma)",
+ "tldr": "An always-on background job that runs every N minutes/hours. Inbox scans, news ingestion, signature counting.",
+ "detail": "See section 4 of this viewer for full explanation."
+ },
+ {
+ "term": "Cron job (Norma)",
+ "tldr": "A scheduled task — runs on a calendar instead of a timer.",
+ "detail": "See section 5 of this viewer."
+ },
+ {
+ "term": "Audit trail",
+ "tldr": "Immutable log of every AI decision, send, approval. The compliance backbone.",
+ "detail": "Stored in append-only tables. Queryable by date, actor, action, resource. Whether a funder, board member, or regulator asks 'who did X?' — one query answers it."
+ },
+ {
+ "term": "Skill (in Steve's stack)",
+ "tldr": "A reusable bundle of instructions + helper scripts that Claude Code can invoke.",
+ "detail": "Steve has 200+ skills in ~/.claude/skills/. Examples: a `settlement` skill that legally vets wallpaper generations, a `george-inbox` skill that triages info@ mail. Skills are project-specific superpowers."
+ },
+ {
+ "term": "Agent (in Steve's stack)",
+ "tldr": "A specialized Claude sub-process spawned to handle one type of task with custom instructions + tools.",
+ "detail": "Steve has 50+ agents (in ~/.claude/agents/). Examples: `lawyer-build-agent` for the lawyer directory project, `doctor-agent` for the doctor directory. Each has its own system prompt + tool allowlist."
+ }
+ ]
+ },
+ {
+ "id": "hosting-deploy",
+ "label": "Hosting + deployment",
+ "icon": "☁️",
+ "terms": [
+ {
+ "term": "Server (cloud)",
+ "tldr": "A computer that's NOT yours, that runs your app 24/7 and serves it to the internet.",
+ "detail": "When SDCC actually adopts Norma, it runs on a cloud server — AWS, Google Cloud, Kamatera (Steve's preference), or similar. SDCC's borrower data lives on that server (in SDCC's tenancy, isolated from other orgs)."
+ },
+ {
+ "term": "Domain (DNS)",
+ "tldr": "The human-readable address. e.g. norma.studentdebtcrisis.org. DNS is what turns the domain into the server's IP address.",
+ "detail": "SDCC would point a subdomain (e.g., norma.studentdebtcrisis.org or pulse.studentdebtcrisis.org) at the production server."
+ },
+ {
+ "term": "SSL / TLS / HTTPS cert",
+ "tldr": "The encryption that makes the URL show a 🔒 padlock in the browser.",
+ "detail": "Let's Encrypt issues free SSL certs that auto-renew every 90 days. Critical for any site handling user data."
+ },
+ {
+ "term": "pm2",
+ "tldr": "A process manager that keeps Node.js apps running. Restarts them if they crash.",
+ "detail": "Norma runs under pm2 in production. `pm2 list` shows all running apps. `pm2 logs norma` tails the live log. `pm2 restart norma` reloads after a code change."
+ },
+ {
+ "term": "Docker / Container",
+ "tldr": "A way to package an app + everything it needs (libraries, configs) into a portable bundle.",
+ "detail": "Norma isn't dockerized today, but could be. Containers make 'works on my laptop, works on the server' actually true."
+ },
+ {
+ "term": "Deployment",
+ "tldr": "The process of taking new code from your laptop → onto the production server → live for users.",
+ "detail": "Typical flow: commit to git → push to GitHub → CI/CD pipeline (GitHub Actions) builds + tests + ships to server → users see the change. Steve uses a `deploy` skill that does rsync + pm2 reload + smoke-test."
+ }
+ ]
+ }
+ ]
+ },
+
"gamify": {
"xp_per_quiz": 25,
"xp_per_correct": 10,
"xp_per_section_complete": 50,
+ "xp_per_term_learned": 5,
"levels": [
{ "min": 0, "name": "Pitch Apprentice", "color": "#6b7280" },
{ "min": 100, "name": "Pitch Builder", "color": "#1b4d7a" },
@@ -523,7 +1276,9 @@
{ "id": "pitch_3", "name": "Three's the Pitch", "criteria": "Include 3+ agents in your pitch", "icon": "📋" },
{ "id": "skipper", "name": "Curator", "criteria": "Skip at least 2 agents (keeping pitch clean)", "icon": "✂️" },
{ "id": "asked_questions", "name": "Interviewer", "criteria": "Open the 'Questions to Ask' modal", "icon": "❓" },
- { "id": "unknown_aware", "name": "Self-Aware", "criteria": "Open the 'Unknown Unknowns' modal", "icon": "🔮" }
+ { "id": "unknown_aware", "name": "Self-Aware", "criteria": "Open the 'Unknown Unknowns' modal", "icon": "🔮" },
+ { "id": "vocab_starter", "name": "Vocab Starter", "criteria": "Mark 5 glossary terms as learned", "icon": "📖" },
+ { "id": "vocab_champ", "name": "Vocab Champ", "criteria": "Mark 20 glossary terms as learned", "icon": "📚" }
]
}
}
diff --git a/public/app.js b/public/app.js
index 5dd19af..ecdca5f 100644
--- a/public/app.js
+++ b/public/app.js
@@ -9,6 +9,8 @@
badges: new Set(),
quizState: {}, // section -> { questionsAnswered: [bool], correctCount, total }
currentAgentId: null,
+ learnedTerms: new Set(), // glossary terms marked as learned (by id "category::term")
+ glossFilter: { category: 'all', search: '' },
};
/* ─────────────── persistence ─────────────── */
@@ -19,6 +21,7 @@
xp: state.xp,
badges: Array.from(state.badges),
quizState: state.quizState,
+ learnedTerms: Array.from(state.learnedTerms),
}));
}
function load() {
@@ -30,6 +33,7 @@
state.xp = obj.xp || 0;
state.badges = new Set(obj.badges || []);
state.quizState = obj.quizState || {};
+ state.learnedTerms = new Set(obj.learnedTerms || []);
} catch (err) { /* ignore */ }
}
@@ -45,15 +49,22 @@
renderCronCards();
renderUiUxCards();
renderUiUxPriorities();
+ renderPriorities();
+ renderObjections();
+ renderShiftTable();
+ renderWixOptions();
+ renderWixPrinciples();
renderAgents();
renderQuizzes();
renderWalkthrough();
renderPitch();
+ renderGlossary();
renderHud();
wireFilters();
wirePitchActions();
wirePrepCards();
wireModalCloseHandlers();
+ wireGlossarySearch();
setupScrollProgress();
}
@@ -527,6 +538,228 @@ ${items.map((a) => `
update();
}
+ /* ─────────────── priorities ─────────────── */
+ function renderPriorities() {
+ const p = state.data.priorities;
+ if (!p) return;
+ const list = document.getElementById('priority-list');
+ if (!list) return;
+ list.innerHTML = p.items.map((it) => `
+ <article class="priority-card">
+ <div class="priority-rank">${it.rank}</div>
+ <div class="priority-body">
+ <span class="priority-icon-tag">${escapeHtml(it.icon || '')}</span>
+ <h3>${escapeHtml(it.title)}</h3>
+ <p class="pain"><span class="priority-section-label">Pain</span>${escapeHtml(it.the_pain)}</p>
+ <p class="solution"><span class="priority-section-label">Solution</span>${escapeHtml(it.the_solution)}</p>
+ ${it.how_norma_helps ? `<ul class="priority-howlist">${it.how_norma_helps.map((h) => `<li>${escapeHtml(h)}</li>`).join('')}</ul>` : ''}
+ ${it.wix_specific_steps ? `<ul class="priority-howlist">${it.wix_specific_steps.map((h) => `<li>${escapeHtml(h)}</li>`).join('')}</ul>` : ''}
+ ${it.before_after ? `
+ <div class="priority-before-after">
+ <div class="ba-cell before"><span class="ba-tag">Before</span>${escapeHtml(it.before_after.before)}</div>
+ <div class="ba-cell after"><span class="ba-tag">After</span>${escapeHtml(it.before_after.after)}</div>
+ </div>` : ''}
+ ${it.agents_skills ? `<div class="priority-agents">${it.agents_skills.map((a) => `<span class="agent-pill">${escapeHtml(a)}</span>`).join('')}</div>` : ''}
+ </div>
+ </article>
+ `).join('');
+ // wire agent pills to scroll to agent grid
+ list.querySelectorAll('.agent-pill').forEach((p) => {
+ p.addEventListener('click', () => {
+ document.querySelector('#agents').scrollIntoView({ behavior: 'smooth' });
+ });
+ });
+ }
+
+ /* ─────────────── reluctant adopter objections ─────────────── */
+ function renderObjections() {
+ const ra = state.data.reluctant_adopter;
+ if (!ra) return;
+ const list = document.getElementById('objections-list');
+ if (!list) return;
+ list.innerHTML = ra.objections.map((o) => `
+ <article class="objection-card">
+ <p class="obj-q">${escapeHtml(o.objection)}</p>
+ <p class="obj-a">${escapeHtml(o.answer)}</p>
+ ${o.before_after ? `
+ <div class="obj-ba">
+ <div class="ba-cell before"><span class="ba-tag">Before</span>${escapeHtml(o.before_after.before)}</div>
+ <div class="ba-cell after"><span class="ba-tag">After</span>${escapeHtml(o.before_after.after)}</div>
+ </div>` : ''}
+ </article>
+ `).join('');
+ }
+
+ /* ─────────────── shift table (companies are databases) ─────────────── */
+ function renderShiftTable() {
+ const c = state.data.companies_are_databases;
+ if (!c) return;
+ const t = document.getElementById('shift-table');
+ if (!t) return;
+ t.innerHTML = c.shift_table.map((row) => `
+ <tr>
+ <td class="before-col">${escapeHtml(row.before)}</td>
+ <td class="after-col">${escapeHtml(row.after)}</td>
+ </tr>
+ `).join('');
+ }
+
+ /* ─────────────── wix options + principles ─────────────── */
+ function renderWixOptions() {
+ const w = state.data.wix_integration;
+ if (!w) return;
+ const list = document.getElementById('wix-options');
+ if (!list) return;
+ list.innerHTML = w.options.map((o) => {
+ const diffClass = o.difficulty.toLowerCase().replace(/[^a-z]/g, '');
+ return `
+ <article class="wix-option-card">
+ <div class="wix-head">
+ <h4>${escapeHtml(o.name)}</h4>
+ <span class="wix-difficulty ${diffClass}">${escapeHtml(o.difficulty)}</span>
+ <span class="wix-time">${escapeHtml(o.time)}</span>
+ </div>
+ <ol class="wix-steps">${o.steps.map((s) => `<li>${escapeHtml(s)}</li>`).join('')}</ol>
+ </article>`;
+ }).join('');
+ }
+ function renderWixPrinciples() {
+ const w = state.data.wix_integration;
+ if (!w) return;
+ const list = document.getElementById('wix-principles');
+ if (!list) return;
+ list.innerHTML = w.wix_ux_ui_principles.map((p) => `<li><strong>${escapeHtml(p.title)}</strong> — ${escapeHtml(p.detail)}</li>`).join('');
+ }
+
+ /* ─────────────── glossary ─────────────── */
+ function termId(catId, term) { return catId + '::' + term; }
+ function totalGlossaryTerms() {
+ return state.data.glossary.categories.reduce((n, c) => n + c.terms.length, 0);
+ }
+
+ function renderGlossary() {
+ if (!state.data.glossary) return;
+ const gloss = state.data.glossary;
+ // chips
+ const chipsEl = document.getElementById('gloss-chips');
+ if (chipsEl) {
+ const total = totalGlossaryTerms();
+ chipsEl.innerHTML = `<button class="gloss-chip ${state.glossFilter.category === 'all' ? 'active' : ''}" data-cat="all">All <span class="gloss-chip-count">${total}</span></button>` +
+ gloss.categories.map((c) => `<button class="gloss-chip ${state.glossFilter.category === c.id ? 'active' : ''}" data-cat="${c.id}"><span>${c.icon}</span>${escapeHtml(c.label)} <span class="gloss-chip-count">${c.terms.length}</span></button>`).join('');
+ chipsEl.querySelectorAll('.gloss-chip').forEach((b) => {
+ b.addEventListener('click', () => {
+ state.glossFilter.category = b.getAttribute('data-cat');
+ renderGlossary();
+ });
+ });
+ }
+ // search input value persist
+ const searchEl = document.getElementById('gloss-search');
+ if (searchEl && state.glossFilter.search) searchEl.value = state.glossFilter.search;
+
+ // body
+ const list = document.getElementById('glossary-list');
+ const q = (state.glossFilter.search || '').trim().toLowerCase();
+ const cats = gloss.categories.filter((c) => state.glossFilter.category === 'all' || c.id === state.glossFilter.category);
+ let totalShown = 0;
+ const html = cats.map((c) => {
+ const matched = c.terms.filter((t) => {
+ if (!q) return true;
+ return (t.term + ' ' + t.tldr + ' ' + (t.detail || '')).toLowerCase().includes(q);
+ });
+ if (!matched.length) return '';
+ totalShown += matched.length;
+ return `
+ <div class="gloss-category" data-cat-id="${c.id}">
+ <div class="gloss-cat-header">
+ <span class="icon">${c.icon}</span>
+ <h3>${escapeHtml(c.label)}</h3>
+ <span class="cat-count">${matched.length} / ${c.terms.length}</span>
+ </div>
+ <div class="gloss-cat-body">
+ ${matched.map((t) => {
+ const id = termId(c.id, t.term);
+ const learned = state.learnedTerms.has(id);
+ return `
+ <div class="gloss-term ${learned ? 'learned' : ''}" data-term-id="${escapeHtml(id)}">
+ <div class="gloss-term-head">
+ <div>
+ <p class="gloss-term-name">${escapeHtml(t.term)}</p>
+ <p class="gloss-term-tldr">${escapeHtml(t.tldr)}</p>
+ </div>
+ <span class="gloss-term-toggle">▼</span>
+ </div>
+ <div class="gloss-term-detail">
+ <p>${escapeHtml(t.detail || '')}</p>
+ ${t.example ? `<div class="gloss-example">${escapeHtml(t.example)}</div>` : ''}
+ <div class="gloss-term-actions">
+ <button class="mark-learned" data-mark="${escapeHtml(id)}">${learned ? '✓ Learned' : 'Mark as learned (+5 XP)'}</button>
+ </div>
+ </div>
+ </div>`;
+ }).join('')}
+ </div>
+ </div>`;
+ }).join('');
+ list.innerHTML = html || `<div class="gloss-empty">No terms match “${escapeHtml(q)}”.</div>`;
+
+ // stats
+ const total = totalGlossaryTerms();
+ const learnedCount = state.learnedTerms.size;
+ const pct = Math.round((learnedCount / total) * 100);
+ const statsEl = document.getElementById('glossary-stats');
+ if (statsEl) {
+ statsEl.innerHTML = `
+ <div><strong>${learnedCount}</strong> of ${total} terms marked learned · ${totalShown} showing</div>
+ <div class="stats-progress"><div style="width:${pct}%"></div></div>
+ `;
+ }
+
+ // wire term clicks
+ list.querySelectorAll('.gloss-term-head').forEach((h) => {
+ h.addEventListener('click', () => {
+ h.closest('.gloss-term').classList.toggle('open');
+ });
+ });
+ list.querySelectorAll('.mark-learned').forEach((b) => {
+ b.addEventListener('click', (e) => {
+ e.stopPropagation();
+ const id = b.getAttribute('data-mark');
+ if (state.learnedTerms.has(id)) {
+ state.learnedTerms.delete(id);
+ } else {
+ state.learnedTerms.add(id);
+ addXp(state.data.gamify.xp_per_term_learned || 5, 'Term learned');
+ // badge checks
+ if (state.learnedTerms.size >= 5 && !state.badges.has('vocab_starter')) {
+ state.badges.add('vocab_starter');
+ toast('badge', '📖 Badge unlocked — Vocab Starter');
+ }
+ if (state.learnedTerms.size >= 20 && !state.badges.has('vocab_champ')) {
+ state.badges.add('vocab_champ');
+ toast('badge', '📚 Badge unlocked — Vocab Champ');
+ }
+ }
+ save();
+ renderGlossary();
+ renderHud();
+ });
+ });
+ }
+
+ function wireGlossarySearch() {
+ const s = document.getElementById('gloss-search');
+ if (!s) return;
+ let timer;
+ s.addEventListener('input', () => {
+ clearTimeout(timer);
+ timer = setTimeout(() => {
+ state.glossFilter.search = s.value;
+ renderGlossary();
+ }, 150);
+ });
+ }
+
/* ─────────────── refresh ─────────────── */
function refreshAll() {
save();
diff --git a/public/index.html b/public/index.html
index ec3a1ae..608c28b 100644
--- a/public/index.html
+++ b/public/index.html
@@ -16,15 +16,18 @@
<span class="brand-name">Norma <span class="muted">×</span> SDCC</span>
</div>
<nav>
- <a href="#why">1. Why AI</a>
- <a href="#long-short">2. Long/Short</a>
- <a href="#projects">3. Projects</a>
- <a href="#loops">4. Loops</a>
- <a href="#cron">5. Cron</a>
- <a href="#ui-ux">6. UI/UX</a>
- <a href="#agents">7. Agents</a>
- <a href="#example">8. Live Demo</a>
- <a href="#prep">9. Pitch Prep</a>
+ <a href="#priorities">⭐ Top 5</a>
+ <a href="#reluctant">Skeptic?</a>
+ <a href="#db-shift">DB Era</a>
+ <a href="#wix">Wix</a>
+ <a href="#why">Why AI</a>
+ <a href="#loops">Loops</a>
+ <a href="#cron">Cron</a>
+ <a href="#ui-ux">UI/UX</a>
+ <a href="#agents">Agents</a>
+ <a href="#example">Demo</a>
+ <a href="#prep">Pitch</a>
+ <a href="#glossary">Glossary</a>
</nav>
<div class="player-hud">
<div class="hud-xp" title="Earn XP by taking quizzes + running examples">
@@ -59,6 +62,70 @@
</div>
</section>
+<!-- ─────────── ⭐ TOP PRIORITIES ─────────── -->
+<section id="priorities" class="lecture priorities-section">
+ <div class="lecture-inner">
+ <span class="section-num gold">★</span>
+ <h2 data-bind="priorities.headline"></h2>
+ <p class="sub" data-bind="priorities.intro"></p>
+ <div class="priority-list" id="priority-list"></div>
+ </div>
+</section>
+
+<!-- ─────────── reluctant adopter ─────────── -->
+<section id="reluctant" class="lecture alt">
+ <div class="lecture-inner">
+ <span class="section-num" style="background:#b91c1c;">!</span>
+ <h2 data-bind="reluctant_adopter.headline"></h2>
+ <p class="sub" data-bind="reluctant_adopter.intro"></p>
+ <div class="tuesday-box">
+ <h3 data-bind="reluctant_adopter.your_tuesday.label"></h3>
+ <div class="tuesday-cols">
+ <div class="tuesday-col before">
+ <h4>Without Norma</h4>
+ <ul data-bind-list="reluctant_adopter.your_tuesday.before"></ul>
+ </div>
+ <div class="tuesday-col after">
+ <h4>With Norma</h4>
+ <ul data-bind-list="reluctant_adopter.your_tuesday.after"></ul>
+ </div>
+ </div>
+ <p class="tuesday-promise" data-bind="reluctant_adopter.the_promise"></p>
+ </div>
+ <h3 class="subsection">Common objections, answered</h3>
+ <div class="objections" id="objections-list"></div>
+ </div>
+</section>
+
+<!-- ─────────── companies are databases ─────────── -->
+<section id="db-shift" class="lecture">
+ <div class="lecture-inner">
+ <span class="section-num" style="background:#7c3aed;">DB</span>
+ <p class="kicker" data-bind="companies_are_databases.kicker"></p>
+ <h2 data-bind="companies_are_databases.headline"></h2>
+ <p class="lecture-para" data-bind="companies_are_databases.body" style="white-space:pre-line"></p>
+ <div class="callout callout-strategy">
+ <h3>What this means for SDCC</h3>
+ <p data-bind="companies_are_databases.implication_for_sdcc"></p>
+ </div>
+ <h3 class="subsection">The shift, in concrete terms</h3>
+ <table class="shift-table" id="shift-table"></table>
+ </div>
+</section>
+
+<!-- ─────────── wix integration ─────────── -->
+<section id="wix" class="lecture alt">
+ <div class="lecture-inner">
+ <span class="section-num" style="background:#0c66ff;">W</span>
+ <h2 data-bind="wix_integration.headline"></h2>
+ <p class="sub" data-bind="wix_integration.intro"></p>
+ <h3 class="subsection">Five integration options (ranked easy → hard)</h3>
+ <div class="wix-options" id="wix-options"></div>
+ <h3 class="subsection">Wix UX + UI principles for SDCC</h3>
+ <ul class="lecture-bullets compact" id="wix-principles"></ul>
+ </div>
+</section>
+
<!-- ─────────── 1. why ai ─────────── -->
<section id="why" class="lecture" data-section="why_ai">
<div class="lecture-inner">
@@ -250,6 +317,21 @@
</div>
</section>
+<!-- ─────────── 10. glossary / onboarding ─────────── -->
+<section id="glossary" class="lecture alt" data-section="glossary">
+ <div class="lecture-inner">
+ <span class="section-num">10</span>
+ <h2 data-bind="glossary.headline"></h2>
+ <p class="sub" data-bind="glossary.intro"></p>
+ <div class="glossary-controls">
+ <input type="search" id="gloss-search" placeholder="Search terms — try 'mcp', 'token', 'iterm', 'rag'…" autocomplete="off">
+ <div class="gloss-chips" id="gloss-chips"></div>
+ </div>
+ <div class="glossary-stats" id="glossary-stats"></div>
+ <div class="glossary-list" id="glossary-list"></div>
+ </div>
+</section>
+
<footer>
<div class="footer-inner">
<div>Norma × SDCC pitch viewer · localhost:9876 · backing Norma at <code>localhost:7400</code></div>
diff --git a/public/styles.css b/public/styles.css
index ef0fd3f..8f372d9 100644
--- a/public/styles.css
+++ b/public/styles.css
@@ -515,6 +515,254 @@ code { font-family: 'SF Mono', Menlo, Consolas, monospace; font-size: .85em; bac
to { opacity: 0; transform: translateY(20px) scale(.95); }
}
+/* ────── section-num color variants ────── */
+.section-num.gold { background: linear-gradient(135deg, var(--sdcc-amber), var(--sdcc-amber-deep)); }
+
+/* ────── priorities ────── */
+.priorities-section {
+ background:
+ radial-gradient(800px 400px at 20% -10%, rgba(245,158,11,.10), transparent 70%),
+ linear-gradient(180deg, var(--warn-soft), #fff);
+}
+.priority-list { display: grid; gap: 22px; }
+.priority-card {
+ background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius-lg);
+ padding: 26px 28px; box-shadow: var(--shadow-sm); transition: all .2s;
+ display: grid; grid-template-columns: 80px 1fr; gap: 20px;
+}
+.priority-card:hover { box-shadow: var(--shadow-md); transform: translateY(-2px); }
+.priority-rank {
+ width: 72px; height: 72px; border-radius: 50%;
+ background: linear-gradient(135deg, var(--sdcc-amber), var(--sdcc-amber-deep));
+ color: #fff; font-size: 32px;
+ display: flex; align-items: center; justify-content: center;
+ font-family: var(--font-serif); font-weight: 700;
+ box-shadow: 0 6px 16px rgba(245,158,11,.30);
+}
+.priority-icon-tag {
+ position: absolute; top: 24px; right: 28px;
+ font-size: 38px; opacity: .85;
+}
+.priority-body { position: relative; padding-right: 60px; }
+.priority-body h3 { font-family: var(--font-serif); font-size: 22px; color: var(--sdcc-blue-deep); margin: 0 0 4px; }
+.priority-body .pain { color: var(--ink-2); font-size: 15px; margin: 12px 0 4px; }
+.priority-body .pain strong { color: var(--danger); }
+.priority-body .solution { color: var(--ink); font-size: 15px; margin: 0 0 14px; }
+.priority-body .solution strong { color: var(--good); }
+.priority-section-label {
+ display: inline-block; background: var(--border-soft); color: var(--muted);
+ padding: 2px 10px; border-radius: 4px; font-size: 11px;
+ font-weight: 700; text-transform: uppercase; letter-spacing: 1px; margin-right: 6px;
+}
+.priority-howlist { margin: 4px 0 14px; padding: 0 0 0 18px; font-size: 14px; color: var(--ink-2); }
+.priority-howlist li { margin: 4px 0; }
+.priority-before-after {
+ display: grid; grid-template-columns: 1fr 1fr; gap: 10px; margin: 16px 0 12px;
+ border-top: 1px solid var(--border-soft); padding-top: 14px;
+}
+@media (max-width: 640px) { .priority-before-after { grid-template-columns: 1fr; } }
+.ba-cell { padding: 12px 14px; border-radius: 8px; font-size: 14px; line-height: 1.5; }
+.ba-cell.before { background: var(--danger-soft); border-left: 3px solid var(--danger); color: #7f1d1d; }
+.ba-cell.after { background: var(--good-soft); border-left: 3px solid var(--good); color: #064e3b; }
+.ba-cell .ba-tag { font-weight: 700; font-size: 11px; text-transform: uppercase; letter-spacing: 1px; margin-bottom: 4px; display: block; }
+.priority-agents {
+ display: flex; gap: 6px; flex-wrap: wrap; margin-top: 6px;
+}
+.priority-agents .agent-pill {
+ background: var(--info-soft); color: var(--info); padding: 3px 10px; border-radius: 999px;
+ font-size: 12px; font-weight: 500; cursor: pointer;
+}
+.priority-agents .agent-pill:hover { background: var(--info); color: #fff; }
+@media (max-width: 720px) {
+ .priority-card { grid-template-columns: 1fr; }
+ .priority-rank { width: 56px; height: 56px; font-size: 24px; }
+}
+
+/* ────── reluctant adopter ────── */
+.tuesday-box {
+ background: linear-gradient(180deg, var(--surface), #f8fafc);
+ border: 1px solid var(--border); border-radius: var(--radius-lg);
+ padding: 26px 28px; margin-bottom: 30px;
+}
+.tuesday-box > h3 { margin: 0 0 18px; font-family: var(--font-serif); font-size: 20px; color: var(--ink); }
+.tuesday-cols { display: grid; grid-template-columns: 1fr 1fr; gap: 16px; }
+@media (max-width: 700px) { .tuesday-cols { grid-template-columns: 1fr; } }
+.tuesday-col { padding: 18px 20px; border-radius: var(--radius); border: 1px solid var(--border); }
+.tuesday-col.before { background: var(--danger-soft); border-color: #fecaca; }
+.tuesday-col.after { background: var(--good-soft); border-color: #a7f3d0; }
+.tuesday-col h4 { margin: 0 0 12px; font-size: 14px; text-transform: uppercase; letter-spacing: 1px; }
+.tuesday-col.before h4 { color: var(--danger); }
+.tuesday-col.after h4 { color: var(--good); }
+.tuesday-col ul { margin: 0; padding-left: 20px; }
+.tuesday-col li { margin: 8px 0; font-size: 14.5px; color: var(--ink-2); line-height: 1.55; }
+.tuesday-promise {
+ margin: 20px 0 0; padding: 14px 18px;
+ background: var(--ink); color: #fff; border-radius: 8px;
+ font-size: 16px; line-height: 1.55; text-align: center; font-family: var(--font-serif); font-style: italic;
+}
+
+.objections { display: grid; gap: 14px; }
+.objection-card {
+ background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius);
+ padding: 20px 22px; box-shadow: var(--shadow-sm);
+}
+.objection-card .obj-q {
+ font-family: var(--font-serif); font-size: 18px; color: var(--danger);
+ margin: 0 0 10px; padding-left: 32px; position: relative;
+}
+.objection-card .obj-q::before {
+ content: "❝"; position: absolute; left: 0; top: -4px;
+ font-size: 28px; color: var(--danger); opacity: .5;
+}
+.objection-card .obj-a { color: var(--ink-2); font-size: 15px; margin: 0 0 14px; line-height: 1.6; }
+.objection-card .obj-ba { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; }
+@media (max-width: 600px) { .objection-card .obj-ba { grid-template-columns: 1fr; } }
+.objection-card .obj-ba .ba-cell { font-size: 13px; padding: 10px 12px; }
+
+/* ────── companies are databases / shift table ────── */
+.shift-table {
+ width: 100%; border-collapse: separate; border-spacing: 0;
+ background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius);
+ overflow: hidden;
+}
+.shift-table tr:not(:last-child) td { border-bottom: 1px solid var(--border-soft); }
+.shift-table td { padding: 14px 18px; font-size: 15px; vertical-align: top; }
+.shift-table td.before-col {
+ background: var(--danger-soft); color: #7f1d1d; width: 50%;
+ border-right: 1px solid #fecaca; position: relative;
+}
+.shift-table td.before-col::after {
+ content: "→"; position: absolute; right: -12px; top: 50%; transform: translateY(-50%);
+ background: #fff; padding: 0 6px; color: var(--ink); font-weight: 700; font-size: 18px;
+}
+.shift-table td.after-col { background: var(--good-soft); color: #064e3b; }
+
+/* ────── wix options ────── */
+.wix-options { display: grid; gap: 14px; margin-bottom: 28px; }
+.wix-option-card {
+ background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius);
+ padding: 20px 22px; box-shadow: var(--shadow-sm);
+}
+.wix-option-card .wix-head {
+ display: flex; justify-content: space-between; align-items: center; gap: 12px;
+ margin-bottom: 12px; flex-wrap: wrap;
+}
+.wix-option-card h4 {
+ margin: 0; font-family: var(--font-serif); font-size: 18px; color: var(--sdcc-blue-deep); flex: 1;
+}
+.wix-difficulty {
+ background: var(--info-soft); color: var(--info); padding: 3px 10px;
+ border-radius: 999px; font-size: 11px; font-weight: 700;
+ text-transform: uppercase; letter-spacing: 1px;
+}
+.wix-difficulty.medium { background: var(--warn-soft); color: var(--warn); }
+.wix-difficulty.harder { background: var(--danger-soft); color: var(--danger); }
+.wix-time { font-size: 13px; color: var(--muted); }
+.wix-steps {
+ list-style: decimal; padding-left: 22px; margin: 0;
+}
+.wix-steps li {
+ margin: 8px 0; font-size: 14.5px; color: var(--ink-2); line-height: 1.6;
+}
+.wix-steps li code {
+ background: #0e1729; color: #e8eef7; padding: 1px 6px; border-radius: 3px;
+ font-size: 12.5px;
+}
+
+/* ────── glossary ────── */
+.glossary-controls {
+ display: flex; gap: 16px; align-items: center; flex-wrap: wrap;
+ background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius);
+ padding: 16px 18px; margin-bottom: 18px; position: sticky; top: 64px; z-index: 5;
+ box-shadow: var(--shadow-sm);
+}
+#gloss-search {
+ flex: 1; min-width: 240px;
+ padding: 10px 14px; border: 1px solid var(--border); border-radius: 8px;
+ font-family: inherit; font-size: 14px; background: var(--bg);
+}
+#gloss-search:focus { outline: none; border-color: var(--sdcc-blue); background: #fff; }
+.gloss-chips { display: flex; gap: 6px; flex-wrap: wrap; }
+.gloss-chip {
+ background: var(--bg); border: 1px solid var(--border); color: var(--ink-2);
+ padding: 6px 12px; font-size: 12px; border-radius: 999px; cursor: pointer;
+ display: inline-flex; align-items: center; gap: 4px; transition: all .12s;
+ font-family: inherit;
+}
+.gloss-chip:hover { border-color: var(--sdcc-blue); color: var(--sdcc-blue); }
+.gloss-chip.active { background: var(--sdcc-blue); color: #fff; border-color: var(--sdcc-blue); }
+.gloss-chip .gloss-chip-count {
+ background: rgba(255,255,255,.25); padding: 1px 6px; border-radius: 4px;
+ font-size: 11px; font-weight: 600;
+}
+.gloss-chip:not(.active) .gloss-chip-count { background: var(--border-soft); color: var(--muted); }
+.glossary-stats {
+ font-size: 13px; color: var(--muted); margin-bottom: 14px;
+ display: flex; justify-content: space-between; align-items: center; gap: 12px;
+}
+.glossary-stats .stats-progress {
+ background: var(--border-soft); height: 6px; border-radius: 999px;
+ flex: 1; max-width: 320px; overflow: hidden;
+}
+.glossary-stats .stats-progress > div {
+ background: linear-gradient(90deg, var(--sdcc-amber), var(--sdcc-amber-deep));
+ height: 100%; width: 0%; transition: width .4s ease;
+}
+
+.gloss-category {
+ background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius);
+ margin-bottom: 16px; overflow: hidden;
+}
+.gloss-cat-header {
+ padding: 16px 20px; background: linear-gradient(180deg, #f8fafc, #fff);
+ border-bottom: 1px solid var(--border-soft);
+ display: flex; align-items: center; gap: 12px;
+}
+.gloss-cat-header .icon { font-size: 22px; }
+.gloss-cat-header h3 { margin: 0; font-size: 17px; color: var(--sdcc-blue-deep); flex: 1; }
+.gloss-cat-header .cat-count { font-size: 12px; color: var(--muted); }
+.gloss-cat-body { padding: 14px 20px 20px; display: grid; gap: 10px; }
+
+.gloss-term {
+ border: 1px solid var(--border); border-radius: 10px; background: var(--bg);
+ padding: 14px 18px; transition: all .15s; position: relative;
+}
+.gloss-term:hover { border-color: var(--sdcc-blue); }
+.gloss-term.learned { background: var(--good-soft); border-color: #a7f3d0; }
+.gloss-term.learned::after {
+ content: "✓ learned"; position: absolute; top: 12px; right: 16px;
+ background: var(--good); color: #fff; padding: 2px 8px; border-radius: 999px;
+ font-size: 11px; font-weight: 600;
+}
+.gloss-term-head {
+ cursor: pointer; display: flex; align-items: center; justify-content: space-between; gap: 12px;
+ padding-right: 90px;
+}
+.gloss-term-name { font-weight: 700; font-size: 15px; color: var(--ink); margin: 0; }
+.gloss-term-tldr { color: var(--ink-2); font-size: 14px; margin: 4px 0 0; }
+.gloss-term-detail {
+ display: none; padding-top: 12px; margin-top: 12px; border-top: 1px solid var(--border-soft);
+ font-size: 14px; color: var(--ink-2); line-height: 1.7;
+}
+.gloss-term-detail .gloss-example {
+ background: #0e1729; color: #e8eef7; padding: 10px 14px; border-radius: 8px;
+ margin-top: 10px; font-family: 'SF Mono', Menlo, Consolas, monospace; font-size: 12px;
+ white-space: pre-wrap; line-height: 1.6;
+}
+.gloss-term.open .gloss-term-detail { display: block; }
+.gloss-term.open .gloss-term-toggle { transform: rotate(180deg); }
+.gloss-term-toggle { color: var(--muted); font-size: 14px; transition: transform .2s; }
+.gloss-term-actions { display: flex; gap: 8px; margin-top: 10px; }
+.gloss-term-actions button {
+ background: var(--surface); border: 1px solid var(--border); color: var(--ink-2);
+ padding: 5px 12px; font-size: 12px; border-radius: 6px; cursor: pointer; font-family: inherit;
+}
+.gloss-term-actions button:hover { border-color: var(--sdcc-blue); color: var(--sdcc-blue); }
+.gloss-term-actions .mark-learned { background: var(--sdcc-blue); color: #fff; border-color: var(--sdcc-blue); }
+.gloss-term-actions .mark-learned:hover { background: var(--sdcc-blue-deep); color: #fff; }
+.gloss-term.learned .mark-learned { background: var(--good); border-color: var(--good); }
+.gloss-empty { padding: 32px; text-align: center; color: var(--muted); font-style: italic; }
+
/* ────── footer ────── */
footer { border-top: 1px solid var(--border); background: var(--surface); padding: 26px 32px; margin-top: 40px; }
.footer-inner { max-width: 1100px; margin: 0 auto; font-size: 13px; color: var(--muted); display: flex; flex-direction: column; gap: 6px; }
← 43cd061 initial scaffold — lecture-style SDCC pitch viewer with quiz
·
back to Norma Sdcc Pitch
·
add: 'Why custom CRM?' section — head-to-head vs Salesforce/ 16362c0 →