# Project export: Bramble

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

## Project metadata

- Hackathon: TreeHacks 2026
- Tagline: Shop wild, shop local
- Devpost: https://devpost.com/software/bramble-4cegjm
- GitHub: https://github.com/JosePerezIV/bramble
- Video: https://www.youtube.com/embed/w6dZJnNeMos?enablejsapi=1&hl=en_US&rel=0&start=&version=3&wmode=transparent
- Team: 2 GitHub contributor(s) — JosePerezIV (48 commits), Claude Sonnet 4.5 (25 commits)

## Devpost submission (written by the team)

### Inspiration

I heard a co-host on the WAN Show mention wanting a browser extension that helps you avoid mega-corporations and find local alternatives. I thought it sounded like a fun hackathon project, so I built it.

### What it does

Bramble is a Chrome extension that works when you're shopping on Amazon, Walmart, Target, or Best Buy. It identifies who actually owns the brand you're looking at, shows you local stores nearby that might carry the item, and suggests small online retailers as alternatives. You can set which brands you want to avoid and see everything on a map with directions. How I built it It's a Chrome extension using Claude API to analyze products and identify parent companies, Google Places API to find local stores, Brave Search API to find small online retailers, and Leaflet for the interactive map. The background service worker handles all the API calls to avoid CORS issues. I used Claude Code to speed up development during the 24-hour time crunch. Challenges Google and Bing both deprecated their web search APIs right when I needed them, so I had to switch to Brave Search API. Getting the extension to work across different retail sites was tricky since they all use different page structures. The biggest challenge was keeping it honest. Early versions just showed nearby stores without knowing if they had the product, which felt misleading. I changed it to say "call to confirm" instead of pretending to have data I don't have. What I learned I learned how Chrome extensions actually work, how to handle API calls in browser extensions without CORS errors, and how to integrate multiple APIs into one project. I also learned that building something mission-driven means saying no to shortcuts, like suggesting Target as an "alternative" to Amazon when they're both mega-corporations.

### What's next

Add real inventory data where possible, let users submit their own local store recommendations, and maybe expand to other browsers. The goal is to make it actually easier to shop locally instead of just telling people they should.

## README (from the GitHub repository)

# Bramble 🌿

Shop independently - find local alternatives to mega-corporations.

A Chrome extension that helps you discover local businesses and ethical alternatives when shopping on major retail sites.

## Features

- **Product Detection**: Automatically detects products on Amazon, Walmart, Target, and Best Buy
- **Side Panel UI**: Beautiful, collapsible side panel with smooth animations
- **Company Analysis**: Identifies parent companies and provides ethical scores
- **Alternative Recommendations**: Suggests local businesses, sustainable options, and fair-trade alternatives
- **Cost-Benefit Analysis**: Shows the positive impact of choosing ethical alternatives
- **Customizable Settings**: Toggle preferences for local businesses, avoiding specific corporations, and sustainable products
- **Impact Tracking**: Keep track of your ethical shopping contributions

## Installation

### Development Mode

1. Clone or download this repository
2. Open Chrome and navigate to `chrome://extensions/`
3. Enable "Developer mode" in the top right corner
4. Click "Load unpacked"
5. Select the `treehacks-project` directory
6. The Bramble extension is now installed!

### Using the Extension

1. Visit any product page on:
   - Amazon.com
   - Walmart.com
   - Target.com
   - BestBuy.com

