# Project export: FireflyBook: A Quote-Saving Tool

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: UC Berkeley AI Hackathon 2026
- Tagline: Quickly and directly save quotes and information with two clicks.
- Devpost: https://devpost.com/software/firefly-a-quote-saving-tool
- GitHub: https://github.com/SoraWongsonegoro/highlighterproj.git
- Video: https://www.youtube.com/embed/3bjHRRQLiHQ?enablejsapi=1&hl=en_US&rel=0&start=&version=3&wmode=transparent
- Team: 2 GitHub contributor(s) — SoraWongsonegoro (38 commits), Manlin Zhang (16 commits)

## Devpost submission (written by the team)

### Inspiration

Both of us are pretty avid online readers, from news articles to online textbooks to web novels to research papers. While reading, sometimes we stumble upon a funny line, a memorable quote, or an interesting fact we wish we could save for future reference. However, copying the quote into a notes app or a Google Doc is both tedious and hard to keep track of. Therefore, we decided to build FireflyBook, a quote-saving tool that makes highlighting and saving quotes from webpages much more convenient, so we can always have our favorite lines ready to reference on the go!

### What it does

FireflyBook is a browser extension that lets you highlight sections of text on a webpage to be stored as a “quote,” noting down the source URL, the date, and additional “context” surrounding the quote if applicable. All saved quotes are stored in a pop-up dashboard that displays saved quotes with sorting, multi-selection, search, and folder features. The extension also supports additional export options, including generating an image of the quote to share with peers. Users can also annotate their quotes with additional notes and save them.

### How we built it

To decide what specific features we wanted FireflyBook to have, we looked at existing book-specific highlight and quote-saving tools, such as Kindle’s notebook feature, for inspiration. Based on that research, and our own wants as avid readers, we created a list of tasks and ambitious features that would eventually shape the current FireflyBook extension. To be able to build the tool within the time constraints of the hackathon, we primarily used the coding agents Claude Code and GitHub Copilot to get the extension up and running. We also used our existing HTML/CSS/JS knowledge to make small changes where necessary. We designed our extension for the popularly accepted framework, ManifestV3. Sora used Procreate for iPad to design and draw the FireflyBook icon.

### Challenges we ran into

We were very wary of duplicating a tool that already existed, given the simple concept of our tool. So, we researched similar highlighting tools to find out what they lacked; some tools only highlighted and saved text on a page, but no functional extensions saved the quotes themselves. We made sure the gaps existed before filling them. Additionally, we changed many of our features over time as we honed in on improving user experience and convenience. We wanted our extension and its features to be as quick and intuitive to use as possible, so we altered many features to streamline the user experience. One example of this was implementing an alternative “drag selection” in our mass-selection screen, which allowed users to click and drag to select multiple quotes instead of having to click each checkbox manually.

### Accomplishments we're proud of

One of our inspirations for the project was the seamless highlighting experience of Kindle, and the extension perfectly matches that while adding additional web-specific features, like linking back to the article of origin and additional export options. There are many convenience features for users, such as a command shortcut for highlighting for quick note-taking, ensuring a smooth user experience, which is something we wanted to focus on. Manlin was happy that the extension was cross-compatible across Chrome, Firefox (her browser of choice), and other browsers. Sora did a great job of designing our FireflyBook icon by hand!

### What we learned

Neither of us has extensive UX design experience, so we drew on our own experience of being web users to guide our main design principles: make actions take as few clicks as possible, and make the interface as uncluttered as possible. To make up for our lack of UX experience, we looked into extensions like Simple Translate and Firefox Multi Account Containers to gauge how to structure our popup and highlighter handling. We also did not have extensive experience using LLMs to create web apps, so we used this opportunity as a chance to explore how AI agents such as Claude could assist us in building software.

### What's next

Generate an automatic AI summary for each quote when it’s selected- users can get detailed AI context when re-reading their saved quote. Launch a web server, so users can log into their saved quotes from anywhere. Support users focusing on research- add automatic citation tools, save articles in project folders, etc.

## README (from the GitHub repository)

# Firefly Book
<em> - A Highlighting and Quote-Saving Tool - </em>