2. Wait for the extension to detect the product (you'll see a green checkmark badge)

3. Click the floating toggle button on the right side of the page to open the side panel

4. Browse ethical alternatives and make informed decisions!

5. Click the extension icon in the toolbar to customize your preferences

## File Structure

```
treehacks-project/
├── manifest.json          # Extension manifest (Manifest V3)
├── content.js            # Content script for product detection
├── sidepanel.html        # Side panel UI structure
├── sidepanel.js          # Side panel controller
├── popup.html            # Extension settings popup
├── popup.js              # Popup controller
├── background.js         # Service worker for messaging
├── styles.css            # Cohesive design system
├── icons/
│   ├── icon16.png        # 16x16 icon
│   ├── icon48.png        # 48x48 icon
│   └── icon128.png       # 128x128 icon
└── README.md            # This file
```

## Design System

Bramble uses a cohesive earth-tone color palette:

- **Primary Green**: `#7ba05b` - Main brand color
- **Dark Green**: `#5d7e47` - Hover states and emphasis
- **Light Green**: `#9cb87f` - Accents
- **Cream**: `#f5f3ed` - Background
- **Warm Gray**: `#d4d0c8` - Borders and subtle elements
- **Text Dark**: `#2d4a2b` - Primary text

## Settings

Configure your preferences in the extension popup:

- **🏪 Support Local Businesses** - Prioritize local shops in recommendations
- **🌱 Sustainable Products** - Prioritize eco-friendly alternatives
- **🚫 Brands to Avoid** - Add any brands or companies you want to avoid (e.g., Amazon, Nestlé, Walmart, etc.)

## Future Enhancements

- Integration with real alternative product databases
- User reviews and ratings for alternatives
- Price comparison tools
- Carbon footprint calculator
- Community-sourced alternatives
- Browser extension for Firefox
- Mobile app version

## Technology Stack

- **Manifest V3**: Latest Chrome extension standard
- **Vanilla JavaScript**: Modern ES6+ syntax
- **CSS3**: Animations and responsive design
- **Chrome Storage API**: Settings and data persistence
- **Chrome Messaging API**: Inter-component communication

## Privacy

Bramble respects your privacy:
- No data collection or tracking
- No external API calls (in current version)
- All data stored locally on your device
- Settings synced via Chrome Sync (optional)
- Open source and transparent

## Contributing

Contributions are welcome! Please feel free to submit issues or pull requests.

## License

MIT License - Feel free to use and modify as needed.

## Credits

Created for TreeHacks 2026 🌲

---

**Note**: This is a prototype with placeholder data for alternatives. In production, this would integrate with real databases of ethical businesses and product alternatives.


## Detected evidence (automated analysis)

Indexed codebase: 12 recognized source files, 312 KB.
- CSS (language) — detected in the code
- HTML (language) — detected in the code
- JavaScript (language) — detected in the code
- AI coding agent: Claude Code — evidence: commit authorship or trailers

## Codebase structure (from repository index)

### Files (14 of 14)

```
.gitignore
api.js
background.js
config.example.js
content.js
lib/leaflet.css
lib/leaflet.js
manifest.json
popup.html
popup.js
README.md
sidepanel.html
styles.css
utils.js
```

### Dependencies

No dependency index available.

### Recent commits (newest first)

- Fix values match: Robust brand matching + better debugging
- Major performance boost: Show company analysis instantly
- Performance + UX: Faster analysis with Haiku + fix values match for avoided brands
- Rebrand: Vinegar → Bramble - Shop Wild, Shop Local
- Fix: Add chrome.storage safety checks to prevent undefined errors
- Simplify stats: Remove CO2 and local economy, keep only alternatives viewed
- Implement preference toggles: Support Local & Sustainable Products
- Add debug logging to diagnose filter issue
- Stricter filtering: Block generic eco shops and irrelevant results
- Fix price scraping: Handle cents correctly + better filtering
- Fix console errors: Silence tab-related errors and attempt CORS fix
- Quality over quantity: Limit to top 5 alternatives with better filtering
- Fix formatting: Truncate long descriptions from search results
- Fix CORS issue: Add Brave Search API to host_permissions
- Switch to Brave Search API (Bing was decommissioned)
- Switch from Google Custom Search to Bing Web Search API
- Implement real data strategy: web search + price scraping (NO HALLUCINATION)
- Major UX fixes: location prompt, filter big box retailers, fix undefined
- Fix 'undefined' showing in alternative address
- Fix critical issues: filter current store and improve price detection

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

### config.example.js

```javascript
/**
 * Bramble Configuration Template
 * Copy this file to config.js and add your actual API key.
 */

const CONFIG = {
  ANTHROPIC_API_KEY: "your-api-key-here"
};

```

### sidepanel.html

```html
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Bramble Side Panel</title>
  <!-- Leaflet will be injected dynamically by content.js -->
</head>
<body>
  <div id="vinegar-panel" class="vinegar-panel">
    <!-- Toggle button -->
    <button id="vinegar-toggle" class="vinegar-toggle" aria-label="Toggle Bramble panel">
      <svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
        <path d="M7 8h10M7 12h10M7 16h10"/>
      </svg>
    </button>

    <!-- Panel content -->
    <div id="vinegar-content" class="vinegar-content">
      <!-- Header -->
      <div class="vinegar-header">
        <h2 class="vinegar-title">🌿 Bramble</h2>
        <p class="vinegar-subtitle">Shop Wild, Shop Local</p>
      </div>

      <!-- Product Info Section -->
      <div class="vinegar-section">
        <h3 class="vinegar-section-title">Current Product</h3>
        <div id="product-info" class="vinegar-product-info">
          <div class="product-name"></div>
          <div class="product-price"></div>
          <div class="product-site"></div>
        </div>
      </div>

      <!-- Parent Company Section -->
      <div class="vinegar-section">
        <h3 class="vinegar-section-title">Company Analysis</h3>
        <div id="company-info" class="vinegar-company-info">
          <div class="company-badge">
            <span class="badge-icon">🏢</span>
            <span class="badge-text">Parent Company: <strong id="parent-company">Amazon Inc.</strong></span>
          </div>
          <div class="ethical-score">
            <span class="score-label">Ethical Score:</span>
            <div class="score-bar">
              <div class="score-fill" id="ethical-score-fill" style="width: 35%"></div>
            </div>
            <span class="score-value" id="ethical-score-value">35/100</span>
          </div>
        </div>
      </div>

      <!-- Alternatives Section -->
      <div class="vinegar-section">
        <h3 class="vinegar-section-title">Ethical Alternatives</h3>
        <div id="alternatives-list" class="vinegar-alternatives">
          <!-- Alternatives will be dynamically inserted here -->
        </div>

        <!-- Map Toggle Button -->
        <button id="toggle-map-btn" class="toggle-map-btn" style="display: none;">
          <span id="map-btn-icon">🗺️</span>
          <span id="map-btn-text">Show Nearby Alternatives</span>
        </button>
      </div>

      <!-- Map Section -->
      <div id="map-section" class="vinegar-section map-section" style="display: none;">
        <h3 class="vinegar-section-title">Nearby Alternatives Map</h3>
        <div id="alternatives-map" class="alternatives-map"></div>
        <div id="map-no-location" class="map-no-location" style="display: none;">
          <span class="map-icon">📍</span>
          <p>Enable location to see map of nearby alternatives</p>
        </div>
      </div>

      <!-- Cost Benefit Analysis -->
      <div class="vinegar-section">
        <h3 class="vinegar-section-title">Cost-Benefit Analysis</h3>
        <div id="cost-benefit" class="vinegar-cost-benefit">
          <div class="benefit-item">
            <span class="benefit-icon">💰</span>
            <span class="benefit-text">Support local economy</span>
          </div>
          <div class="benefit-item">
            <span class="benefit-icon">🌱</span>
            <span class="benefit-text">Reduced carbon footprint</span>
          </div>
          <div class="benefit-item">
            <span class="benefit-icon">🤝</span>
            <span class="benefit-text">Fair labor practices</span>
          </div>
          <div class="benefit-item">
            <span class="benefit-icon">📦</span>
            <span class="benefit-text">Sustainable packaging</span>
          </div>
        </div>
      </div>

      <!-- Loading state -->
      <div id="loading-state" class="vinegar-loading" style="display: none;">
        <div class="spinner"></div>
        <p>Finding ethical alternatives...</p>
      </div>
    </div>
  </div>
</body>
</html>

```

### utils.js

```javascript
/**
 * Bramble Utility Functions
 * Distance calculations and location helpers
 */

/**
 * Calculate distance between two coordinates using Haversine formula
 * @param {number} lat1 - Latitude of first point
 * @param {number} lon1 - Longitude of first point
 * @param {number} lat2 - Latitude of second point
 * @param {number} lon2 - Longitude of second point
 * @returns {number} Distance in miles
 */
function calculateDistance(lat1, lon1, lat2, lon2) {
  const R = 3959; // Radius of Earth in miles
  const dLat = toRadians(lat2 - lat1);
  const dLon = toRadians(lon2 - lon1);

  const a =
    Math.sin(dLat / 2) * Math.sin(dLat / 2) +
    Math.cos(toRadians(lat1)) *
      Math.cos(toRadians(lat2)) *
      Math.sin(dLon / 2) *
      Math.sin(dLon / 2);

  const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
  const distance = R * c;

  return distance;
}

/**
 * Convert degrees to radians
 */
function toRadians(degrees) {
  return degrees * (Math.PI / 180);
}

/**
 * Categorize distance into ranges
 * @param {number} distance - Distance in miles
 * @returns {string} Distance category
 */
function categorizeDistance(distance) {
  if (distance < 5) {
    return '< 5 mi';
  } else if (distance < 20) {
    return '5-20 mi';
  } else if (distance < 50) {
    return '20-50 mi';
  } else {
    return '> 50 mi';
  }
}

/**
 * Get formatted distance string
 * @param {number} distance - Distance in miles
 * @returns {string} Formatted distance (e.g., "3.2 mi", "45.7 mi")
 */
function formatDistance(distance) {
  if (distance < 0.1) {
    return '< 0.1 mi';
  }
  return `${distance.toFixed(1)} mi`;
}

/**
 * Calculate ethical score bonus based on distance
 * @param {number} distance - Distance in miles
 * @returns {number} Score bonus (0-20 points)
 */
function getDistanceBonus(distance) {
  if (distance < 10) {
    return 20;
  } else if (distance < 25) {
    return 10;
  }
  return 0;
}

/**
 * Get rough location description from coordinates
 * In production, this would use a reverse geocoding API
 * For now, just returns formatted coordinates
 * @param {number} lat - Latitude
 * @param {number} lon - Longitude
 * @returns {string} Location description
 */
function getLocationDescription(lat, lon) {
  // Format coordinates nicely
  const latDir = lat >= 0 ? 'N' : 'S';
  const lonDir = lon >= 0 ? 'E' : 'W';

  return `${Math.abs(lat).toFixed(2)}°${latDir}, ${Math.abs(lon).toFixed(2)}°${lonDir}`;
}

/**
 * Estimate city/state from coordinates (mock implementation)
 * In production, this would use a reverse geocoding API like Google Maps or Nominatim
 * For now, returns rough estimates based on known coordinates
 * @param {number} lat - Latitude
 * @param {number} lon - Longitude
 * @returns {string} City, State
 */
function estimateCityState(lat, lon) {
  // Mock implementation - in production, use reverse geocoding API
  // This is just for demonstration purposes

  // San Francisco Bay Area
  if (lat > 37 && lat < 38 && lon > -123 && lon < -121) {
    return 'San Francisco, CA';
  }
  // Los Angeles Area
  else if (lat > 33 && lat < 35 && lon > -119 && lon < -117) {
    return 'Los Angeles, CA';
  }
  // New York Area
  else if (lat > 40 && lat < 41 && lon > -75 && lon < -73) {
    return 'New York, NY';
  }
  // Seattle Area
  else if (lat > 47 && lat < 48 && lon > -123 && lon < -121) {
    return 'Seattle, WA';
  }
  // Chicago Area
  else if (lat > 41 && lat < 42 && lon > -88 && lon < -87) {
    return 'Chicago, IL';
  }
  // Default: return coordinates
  else {
    return getLocationDescription(lat, lon);
  }
}

/**
 * Validate coordinates
 * @param {number} lat - Latitude
 * @param {number} lon - Longitude
 * @returns {boolean} True if valid
 */
function isValidCoordinates(lat, lon) {
  return (
    typeof lat === 'number' &&
    typeof lon === 'number' &&
    lat >= -90 &&
    lat <= 90 &&
    lon >= -180 &&
    lon <= 180 &&
    !isNaN(lat) &&
    !isNaN(lon)
  );
}

// Export functions for use in other scripts
if (typeof module !== 'undefined' && module.exports) {
  module.exports = {
    calculateDistance,
    categorizeDistance,
    formatDistance,
    getDistanceBonus,
    getLocationDescription,
    estimateCityState,
    isValidCoordinates
  };
}

```

### api.js

```javascript
/**
 * Bramble API Integration
 * Claude API for product analysis and ethical scoring
 */

/**
 * Analyze product using Claude API
 * @param {string} productName - Name of the product
 * @param {Object} userPreferences - User's preferences (avoided brands, location, etc.)
 * @returns {Promise<Object>} Analysis results
 */
async function analyzeProduct(productName, userPreferences = {}) {
  console.log('Bramble API: Analyzing product:', productName);

  try {

    // Build the prompt
    const prompt = buildAnalysisPrompt(productName, userPreferences);

    // Call Claude API
    const response = await fetch('https://api.anthropic.com/v1/messages', {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        'x-api-key': CONFIG.ANTHROPIC_API_KEY,
        'anthropic-version': '2023-06-01'
      },
      body: JSON.stringify({
        model: 'claude-haiku-4-5-20251001',
        max_tokens: 1200,
        messages: [
          {
            role: 'user',
            content: prompt
          }
        ]
      })
    });

    if (!response.ok) {
      const errorData = await response.json().catch(() => ({}));
      console.error('Bramble API: API error:', errorData);

      if (response.status === 429) {
        throw new Error('API_RATE_LIMIT');
      } else {
        throw new Error(`API_ERROR: ${response.status}`);
      }
    }

    const data = await response.json();
    console.log('Bramble API: Raw response:', data);

    // Extract text from Claude's response
    const text = data.content[0].text;

    // Parse JSON, stripping markdown if present
    const analysis = parseClaudeResponse(text);

    console.log('Bramble API: Parsed analysis:', analysis);

    return analysis;

  } catch (error) {
    console.error('Bramble API: Error:', error);
    throw error;
  }
}

/**
 * Build analysis prompt for Claude
 */
function buildAnalysisPrompt(productName, userPreferences) {
  const avoidedBrands = userPreferences.avoidedBrands || [];
  const location = userPreferences.location || null;

  return `You are analyzing a product for ethical shopping purposes. The user is considering buying: "${productName}"

User's preferences:
- Brands to avoid: ${avoidedBrands.length > 0 ? avoidedBrands.join(', ') : 'None specified'}
- Location: ${location ? `${location.display}` : 'Not provided'}

Analyze this product and provide information about:
1. The parent company that manufactures or owns this product
2. An ethical score (0-100) based on labor practices, environmental impact, corporate ethics
3. Key ethical concerns (if any)
4. Product category
5. Types of ethical alternatives available
6. Cost-benefit analysis explaining why choosing alternatives matters
7. Keywords for finding local alternatives

Return ONLY valid JSON with this exact structure (no markdown, no code blocks, no explanation):
{
  "parentCompany": "Company Name",
  "ethicalScore": 0-100,
  "concerns": ["concern1", "concern2", "concern3"],
  "productCategory": "category name",
  "alternativeTypes": ["Local businesses", "Sustainable brands", "Fair trade options"],
  "costBenefitAnalysis": "A persuasive 2-3 sentence explanation of why choosing ethical alternatives for this product matters, focusing on real-world impact.",
  "suggestedStoreKeywords": ["keyword1", "keyword2", "keyword3"]
}

Important: Return ONLY the JSON object, no other text.`;
}

/**
 * Parse Claude's response, stripping markdown if present
 */
function parseClaudeResponse(text) {
  try {
    // Remove markdown code blocks if present
    let cleanText = text.trim();

    // Remove ```json and ``` markers
    cleanText = cleanText.replace(/^```json\s*/i, '');
    cleanText = cleanText.replace(/^```\s*/, '');
    cleanText = cleanText.replace(/```\s*$/, '');

    // Trim again
    cleanText = cleanText.trim();

    // Parse JSON
    const parsed = JSON.parse(cleanText);

    // Validate required fields
    if (!parsed.parentCompany || typeof parsed.ethicalScore !== 'number') {
      throw new Error('Invalid response structure');
    }

    // Ensure arrays exist
    parsed.concerns = parsed.concerns || [];
    parsed.alternativeTypes = parsed.alternativeTypes || [];
    parsed.suggestedStoreKeywords = parsed.suggestedStoreKeywords || [];

    // Clamp ethical score to 0-100
    parsed.ethicalScore = Math.max(0, Math.min(100, parsed.ethicalScore));

    return parsed;

  } catch (error) {
    console.error('Bramble API: Failed to parse response:', error);
    console.error('Bramble API: Raw text:', text);
    throw new Error('PARSE_ERROR');
  }
}