To download and use Firefly Book, first pull the contents of this repository into a folder. Next, find your browser below and follow the installation steps. If you cannot find your browser, the general steps include finding the extensions manager page for your browser, turning on "developer mode", and then uploading the Firefly Book folder through the "Load Unpacked" option. 

Firefly Book works best when it is pinned to your browser for easy access to the dashboard. The extension has a built-in guide that can be accessed by clicking the question mark in the top right of the dashboard.


<br/>



### If on Chrome:

Go to chrome://extensions/ and turn on developer mode. Then, click "Load unpacked" and upload the Firefly Book folder. You may need to refresh the page for the extension to activate. 

### If on Firefox (temporary-only):

Download the repository to your device. Zip the folder so that "manifest.json" is a top-level file. On the Firefox browser, go to about:debugging. Click on This Firefox and click on Load Temporary Add-on. Upload your ZIP file. Note that this is method is a temporary add-on only; the data will be cleared when the Firefox browser is closed.


### If on Microsoft Edge: 

Click the three dots (...) on the top right of the browser and navigate to the Extensions option. Turn on developer mode, click "Load unpacked," and upload the Firefly Book Folder.

## Detected evidence (automated analysis)

Indexed codebase: 8 recognized source files, 125 KB.
- CSS (language) — detected in the code
- HTML (language) — detected in the code
- JavaScript (language) — detected in the code

## Codebase structure (from repository index)

### Files (9 of 9)

```
content.js
manifest.json
popup.css
popup.html
popup.js
README.md
settings.html
settings.js
styles.css
```

### Dependencies

No dependency index available.

### Recent commits (newest first)

- fixed innerHTML issues for firefox browser & updated images
- supported firefox browser specific setting
- fixed some CSS bugs
- changed title hover
- added instructions
- Merge branch 'main' of https://github.com/SoraWongsonegoro/highlighterproj
- made settings an external page
- Enhance README with guide access instructions
- Update README with installation steps for Chrome and Edge
- Rename project to Firefly Book and add usage info
- merge
- context will only be saved and shown for quotes from paragraph/block text
- changed name of extension
- fixed firefly icon
- added context and some QOL updates
- Merge branch 'main' of https://github.com/SoraWongsonegoro/highlighterproj
- added annotations and collections mulit-select, which fixed the collection deselect bug
- new icon stuff
- images folder for tutorial images & icon
- added tutorial menu, fixed selection screen, added reset button

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

### styles.css

```css
.ts-ext-save-btn {
    position: absolute;
    display: none;
    z-index: 2147483647; /* Maximum possible z-index to stay above everything */
    width: 42px;
    height: 42px;
    border: none;
    border-radius: 50%;
    background: #ffffff;
    color: #111;
    cursor: pointer;
    box-shadow: 0 6px 18px rgba(0,0,0,0.18);
    display: none;
    padding: 0;
    transition: background 0.18s ease, transform 0.18s ease, box-shadow 0.18s ease;
    display: inline-flex;
    align-items: center;
    justify-content: center;
}

.ts-ext-save-btn:hover {
    background: #c21b1b;
    color: white;
    transform: translateY(-1px);
    box-shadow: 0 8px 24px rgba(0,0,0,0.24);
}

.ts-ext-highlight {
    background-color: #f2ff66;
    color: inherit;
    border-radius: 2px;
}
```

### popup.css

```css
body {
    width: 320px;
    margin: 0;
    padding: 15px;
    font-family: Arial, sans-serif;
    background: #f9f9f9;
}

h3 {
    margin-top: 0;
    text-align: center;
    color: #333;
}

ul {
    list-style: none;
    padding: 0;
    margin: 0;
    max-height: 400px;
    overflow-y: auto;
}

li {
    padding: 12px;
    border-bottom: 1px solid #eee;
    background: white;
    margin-bottom: 8px;
    border-radius: 4px;
    box-shadow: 0 1px 3px rgba(0,0,0,0.05);
    word-wrap: break-word;
}

/* Styles for Title and Text */
.snippet-title {
    display: block;
    font-weight: bold;
    color: #0056b3;
    text-decoration: none;
    margin-bottom: 6px;
    font-size: 14px;
    line-height: 1.3;
}

.snippet-title:hover {
    text-decoration: underline;
}

.snippet-text {
    font-size: 13px;
    color: #555;
    border-left: 3px solid #ddd;
    padding-left: 8px;
    margin: 0;
    font-style: italic;
}
```

### settings.html

```html
<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
    <title>FireflyBook — Settings</title>
    <style>
        * { box-sizing: border-box; margin: 0; padding: 0; }
        body {
            font-family: Arial, sans-serif;
            background: #f4f4f4;
            color: #111;
            padding: 40px 16px;
            display: flex;
            justify-content: center;
        }
        .wrap {
            background: #fff;
            width: 100%;
            max-width: 480px;
            border-radius: 14px;
            padding: 28px;
            box-shadow: 0 8px 30px rgba(0,0,0,0.12);
        }
        h1 {
            font-family: Cambria, "Times New Roman", serif;
            font-size: 22px;
            color: #111;
            margin-bottom: 6px;
        }
        .desc { font-size: 13px; color: #555; line-height: 1.5; margin-bottom: 20px; }
        .label { font-size: 13px; color: #333; margin: 14px 0 6px 0; font-weight: 600; }
        button { font-family: Arial, sans-serif; }
        .btn-primary {
            width: 100%; padding: 12px; background: #c21b1b; color: #fff; border: none;
            border-radius: 10px; cursor: pointer; font-size: 14px; margin-bottom: 10px;
        }
        .btn-secondary {
            width: 100%; padding: 12px; background: #fff; color: #111; border: 1px solid #c21b1b;
            border-radius: 10px; cursor: pointer; font-size: 14px; margin-bottom: 10px;
        }
        .btn-secondary:hover, .btn-primary:hover { opacity: 0.92; }
        select {
            width: 100%; padding: 10px; border: 1px solid #d1d1d1; border-radius: 10px;
            background: white; font-size: 13px; font-family: Arial, sans-serif;
        }
        #color-swatches { display: flex; flex-wrap: wrap; gap: 8px; }
        .color-swatch { width: 34px; height: 34px; border-radius: 6px; border: 1px solid #ddd; cursor: pointer; }
        .custom-row { display: flex; gap: 8px; align-items: center; margin-top: 10px; }
        #custom-color-input { width: 44px; height: 34px; border-radius: 6px; border: 1px solid #d1d1d1; }
        #apply-custom-color { padding: 8px 12px; border-radius: 8px; background: #f4f4f4; border: none; cursor: pointer; }
        .check { display: flex; align-items: center; gap: 10px; font-size: 13px; color: #333; margin-top: 14px; cursor: pointer; }
        .check input { width: 15px; height: 15px; accent-color: #c21b1b; }
        #settings-feedback { font-size: 13px; color: #c21b1b; min-height: 20px; margin-top: 14px; opacity: 0; transition: opacity 140ms ease; }
        #reset-data-btn {
            width: 100%; padding: 12px; background: #fff; color: #c21b1b; border: 1px solid #c21b1b;
            border-radius: 10px; cursor: pointer; font-size: 13px; margin-top: 6px;
        }
        hr { border: none; border-top: 1px solid #eee; margin: 22px 0; }
    </style>
</head>
<body>
    <div class="wrap">
        <h1>Settings</h1>
        <p class="desc">Export or import your saved quotes as a database file. Imported quotes are merged into your current library (duplicates are skipped).</p>

        <button id="export-db-btn" class="btn-primary">Export database</button>
        <button id="import-db-btn" class="btn-secondary">Import database</button>
        <input id="import-db-input" type="file" accept=".db,.json,.txt,application/json,text/plain" style="display:none;" />

        <hr />

        <label for="sort-select" class="label">Sort order</label>
        <select id="sort-select">
            <option value="date">Date</option>
            <option value="alpha">A–Z</option>
        </select>

        <div class="label">Highlight color</div>
        <div id="color-swatches">
            <button class="color-swatch" data-color="#e74c3c" title="Red" style="background:#e74c3c;"></button>
            <button class="color-swatch" data-color="#3498db" title="Blue" style="background:#3498db;"></button>
            <button class="color-swatch" data-color="#ffd54f" title="Yellow" style="background:#ffd54f;"></button>
            <button class="color-swatch" data-color="#66bb6a" title="Green" style="background:#66bb6a;"></button>
            <button class="color-swatch" data-color="#ff8a65" title="Orange" style="background:#ff8a65;"></button>
            <button class="color-swatch" data-color="#f06292" title="Pink" style="background:#f06292;"></button>
            <button class="color-swatch" data-color="#9575cd" title="Purple" style="background:#9575cd;"></button>
            <button class="color-swatch" data-color="#ffffff" title="White" style="background:#ffffff;"></button>
            <button class="color-swatch" data-color="#000000" title="Black" style="background:#000000;"></button>
        </div>
        <div class="custom-row">
            <input id="custom-color-input" type="color" value="#ffd54f" />
            <button id="apply-custom-color">Apply</button>
        </div>
        <label class="check">
            <input type="checkbox" id="transparent-highlight" /> Transparent highlight (invisible on page)
        </label>

        <div id="settings-feedback"></div>

        <hr />

        <button id="reset-data-btn">Reset data (wipe all quotes &amp; collections)</button>
    </div>
    <script src="settings.js"></script>
</body>
</html>

```