/**
 * Get fallback analysis when API fails
 */
function getFallbackAnalysis(productName) {
  return {
    parentCompany: 'Unknown',
    ethicalScore: 50,
    concerns: ['Unable to analyze - API unavailable'],
    productCategory: 'General',
    alternativeTypes: ['Local businesses', 'Sustainable options', 'Fair trade alternatives'],
    costBenefitAnalysis: 'Supporting local and ethical businesses helps build stronger communities, promotes fair labor practices, and reduces environmental impact.',
    suggestedStoreKeywords: ['local', 'sustainable', 'ethical']
  };
}

// Export functions if in module context
if (typeof module !== 'undefined' && module.exports) {
  module.exports = {
    analyzeProduct,
    getFallbackAnalysis
  };
}

```

### popup.html

```html
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Bramble Settings</title>
  <link rel="stylesheet" href="styles.css">
  <style>
    body {
      width: 350px;
      min-height: 400px;
      margin: 0;
      padding: 0;
      font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif;
      background: linear-gradient(135deg, #f5f7f0 0%, #e8ebe0 100%);
    }

    .popup-container {
      padding: 20px;
    }

    .popup-header {
      text-align: center;
      margin-bottom: 24px;
      padding-bottom: 16px;
      border-bottom: 2px solid #7ba05b;
    }

    .popup-logo {
      font-size: 48px;
      margin-bottom: 8px;
    }

    .popup-title {
      font-size: 24px;
      font-weight: 700;
      color: #2d4a2b;
      margin: 0 0 4px 0;
    }

    .popup-subtitle {
      font-size: 13px;
      color: #6b8e5f;
      margin: 0;
    }

    .settings-section {
      margin-bottom: 24px;
    }

    .section-title {
      font-size: 14px;
      font-weight: 600;
      color: #2d4a2b;
      margin: 0 0 12px 0;
      text-transform: uppercase;
      letter-spacing: 0.5px;
    }

    .setting-item {
      display: flex;
      justify-content: space-between;
      align-items: center;
      padding: 12px;
      margin-bottom: 8px;
      background: white;
      border-radius: 8px;
      box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
      transition: all 0.2s ease;
    }

    .setting-item:hover {
      box-shadow: 0 2px 6px rgba(0, 0, 0, 0.15);
      transform: translateY(-1px);
    }

    .setting-info {
      flex: 1;
    }

    .setting-label {
      font-size: 14px;
      font-weight: 500;
      color: #2d4a2b;
      display: block;
      margin-bottom: 2px;
    }

    .setting-description {
      font-size: 12px;
      color: #7ba05b;
    }

    /* Toggle Switch Styles */
    .toggle-switch {
      position: relative;
      display: inline-block;
      width: 48px;
      height: 26px;
      margin-left: 12px;
    }

    .toggle-switch input {
      opacity: 0;
      width: 0;
      height: 0;
    }

    .toggle-slider {
      position: absolute;
      cursor: pointer;
      top: 0;
      left: 0;
      right: 0;
      bottom: 0;
      background-color: #ccc;
      transition: 0.3s;
      border-radius: 34px;
    }

    .toggle-slider:before {
      position: absolute;
      content: "";
      height: 20px;
      width: 20px;
      left: 3px;
      bottom: 3px;
      background-color: white;
      transition: 0.3s;
      border-radius: 50%;
    }

    input:checked + .toggle-slider {
      background-color: #7ba05b;
    }

    input:checked + .toggle-slider:before {
      transform: translateX(22px);
    }

    .stats-section {
      background: white;
      border-radius: 12px;
      padding: 16px;
      text-align: center;
      box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
    }

    .stat-item {
      margin-bottom: 12px;
    }

    .stat-item:last-child {
      margin-bottom: 0;
    }

    .stat-value {
      font-size: 28px;
      font-weight: 700;
      color: #7ba05b;
      display: block;
    }

    .stat-label {
      font-size: 12px;
      color: #6b8e5f;
      text-transform: uppercase;
      letter-spacing: 0.5px;
    }

    .footer {
      margin-top: 24px;
      padding-top: 16px;
      border-top: 1px solid #d4dac9;
      text-align: center;
    }

    .footer-link {
      font-size: 12px;
      color: #7ba05b;
      text-decoration: none;
      margin: 0 8px;
    }

    .footer-link:hover {
      color: #5d7e47;
      text-decoration: underline;
    }

    /* Brands to Avoid Section */
    .brands-section {
      background: white;
      border-radius: 12px;
      padding: 16px;
      box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
    }

    .add-brand-container {
      display: flex;
      gap: 8px;
      margin-bottom: 12px;
    }

    .brand-input {
      flex: 1;
      padding: 10px 12px;
      border: 2px solid #d4dac9;
      border-radius: 8px;
      font-size: 14px;
      font-family: inherit;
      transition: border-color 0.2s ease;
    }

    .brand-input:focus {
      outline: none;
      border-color: #7ba05b;
    }

    .add-brand-btn {
      padding: 10px 16px;
      background: #7ba05b;
      color: white;
      border: none;
      border-radius: 8px;
      font-size: 14px;
      font-weight: 600;
      cursor: pointer;
      transition: all 0.2s ease;
    }

    .add-brand-btn:hover {
      background: #5d7e47;
    }

    .brands-list {
      display: flex;
      flex-direction: column;
      gap: 6px;
      max-height: 150px;
      overflow-y: auto;
    }

    .brand-tag {
      display: flex;
      justify-content: space-between;
      align-items: center;
      padding: 8px 12px;
      background: #f5f7f0;
      border-radius: 6px;
      font-size: 13px;
      color: #2d4a2b;
      animation: slideIn 0.2s ease;
    }

    .brand-name {
      flex: 1;
      font-weight: 500;
    }

    .remove-brand-btn {
      background: none;
      border: none;
      color: #e74c3c;
      cursor: pointer;
      font-size: 16px;
      padding: 0 4px;
      transition: transform 0.2s ease;
    }

    .remove-brand-btn:hover {
      transform: scale(1.2);
    }

    .empty-brands {
      text-align: center;
      padding: 20px;
      color: #7ba05b;
      font-size: 13px;
    }

    @keyframes slideIn {
      from {
        opacity: 0;
        transform: translateX(-10px);
      }
      to {
        opacity: 1;
        transform: translateX(0);
      }
    }

    /* Location Section */
    .location-section {
      background: white;
      border-radius: 12px;
      padding: 16px;
      box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
    }

    .location-status {
      display: flex;
      align-items: center;
      gap: 8px;
      padding: 12px;
      background: #f5f7f0;
      border-radius: 8px;
      margin-bottom: 12px;
    }

    .location-icon {
      fon
[truncated — 6334 more characters]
```

### styles.css

```css
/**
 * Vinegar - Cohesive Design System
 * Earth tones: greens, warm neutrals, creams
 */

/* CSS Variables for consistent theming */
:root {
  --primary-green: #7ba05b;
  --dark-green: #5d7e47;
  --light-green: #9cb87f;
  --primary-brown: #8b7355;
  --dark-brown: #6d5a45;
  --cream: #f5f3ed;
  --light-cream: #faf9f5;
  --warm-gray: #d4d0c8;
  --text-dark: #2d4a2b;
  --text-medium: #6b8e5f;
  --text-light: #a8c29a;

  --shadow-sm: 0 1px 3px rgba(45, 74, 43, 0.1);
  --shadow-md: 0 4px 6px rgba(45, 74, 43, 0.15);
  --shadow-lg: 0 10px 20px rgba(45, 74, 43, 0.2);

  --transition-fast: 0.15s ease;
  --transition-normal: 0.3s ease;
  --transition-slow: 0.5s ease;
}

/* Side Panel Container */
#vinegar-sidepanel-container {
  position: fixed;
  top: 0;
  right: 0;
  height: 100vh;
  z-index: 999999;
  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif;
  transition: transform var(--transition-normal);
}

#vinegar-sidepanel-container.vinegar-collapsed {
  transform: translateX(100%);
}