### settings.js

```javascript
// Settings page logic. Runs in a full extension tab (not the action popup), so the
// import file picker stays open and parsing works reliably in Firefox.

function showSettingsFeedback(message) {
    const feedback = document.getElementById('settings-feedback');
    if (!feedback) return;
    feedback.textContent = message;
    feedback.style.opacity = '1';
    clearTimeout(feedback.hideTimeout);
    feedback.hideTimeout = setTimeout(() => {
        feedback.style.opacity = '0';
    }, 2400);
}

function exportDatabase() {
    chrome.storage.local.get(['saved_snippets', 'collections'], (result) => {
        const data = {
            saved_snippets: result.saved_snippets || [],
            collections: result.collections || {}
        };
        const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' });
        const link = document.createElement('a');
        link.href = URL.createObjectURL(blob);
        link.download = `fireflybook-quotes-${new Date().toISOString().slice(0, 10)}.db`;
        document.body.appendChild(link);
        link.click();
        URL.revokeObjectURL(link.href);
        document.body.removeChild(link);
        showSettingsFeedback('Export complete.');
    });
}

function importDatabaseFile(file) {
    const handleText = (raw) => {
        const text = String(raw == null ? '' : raw).replace(/^﻿/, '').trim();
        if (!text) {
            showSettingsFeedback('Import failed: file is empty.');
            return;
        }
        let payload;
        try {
            payload = JSON.parse(text);
        } catch (err) {
            console.error('[import] JSON parse failed:', err, 'first 100 chars:', text.slice(0, 100));
            showSettingsFeedback('Import failed: file is not valid JSON.');
            return;
        }
        try {
            mergeImportedDatabase(payload);
        } catch (err) {
            console.error('[import] merge failed:', err);
            showSettingsFeedback(`Import failed: ${err.message}`);
        }
    };

    if (typeof file.text === 'function') {
        file.text().then(handleText).catch((err) => {
            console.error('[import] read failed:', err);
            showSettingsFeedback('Import failed: unable to read file.');
        });
    } else {
        const reader = new FileReader();
        reader.onload = (event) => handleText(event.target && event.target.result);
        reader.onerror = () => showSettingsFeedback('Import failed: unable to read file.');
        reader.readAsText(file);
    }
}

function mergeImportedDatabase(payload) {
    const importedSnippets = Array.isArray(payload?.saved_snippets)
        ? payload.saved_snippets
        : Array.isArray(payload?.savedQuotes)
            ? payload.savedQuotes
            : Array.isArray(payload?.quotes)
                ? payload.quotes
                : Array.isArray(payload?.highlights)
                    ? payload.highlights
                    : Array.isArray(payload?.items)
                        ? payload.items
                        : Array.isArray(payload?.snippets)
                            ? payload.snippets
                            : Array.isArray(payload?.data)
                                ? payload.data
                                : Array.isArray(payload)
                                    ? payload
                                    : [];
    const importedCollections = payload?.collections && typeof payload.collections === 'object'
        ? payload.collections
        : {};

    if (!Array.isArray(importedSnippets)) {
        showSettingsFeedback('Import failed: no valid quotes found.');
        return;
    }

    chrome.storage.local.get(['saved_snippets', 'collections'], (result) => {
        const existingSnippets = result.saved_snippets || [];
        const existingCollections = result.collections || {};

        const snippetKey = (snippet) => [snippet.text || '', snippet.url || '', snippet.title || '', snippet.savedAt || ''].join('||');
        const existingKeys = new Set(existingSnippets.map(snippetKey));

        let added = 0;
        const mergedSnippets = [...existingSnippets];
        importedSnippets.forEach((snippet) => {
            if (snippet && typeof snippet === 'object') {
                if (!existingKeys.has(snippetKey(snippet))) {
                    mergedSnippets.push({
                        text: snippet.text || '',
                        url: snippet.url || '',
                        title: snippet.title || '',
                        savedAt: snippet.savedAt || new Date().toISOString(),
                        annotation: snippet.annotation || '',
                        contextBefore: snippet.contextBefore || '',
                        contextAfter: snippet.contextAfter || ''
                    });
                    existingKeys.add(snippetKey(snippet));
                    added++;
                }
            }
        });

        const mergedCollections = { ...existingCollections };
        Object.entries(importedCollections).forEach(([collectionName, snippets]) => {
            if (!Array.isArray(snippets)) return;
            if (!Array.isArray(mergedCollections[collectionName])) {
                mergedCollections[collectionName] = [];
            }
            const collectionKeys = new Set((mergedCollections[collectionName] || []).map(snippetKey));
            snippets.forEach((snippet) => {
                if (snippet && typeof snippet === 'object' && !collectionKeys.has(snippetKey(snippet))) {
                    mergedCollections[collectionName].push({
                        text: snippet.text || '',
                        url: snippet.url || '',
                        title: snippet.title || '',
                        savedAt: snippet.savedAt || new Date().toISOString(),
                        annotation: snippet.annotation || '',
                        contextBefore: snippet.contextBefore || '',
                        contextAfter: snipp
[truncated — 4701 more characters]
```

### content.js

```javascript
const popupBtn = document.createElement('button');
popupBtn.className = 'ts-ext-save-btn';
popupBtn.setAttribute('aria-label', 'Save highlight');
popupBtn.innerHTML = `
    <svg width="18" height="18" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
        <path d="M6 4h12a2 2 0 0 1 2 2v12a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2Z" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"/>
        <path d="M8 9h8M8 13h8M8 17h5" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"/>
    </svg>
`;
document.body.appendChild(popupBtn);

let pendingRange = null;
let highlightColor = '#ffd54f'; // default soft yellow

function hexToLuma(hex) {
    if (!hex) return 1;
    const h = hex.replace('#', '');
    const bigint = parseInt(h.length === 3 ? h.split('').map(c => c + c).join('') : h, 16);
    const r = (bigint >> 16) & 255;
    const g = (bigint >> 8) & 255;
    const b = bigint & 255;
    // relative luminance
    return (0.2126 * r + 0.7152 * g + 0.0722 * b) / 255;
}

function getContrastColor(hex) {
    const luma = hexToLuma(hex || '#ffffff');
    return luma > 0.6 ? '#000000' : '#ffffff';
}

function applyHighlightStyle(el, color) {
    // 'transparent' = invisible highlight: keep the text styling untouched.
    if (color === 'transparent' || color === 'none') {
        el.style.backgroundColor = 'transparent';
        el.style.color = 'inherit';
        el.style.padding = '0';
        el.style.borderRadius = '0';
        return;
    }
    el.style.backgroundColor = color;
    el.style.color = getContrastColor(color);
    el.style.padding = '0 0.1em';
    el.style.borderRadius = '2px';
}

function updateExistingHighlights(color) {
    try {
        const nodes = document.querySelectorAll('.ts-ext-highlight');
        nodes.forEach((el) => applyHighlightStyle(el, color));
    } catch (e) {
        // ignore
    }
}

// Load configured highlight color and apply to existing highlights
try {
    chrome.storage.local.get(['highlight_color'], (res) => {
        if (res && res.highlight_color) {
            highlightColor = res.highlight_color;
        }
        updateExistingHighlights(highlightColor);
    });
    chrome.storage.onChanged.addListener((changes, area) => {
        if (area === 'local' && changes.highlight_color) {
            highlightColor = changes.highlight_color.newValue;
            updateExistingHighlights(highlightColor);
        }
    });
} catch (e) {
    // ignore in page context if chrome storage unavailable
}

function isExtensionValid() {
    try {
        return !!chrome.runtime?.id;
    } catch (e) {
        return false;
    }
}

function lastSentence(text) {
    const parts = (text || '').split(/(?<=[.!?])\s+/).map((s) => s.trim()).filter(Boolean);
    return parts.length ? parts[parts.length - 1] : '';
}

function firstSentence(text) {
    const parts = (text || '').split(/(?<=[.!?])\s+/).map((s) => s.trim()).filter(Boolean);
    return parts.length ? parts[0] : '';
}

// Extract the sentence immediately before and after the selected quote, using the
// surrounding block element as the source of context.
function getQuoteContext(range) {
    try {
        let container = range.commonAncestorContainer;
        if (container.nodeType === Node.TEXT_NODE) container = container.parentNode;

        // If the selection is inside a header/title element, don't capture context.
        if (container.closest && container.closest('h1,h2,h3,h4,h5,h6,header')) {
            return { before: '', after: '' };
        }

        // Only consider paragraph-like blocks for context (avoid generic divs)
        const block = (container.closest &&
            container.closest('p, li, blockquote, article, section, td, dd, dt')) || null;

        if (!block) return { before: '', after: '' };

        const fullText = (block.textContent || '').replace(/\s+/g, ' ').trim();
        const selected = range.toString().replace(/\s+/g, ' ').trim();
        if (!fullText || !selected) return { before: '', after: '' };

        const idx = fullText.indexOf(selected);
        if (idx === -1) return { before: '', after: '' };

        const beforeSlice = fullText.slice(0, idx);
        const afterSlice = fullText.slice(idx + selected.length);
        let before = lastSentence(beforeSlice);
        let after = firstSentence(afterSlice);
        // Preserve whitespace joining context to the quote for smoother display.
        if (before && /\s$/.test(beforeSlice)) before += ' ';
        if (after && /^\s/.test(afterSlice)) after = ' ' + after;
        return { before, after };
    } catch (e) {
        return { before: '', after: '' };
    }
}

function highlightRange(range) {
    const living = [];
    const rangeWalker = document.createTreeWalker(
        range.commonAncestorContainer.nodeType === Node.TEXT_NODE
            ? range.commonAncestorContainer.parentNode
            : range.commonAncestorContainer,
        NodeFilter.SHOW_TEXT
    );

    let node;
    while ((node = rangeWalker.nextNode())) {
        if (range.intersectsNode(node)) {
            living.push(node);
        }
    }

    living.forEach((textNode) => {
        const nodeRange = document.createRange();
        nodeRange.selectNodeContents(textNode);

        if (textNode === range.startContainer) {
            nodeRange.setStart(textNode, range.startOffset);
        }
        if (textNode === range.endContainer) {
            nodeRange.setEnd(textNode, range.endOffset);
        }

        const highlight = document.createElement('mark');
        highlight.className = 'ts-ext-highlight';
        // apply configured highlight color
        try {
            applyHighlightStyle(highlight, highlightColor);
        } catch (e) {
            // ignore styling errors
        }
        nodeRange.surroundContents(highlight);
    });
}

document.addEventListener('mouseup', (e) => {
    if (popupBtn.contains(e.target)) return;

    const 
[truncated — 9305 more characters]
```