#vinegar-sidepanel-container.vinegar-expanded {
  transform: translateX(0);
}

/* Main Panel */
.vinegar-panel {
  position: relative;
  width: 420px;
  height: 100vh;
  background: linear-gradient(135deg, var(--light-cream) 0%, var(--cream) 100%);
  box-shadow: -4px 0 24px rgba(45, 74, 43, 0.2);
  display: flex;
  flex-direction: column;
}

/* Toggle Button */
.vinegar-toggle {
  position: absolute;
  left: -48px;
  top: 50%;
  transform: translateY(-50%);
  width: 48px;
  height: 64px;
  background: var(--primary-green);
  border: none;
  border-radius: 8px 0 0 8px;
  color: white;
  cursor: pointer;
  display: flex;
  align-items: center;
  justify-content: center;
  box-shadow: var(--shadow-md);
  transition: all var(--transition-fast);
}

.vinegar-toggle:hover {
  background: var(--dark-green);
  transform: translateY(-50%) translateX(-2px);
  box-shadow: var(--shadow-lg);
}

.vinegar-toggle svg {
  width: 24px;
  height: 24px;
}

/* Panel Content */
.vinegar-content {
  flex: 1;
  overflow-y: auto;
  padding: 24px;
  scrollbar-width: thin;
  scrollbar-color: var(--primary-green) var(--cream);
}

.vinegar-content::-webkit-scrollbar {
  width: 8px;
}

.vinegar-content::-webkit-scrollbar-track {
  background: var(--cream);
}

.vinegar-content::-webkit-scrollbar-thumb {
  background: var(--primary-green);
  border-radius: 4px;
}

/* Header */
.vinegar-header {
  text-align: center;
  margin-bottom: 28px;
  padding-bottom: 20px;
  border-bottom: 2px solid var(--primary-green);
}

.vinegar-title {
  font-size: 32px;
  font-weight: 700;
  color: var(--text-dark);
  margin: 0 0 6px 0;
  letter-spacing: -0.5px;
}

.vinegar-subtitle {
  font-size: 14px;
  color: var(--text-medium);
  margin: 0;
  font-weight: 500;
}

/* Sections */
.vinegar-section {
  margin-bottom: 24px;
  animation: fadeInUp 0.4s ease;
}

.vinegar-section-title {
  font-size: 16px;
  font-weight: 600;
  color: var(--text-dark);
  margin: 0 0 12px 0;
  text-transform: uppercase;
  letter-spacing: 0.8px;
  display: flex;
  align-items: center;
}

.vinegar-section-title:before {
  content: '';
  width: 4px;
  height: 16px;
  background: var(--primary-green);
  margin-right: 8px;
  border-radius: 2px;
}