### popup.html

```html
<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
    <style>
        * { box-sizing: border-box; margin: 0; padding: 0; }

        /* Primary font is Cambria, small numeric/text UI uses Arial */
        body {
            width: 600px;
            height: 560px;
            font-family: Cambria, "Times New Roman", serif;
            background: #ffffff;
            display: flex;
            flex-direction: column;
            overflow: hidden;
            color: #111;
            padding: 12px 12px 12px 12px;
        }

        /* --- Tab Bar --- */
        #tab-bar {
            display: flex;
            gap: 10px;
            padding: 6px 0 12px 0;
            align-items: center;
            border-bottom: 1px solid #e6e6e6;
            margin-bottom: 8px;
        }
        .tab {
            padding: 5px 10px;
            color: #222;
            cursor: pointer;
            font-size: 13px;
            font-family: Cambria, "Times New Roman", serif;
            user-select: none;
            border-radius: 3px;
            white-space: nowrap;
        }
        .tab.active {
            background: #c21b1b;
            color: #fff;
            box-shadow: inset 0 -3px 0 rgba(0,0,0,0.05);
            padding: 6px 12px;
        }
        .search-input {
            flex: 1;
            min-width: 110px;
            max-width: 220px;
            padding: 6px 10px;
            border: 1px solid #d1d1d1;
            border-radius: 8px;
            font-size: 13px;
            font-family: Arial, sans-serif;
            color: #111;
            background: #fafafa;
        }
        .search-input:focus {
            outline: none;
            border-color: #c21b1b;
            background: white;
        }

        .settings-btn {
            width: 34px;
            height: 34px;
            border: none;
            border-radius: 50%;
            background: #f4f4f4;
            cursor: pointer;
            display: inline-flex;
            align-items: center;
            justify-content: center;
            color: #111;
            font-size: 16px;
            font-family: Arial, sans-serif;
        }
        .settings-btn:hover {
            background: #eaeaea;
        }

        /* --- Pages --- */
        #page-container {
            flex: 1;
            overflow: hidden;
            position: relative;
        }
        .page {
            display: none;
            flex-direction: column;
            height: 100%;
            padding: 4px 2px 16px 2px;
            overflow-y: auto;
        }
        .page.active { display: flex; }

        /* --- Toolbar row (search + add collection) --- */
        .toolbar {
            display: flex;
            align-items: center;
            gap: 10px;
            margin-bottom: 14px;
            flex-shrink: 0;
            position: relative;
            overflow: visible;
        }
        .toolbar-icon {
            width: 34px;
            height: 34px;
            border: none;
            border-radius: 10px;
            background: #f4f4f4;
            cursor: pointer;
            display: inline-flex;
            align-items: center;
            justify-content: center;
            color: #111;
            font-size: 18px;
            font-weight: 700;
            line-height: 1;
        }
        .toolbar-icon:hover { background: #eaeaea; }
        #new-collection-popup {
            position: absolute;
            top: 50px;
            right: 0;
            background: white;
            border: 1px solid #d8d8d8;
            box-shadow: 0 8px 24px rgba(0,0,0,0.14);
            border-radius: 12px;
            padding: 14px;
            display: none;
            width: 260px;
            z-index: 999;
        }
        #new-collection-popup.visible { display: block; }
        #new-collection-popup::before {
            content: '';
            position: absolute;
            top: -7px;
            right: 20px;
            width: 14px;
            height: 14px;
            background: white;
            transform: rotate(45deg);
            border-left: 1px solid #d8d8d8;
            border-top: 1px solid #d8d8d8;
        }
        #new-collection-popup input {
            width: 100%;
            padding: 9px 10px;
            border: 1px solid #d1d1d1;
            border-radius: 8px;
            font-size: 13px;
            font-family: Arial, sans-serif;
            margin-bottom: 10px;
            background: #fafafa;
        }
        #new-collection-popup input:focus {
            outline: none;
            border-color: #c21b1b;
            background: white;
        }
        #new-collection-popup button {
            position: absolute;
            right: 14px;
            bottom: 14px;
            width: 38px;
            height: 38px;
            padding: 0;
            border: none;
            border-radius: 50%;
            background: #ffffff;
            color: #111;
            cursor: pointer;
            display: inline-flex;
            align-items: center;
            justify-content: center;
            box-shadow: 0 6px 18px rgba(0,0,0,0.12);
            transition: background 120ms ease, transform 120ms ease;
        }
        #new-collection-popup button:hover {
            background: #c21b1b;
            transform: translateY(-1px);
        }
        #new-collection-popup button svg path {
            stroke: currentColor;
        }

        /* --- Removed clear button (UI requirement) --- */

        /* --- Section headers --- */
        .section-header {
            display: flex;
            justify-content: space-between;
            align-items: center;
            padding: 6px 4px;
            font-weight: bold;
            font-size: 13px;
            color: #333;
            margin: 8px 0 6px 0;
        }
        .section-header .chevron { font-size: 11px; color: #666; }
        .delete-btn {
            background: none;
            border: none;
            color: #dc3545;
         
[truncated — 24685 more characters]
```