/* Product Info */
.vinegar-product-info {
  background: white;
  border-radius: 12px;
  padding: 16px;
  box-shadow: var(--shadow-sm);
  border: 1px solid var(--warm-gray);
}

.product-name {
  font-size: 15px;
  font-weight: 600;
  color: var(--text-dark);
  margin-bottom: 8px;
  line-height: 1.4;
}

.product-price {
  font-size: 20px;
  font-weight: 700;
  color: var(--primary-green);
  margin-bottom: 8px;
}

.product-site {
  font-size: 13px;
  color: var(--text-medium);
}

.site-badge {
  display: inline-block;
  padding: 4px 12px;
  background: var(--cream);
  border-radius: 16px;
  font-weight: 500;
}

/* Company Info */
.vinegar-company-info {
  background: white;
  border-radius: 12px;
  padding: 16px;
  box-shadow: var(--shadow-sm);
  border: 1px solid var(--warm-gray);
}

.company-badge {
  display: flex;
  align-items: center;
  margin-bottom: 16px;
  padding: 12px;
  background: var(--cream);
  border-radius: 8px;
}

.badge-icon {
  font-size: 24px;
  margin-right: 12px;
}

.badge-text {
  font-size: 14px;
  color: var(--text-dark);
}

/* Ethical Score */
.ethical-score {
  display: flex;
  align-items: center;
  gap: 12px;
}

.score-label {
  font-size: 13px;
  font-weight: 600;
  color: var(--text-dark);
  white-space: nowrap;
}

.score-bar {
  flex: 1;
  height: 8px;
  background: var(--cream);
  border-radius: 4px;
  overflow: hidden;
}

.score-fill {
  height: 100%;
  background: linear-gradient(90deg, #e74c3c 0%, #f39c12 50%, var(--primary-green) 100%);
  transition: width var(--transition-slow);
  border-radius: 4px;
}

.score-value {
  font-size: 14px;
  font-weight: 700;
  color: var(--text-dark);
  white-space: nowrap;
}

/* Alternatives List */
.vinegar-alternatives {
  display: flex;
  flex-direction: column;
  gap: 16px;
}

.alternative-card {
  background: white;
  border-radius: 12px;
  padding: 16px;
  box-shadow: var(--shadow-sm);
  border: 1px solid var(--warm-gray);
  transition: all var(--transition-fast);
  opacity: 0;
}

.alternative-card.vinegar-fade-in {
  animation: fadeInUp 0.4s ease forwards;
}

.alternative-card:hover {
  box-shadow: var(--shadow-md);
  transform: translateY(-2px);
}

.alt-header {
  display: flex;
  justify-content: space-between;
  align-items: flex-start;
  margin-bottom: 12px;
}

.alt-name {
  font-size: 15px;
  font-weight: 600;
  color: var(--text-dark);
  margin: 0;
  flex: 1;
  line-height: 1.3;
}

.alt-badge {
  padding: 4px 10px;
  border-radius: 12px;
  font-size: 11px;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.5px;
  white-space: nowrap;
  margin-left: 8px;
}

.alt-badge.local {
  background: #e8f5e9;
  color: #2e7d32;
}

.alt-badge.sustainable {
  background: #e0f2f1;
  color: #00695c;
}

.alt-badge.ethical {
 
[truncated — 6358 more characters]
```

### popup.js

```javascript
/**
 * Bramble Popup Controller
 * Manages extension settings and user preferences
 */

// Default settings
const DEFAULT_SETTINGS = {
  supportLocal: true,
  sustainableProducts: true,
  avoidedBrands: [], // Array of brand names to avoid
  location: null // { lat, lon, display, zipCode }
};

/**
 * Initialize popup
 */
document.addEventListener('DOMContentLoaded', async () => {
  await loadSettings();
  await loadLocation();
  setupEventListeners();
  updateStats();
});

/**
 * Load settings from Chrome storage
 */
async function loadSettings() {
  try {
    const result = await chrome.storage.sync.get('settings');
    const settings = result.settings || DEFAULT_SETTINGS;

    // Update toggle switches
    document.getElementById('toggle-local').checked = settings.supportLocal !== false;
    document.getElementById('toggle-sustainable').checked = settings.sustainableProducts !== false;

    // Load and display avoided brands
    loadBrandsList(settings.avoidedBrands || []);

    // Load location is called separately in initialize
  } catch (error) {
    console.error('Error loading settings:', error);
  }
}

/**
 * Load and display location
 */
async function loadLocation() {
  try {
    const result = await chrome.storage.sync.get('settings');
    const settings = result.settings || DEFAULT_SETTINGS;
    const location = settings.location;

    const locationDisplay = document.getElementById('location-display');
    if (!locationDisplay) return;

    if (location && location.lat && location.lon) {
      locationDisplay.textContent = location.display || estimateCityState(location.lat, location.lon);
      locationDisplay.classList.remove('not-set');
    } else {
      locationDisplay.textContent = 'Not set';
      locationDisplay.classList.add('not-set');
    }
  } catch (error) {
    console.error('Error loading location:', error);
  }
}

/**
 * Save settings to Chrome storage
 */
async function saveSettings() {
  const settings = {
    supportLocal: document.getElementById('toggle-local').checked,
    sustainableProducts: document.getElementById('toggle-sustainable').checked,
    avoidedBrands: await getAvoidedBrands()
  };

  try {
    await chrome.storage.sync.set({ settings });
    console.log('Settings saved:', settings);

    // Notify content scripts of settings change
    chrome.runtime.sendMessage({
      type: 'SETTINGS_UPDATED',
      data: settings
    }).catch(() => {
      // Content script might not be loaded yet, that's okay
    });

    // Show visual feedback
    showSaveNotification();
  } catch (error) {
    console.error('Error saving settings:', error);
  }
}

/**
 * Get current list of avoided brands
 */
async function getAvoidedBrands() {
  try {
    const result = await chrome.storage.sync.get('settings');
    const settings = result.settings || DEFAULT_SETTINGS;
    return settings.avoidedBrands || [];
  } catch (error) {
    console.error('Error getting avoided brands:', error);
    return [];
  }
}

/**
 * Load and display brands list
 */
function loadBrandsList(brands) {
  const brandsList = document.getElementById('brands-list');
  if (!brandsList) return;

  if (!brands || brands.length === 0) {
    brandsList.innerHTML = '<div class="empty-brands">No brands added yet. Add brands above to avoid them.</div>';
    return;
  }

  brandsList.innerHTML = '';
  brands.forEach(brand => {
    const brandTag = createBrandTag(brand);
    brandsList.appendChild(brandTag);
  });
}

/**
 * Create a brand tag element
 */
function createBrandTag(brandName) {
  const tag = document.createElement('div');
  tag.className = 'brand-tag';

  const name = document.createElement('span');
  name.className = 'brand-name';
  name.textContent = brandName;

  const removeBtn = document.createElement('button');
  removeBtn.className = 'remove-brand-btn';
  removeBtn.textContent = '×';
  removeBtn.setAttribute('aria-label', `Remove ${brandName}`);
  removeBtn.addEventListener('click', () => removeBrand(brandName));

  tag.appendChild(name);
  tag.appendChild(removeBtn);

  return tag;
}

/**
 * Add a new brand to avoid
 */
async function addBrand() {
  const input = document.getElementById('brand-input');
  if (!input) return;

  const brandName = input.value.trim();

  // Validate input
  if (!brandName) {
    return;
  }

  if (brandName.length > 50) {
    alert('Brand name is too long (max 50 characters)');
    return;
  }

  // Get current brands
  const brands = await getAvoidedBrands();

  // Check for duplicates (case-insensitive)
  const brandLower = brandName.toLowerCase();
  if (brands.some(b => b.toLowerCase() === brandLower)) {
    alert('This brand is already in your list');
    return;
  }

  // Add new brand
  brands.push(brandName);

  // Save to storage
  const settings = {
    supportLocal: document.getElementById('toggle-local').checked,
    sustainableProducts: document.getElementById('toggle-sustainable').checked,
    avoidedBrands: brands
  };

  try {
    await chrome.storage.sync.set({ settings });

    // Update UI
    loadBrandsList(brands);

    // Clear input
    input.value = '';

    // Show notification
    showSaveNotification();

    // Notify content scripts
    chrome.runtime.sendMessage({
      type: 'SETTINGS_UPDATED',
      data: settings
    }).catch(() => {});

  } catch (error) {
    console.error('Error adding brand:', error);
    alert('Failed to add brand. Please try again.');
  }
}

/**
 * Remove a brand from the avoid list
 */
async function removeBrand(brandName) {
  const brands = await getAvoidedBrands();
  const updatedBrands = brands.filter(b => b !== brandName);

  // Save to storage
  const settings = {
    supportLocal: document.getElementById('toggle-local').checked,
    sustainableProducts: document.getElementById('toggle-sustainable').checked,
    avoidedBrands: updatedBrands
  };

  try {
    await chrome.storage.sync.set({ settings });

    // Update UI
    loadBrandsList(updatedBrands);

    // Show notification
    showSave
[truncated — 11438 more characters]
```

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