diff --git a/.github/workflows/docker-publish.yml b/.github/workflows/docker-publish.yml
index d9e4f6b..a063f4a 100644
--- a/.github/workflows/docker-publish.yml
+++ b/.github/workflows/docker-publish.yml
@@ -2,7 +2,7 @@ name: Build and Publish Docker Image
on:
push:
- branches: [ "main" ]
+ branches: ["main"]
workflow_dispatch:
env:
diff --git a/.github/workflows/release-extension.yml b/.github/workflows/release-extension.yml
index d3356f0..222bcca 100644
--- a/.github/workflows/release-extension.yml
+++ b/.github/workflows/release-extension.yml
@@ -3,7 +3,7 @@ name: Release Extension
on:
push:
tags:
- - 'v*'
+ - "v*"
jobs:
release:
@@ -19,7 +19,7 @@ jobs:
run: |
VERSION=${GITHUB_REF_NAME#v}
echo "Updating manifests to version $VERSION"
-
+
cd extension
for manifest in manifest.json manifest.chrome.json manifest.firefox.json; do
if [ -f "$manifest" ]; then
@@ -36,7 +36,7 @@ jobs:
cp manifest.chrome.json manifest.json
zip -r ../margin-extension-chrome.zip . -x "*.DS_Store" -x "*.git*" -x "manifest.*.json"
cd ..
-
+
- name: Build Extension (Firefox)
run: |
cd extension
diff --git a/README.md b/README.md
index 6974e26..5f297eb 100644
--- a/README.md
+++ b/README.md
@@ -1,6 +1,6 @@
# Margin
-*Write in the margins of the web*
+_Write in the margins of the web_
A web comments layer built on [AT Protocol](https://atproto.com) that lets you annotate any URL on the internet.
diff --git a/backend/cmd/server/main.go b/backend/cmd/server/main.go
index f390c4f..1258b20 100644
--- a/backend/cmd/server/main.go
+++ b/backend/cmd/server/main.go
@@ -101,6 +101,8 @@ func main() {
r.Get("/{handle}/highlight/{rkey}", ogHandler.HandleAnnotationPage)
r.Get("/{handle}/bookmark/{rkey}", ogHandler.HandleAnnotationPage)
+ r.Get("/api/tags/trending", handler.HandleGetTrendingTags)
+
r.Get("/collection/{uri}", ogHandler.HandleCollectionPage)
r.Get("/{handle}/collection/{rkey}", ogHandler.HandleCollectionPage)
diff --git a/backend/internal/api/tags.go b/backend/internal/api/tags.go
new file mode 100644
index 0000000..3d2e59e
--- /dev/null
+++ b/backend/internal/api/tags.go
@@ -0,0 +1,25 @@
+package api
+
+import (
+ "encoding/json"
+ "net/http"
+ "strconv"
+)
+
+func (h *Handler) HandleGetTrendingTags(w http.ResponseWriter, r *http.Request) {
+ limit := 10
+ if l := r.URL.Query().Get("limit"); l != "" {
+ if val, err := strconv.Atoi(l); err == nil && val > 0 && val <= 50 {
+ limit = val
+ }
+ }
+
+ tags, err := h.db.GetTrendingTags(limit)
+ if err != nil {
+ http.Error(w, `{"error": "Failed to fetch trending tags: `+err.Error()+`"}`, http.StatusInternalServerError)
+ return
+ }
+
+ w.Header().Set("Content-Type", "application/json")
+ json.NewEncoder(w).Encode(tags)
+}
diff --git a/backend/internal/db/tags.go b/backend/internal/db/tags.go
new file mode 100644
index 0000000..b14779c
--- /dev/null
+++ b/backend/internal/db/tags.go
@@ -0,0 +1,46 @@
+package db
+
+type TrendingTag struct {
+ Tag string `json:"tag"`
+ Count int `json:"count"`
+}
+
+func (db *DB) GetTrendingTags(limit int) ([]TrendingTag, error) {
+ query := `
+ SELECT
+ json_each.value as tag,
+ COUNT(*) as count
+ FROM annotations, json_each(annotations.tags_json)
+ WHERE tags_json IS NOT NULL
+ AND tags_json != ''
+ AND tags_json != '[]'
+ GROUP BY tag
+ ORDER BY count DESC
+ LIMIT ?
+ `
+
+ rows, err := db.Query(db.Rebind(query), limit)
+ if err != nil {
+ return nil, err
+ }
+ defer rows.Close()
+
+ var tags []TrendingTag
+ for rows.Next() {
+ var t TrendingTag
+ if err := rows.Scan(&t.Tag, &t.Count); err != nil {
+ return nil, err
+ }
+ tags = append(tags, t)
+ }
+
+ if err = rows.Err(); err != nil {
+ return nil, err
+ }
+
+ if tags == nil {
+ return []TrendingTag{}, nil
+ }
+
+ return tags, nil
+}
diff --git a/extension/background/service-worker.js b/extension/background/service-worker.js
index 4ff44e5..a3a2b6f 100644
--- a/extension/background/service-worker.js
+++ b/extension/background/service-worker.js
@@ -334,9 +334,14 @@ async function handleMessage(request, sender, sendResponse) {
}
case "GET_ANNOTATIONS": {
+ const stored = await chrome.storage.local.get(["apiUrl"]);
+ const currentApiUrl = stored.apiUrl
+ ? stored.apiUrl.replace(/\/$/, "")
+ : API_BASE;
+
const pageUrl = request.data.url;
const res = await fetch(
- `${API_BASE}/api/targets?source=${encodeURIComponent(pageUrl)}`,
+ `${currentApiUrl}/api/targets?source=${encodeURIComponent(pageUrl)}`,
);
const data = await res.json();
@@ -422,7 +427,6 @@ async function handleMessage(request, sender, sendResponse) {
return;
}
const { url, selector } = request.data;
-
let composeUrl = `${WEB_BASE}/new?url=${encodeURIComponent(url)}`;
if (selector) {
composeUrl += `&selector=${encodeURIComponent(JSON.stringify(selector))}`;
@@ -431,6 +435,25 @@ async function handleMessage(request, sender, sendResponse) {
break;
}
+ case "OPEN_APP_URL": {
+ if (!WEB_BASE) {
+ chrome.runtime.openOptionsPage();
+ return;
+ }
+ const path = request.data.path;
+ const safePath = path.startsWith("/") ? path : `/${path}`;
+ chrome.tabs.create({ url: `${WEB_BASE}${safePath}` });
+ break;
+ }
+
+ case "OPEN_SIDE_PANEL":
+ if (sender.tab && sender.tab.windowId) {
+ chrome.sidePanel
+ .open({ windowId: sender.tab.windowId })
+ .catch((err) => console.error("Failed to open side panel", err));
+ }
+ break;
+
case "CREATE_BOOKMARK": {
if (!API_BASE) {
sendResponse({ success: false, error: "API URL not configured" });
diff --git a/extension/content/content.js b/extension/content/content.js
index 6271643..76f9166 100644
--- a/extension/content/content.js
+++ b/extension/content/content.js
@@ -1,318 +1,701 @@
(() => {
- function buildTextQuoteSelector(selection) {
- const exact = selection.toString().trim();
- if (!exact) return null;
-
- const range = selection.getRangeAt(0);
- const contextLength = 32;
-
- let prefix = "";
- try {
- const preRange = document.createRange();
- preRange.selectNodeContents(document.body);
- preRange.setEnd(range.startContainer, range.startOffset);
- const preText = preRange.toString();
- prefix = preText.slice(-contextLength).trim();
- } catch (e) {
- console.warn("Could not get prefix:", e);
- }
-
- let suffix = "";
- try {
- const postRange = document.createRange();
- postRange.selectNodeContents(document.body);
- postRange.setStart(range.endContainer, range.endOffset);
- const postText = postRange.toString();
- suffix = postText.slice(0, contextLength).trim();
- } catch (e) {
- console.warn("Could not get suffix:", e);
- }
-
- return {
- type: "TextQuoteSelector",
- exact: exact,
- prefix: prefix || undefined,
- suffix: suffix || undefined,
- };
- }
+ let sidebarHost = null;
+ let sidebarShadow = null;
+ let popoverEl = null;
+
+ let activeItems = [];
+
+ let hoveredItems = [];
+ let tooltipEl = null;
+ let hideTimer = null;
+
+ const OVERLAY_STYLES = `
+ :host { all: initial; }
+ .margin-overlay {
+ position: absolute;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 100%;
+ pointer-events: none;
+ }
+ .margin-badge {
+ position: absolute;
+ background: #6366f1;
+ color: white;
+ padding: 4px 10px;
+ border-radius: 99px;
+ font-size: 11px;
+ font-weight: 600;
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
+ cursor: pointer;
+ pointer-events: auto;
+ box-shadow: 0 2px 8px rgba(99, 102, 241, 0.3);
+ display: flex;
+ align-items: center;
+ gap: 6px;
+ transform: translateY(-120%);
+ white-space: nowrap;
+ transition: transform 0.15s, background-color 0.15s;
+ z-index: 2147483647;
+ }
+ .margin-badge:hover {
+ transform: translateY(-125%) scale(1.05);
+ background: #4f46e5;
+ z-index: 2147483647;
+ }
+ .margin-badge-avatar {
+ width: 16px;
+ height: 16px;
+ border-radius: 50%;
+ background: rgba(255,255,255,0.2);
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ font-size: 9px;
+ object-fit: cover;
+ }
+ .margin-badge-stack {
+ display: flex;
+ align-items: center;
+ }
+ .margin-badge-stack .margin-badge-avatar {
+ margin-left: -6px;
+ border: 1px solid #6366f1;
+ }
+ .margin-badge-stack .margin-badge-avatar:first-child {
+ margin-left: 0;
+ }
+ .margin-badge-stem {
+ position: absolute;
+ left: 14px;
+ bottom: -6px;
+ width: 2px;
+ height: 6px;
+ background: #6366f1;
+ border-radius: 2px;
+ }
- function findAndScrollToText(selector) {
- if (!selector || !selector.exact) return false;
+ .margin-popover {
+ position: absolute;
+ width: 320px;
+ background: #09090b;
+ border: 1px solid #27272a;
+ border-radius: 12px;
+ padding: 0;
+ box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.5), 0 10px 10px -5px rgba(0, 0, 0, 0.2);
+ display: flex;
+ flex-direction: column;
+ pointer-events: auto;
+ z-index: 2147483647;
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
+ color: #e4e4e7;
+ opacity: 0;
+ transform: scale(0.95);
+ animation: popover-in 0.15s forwards;
+ max-height: 480px;
+ overflow: hidden;
+ }
+ @keyframes popover-in { to { opacity: 1; transform: scale(1); } }
+ .popover-header {
+ padding: 12px 16px;
+ border-bottom: 1px solid #27272a;
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ background: #0f0f12;
+ border-radius: 12px 12px 0 0;
+ font-weight: 600;
+ font-size: 13px;
+ }
+ .popover-scroll-area {
+ overflow-y: auto;
+ max-height: 400px;
+ }
+ .popover-item-block {
+ border-bottom: 1px solid #27272a;
+ margin-bottom: 0;
+ animation: fade-in 0.2s;
+ }
+ .popover-item-block:last-child {
+ border-bottom: none;
+ }
+ .popover-item-header {
+ padding: 12px 16px 4px;
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ }
+ .popover-avatar {
+ width: 24px; height: 24px; border-radius: 50%; background: #27272a;
+ display: flex; align-items: center; justify-content: center;
+ font-size: 10px; color: #a1a1aa;
+ }
+ .popover-handle { font-size: 12px; font-weight: 600; color: #e4e4e7; }
+ .popover-close { background: none; border: none; color: #71717a; cursor: pointer; padding: 4px; }
+ .popover-close:hover { color: #e4e4e7; }
+ .popover-content { padding: 4px 16px 12px; font-size: 13px; line-height: 1.5; color: #e4e4e7; }
+ .popover-quote {
+ margin-top: 8px; padding: 6px 10px; background: #18181b;
+ border-left: 2px solid #6366f1; border-radius: 4px;
+ font-size: 11px; color: #a1a1aa; font-style: italic;
+ }
+ .popover-actions {
+ padding: 8px 16px;
+ display: flex; justify-content: flex-end; gap: 8px;
+ }
+ .btn-action {
+ background: none; border: 1px solid #27272a; border-radius: 4px;
+ padding: 4px 8px; color: #a1a1aa; font-size: 11px; cursor: pointer;
+ }
+ .btn-action:hover { background: #27272a; color: #e4e4e7; }
+ `;
+
+ class DOMTextMatcher {
+ constructor() {
+ this.textNodes = [];
+ this.corpus = "";
+ this.indices = [];
+ this.buildMap();
+ }
- const searchText = selector.exact.trim();
- const normalizedSearch = searchText.replace(/\s+/g, " ");
+ buildMap() {
+ const walker = document.createTreeWalker(
+ document.body,
+ NodeFilter.SHOW_TEXT,
+ {
+ acceptNode: (node) => {
+ if (!node.parentNode) return NodeFilter.FILTER_REJECT;
+ const tag = node.parentNode.tagName;
+ if (
+ ["SCRIPT", "STYLE", "NOSCRIPT", "TEXTAREA", "INPUT"].includes(tag)
+ )
+ return NodeFilter.FILTER_REJECT;
+ if (node.textContent.trim().length === 0)
+ return NodeFilter.FILTER_SKIP;
+
+ if (node.parentNode.offsetParent === null)
+ return NodeFilter.FILTER_REJECT;
+
+ return NodeFilter.FILTER_ACCEPT;
+ },
+ },
+ );
- const treeWalker = document.createTreeWalker(
- document.body,
- NodeFilter.SHOW_TEXT,
- null,
- false,
- );
+ let currentNode;
+ let index = 0;
+ while ((currentNode = walker.nextNode())) {
+ const text = currentNode.textContent;
+ this.textNodes.push(currentNode);
+ this.corpus += text;
+ this.indices.push({
+ start: index,
+ node: currentNode,
+ length: text.length,
+ });
+ index += text.length;
+ }
+ }
- let currentNode;
- while ((currentNode = treeWalker.nextNode())) {
- const nodeText = currentNode.textContent;
- const normalizedNode = nodeText.replace(/\s+/g, " ");
+ findRange(searchText) {
+ if (!searchText) return null;
- let index = nodeText.indexOf(searchText);
+ let matchIndex = this.corpus.indexOf(searchText);
- if (index === -1) {
- const normIndex = normalizedNode.indexOf(normalizedSearch);
- if (normIndex !== -1) {
- index = nodeText.indexOf(searchText.substring(0, 20));
- if (index === -1) index = 0;
- }
+ if (matchIndex === -1) {
+ const cleaned = searchText.replace(/\s+/g, " ");
+ return null;
}
- if (index !== -1 && nodeText.trim().length > 0) {
- try {
- const range = document.createRange();
- const endIndex = Math.min(index + searchText.length, nodeText.length);
- range.setStart(currentNode, index);
- range.setEnd(currentNode, endIndex);
-
- if (typeof CSS !== "undefined" && CSS.highlights) {
- const highlight = new Highlight(range);
- CSS.highlights.set("margin-scroll-highlight", highlight);
-
- setTimeout(() => {
- CSS.highlights.delete("margin-scroll-highlight");
- }, 3000);
- }
-
- const rect = range.getBoundingClientRect();
- window.scrollTo({
- top: window.scrollY + rect.top - window.innerHeight / 3,
- behavior: "smooth",
- });
+ const start = this.mapIndexToPoint(matchIndex);
+ const end = this.mapIndexToPoint(matchIndex + searchText.length);
- window.scrollTo({
- top: window.scrollY + rect.top - window.innerHeight / 3,
- behavior: "smooth",
- });
-
- return true;
- } catch (e) {
- console.warn("Could not create range:", e);
- }
+ if (start && end) {
+ const range = document.createRange();
+ range.setStart(start.node, start.offset);
+ range.setEnd(end.node, end.offset);
+ return range;
}
+ return null;
}
- if (window.find) {
- window.getSelection()?.removeAllRanges();
- const found = window.find(searchText, false, false, true, false);
- if (found) {
- const selection = window.getSelection();
- if (selection && selection.rangeCount > 0) {
- const range = selection.getRangeAt(0);
- const rect = range.getBoundingClientRect();
- window.scrollTo({
- top: window.scrollY + rect.top - window.innerHeight / 3,
- behavior: "smooth",
- });
+ mapIndexToPoint(corpusIndex) {
+ for (const info of this.indices) {
+ if (
+ corpusIndex >= info.start &&
+ corpusIndex < info.start + info.length
+ ) {
+ return { node: info.node, offset: corpusIndex - info.start };
+ }
+ }
+ if (this.indices.length > 0) {
+ const last = this.indices[this.indices.length - 1];
+ if (corpusIndex === last.start + last.length) {
+ return { node: last.node, offset: last.length };
}
- return true;
}
+ return null;
}
-
- return false;
}
- function renderPageHighlights(highlights) {
- if (!highlights || !Array.isArray(highlights) || !CSS.highlights) return;
+ function initOverlay() {
+ sidebarHost = document.createElement("div");
+ sidebarHost.id = "margin-overlay-host";
+ const getScrollHeight = () => {
+ const bodyH = document.body?.scrollHeight || 0;
+ const docH = document.documentElement?.scrollHeight || 0;
+ return Math.max(bodyH, docH);
+ };
+
+ sidebarHost.style.cssText = `
+ position: absolute; top: 0; left: 0; width: 100%;
+ height: ${getScrollHeight()}px;
+ pointer-events: none; z-index: 2147483647;
+ `;
+ document.body?.appendChild(sidebarHost) ||
+ document.documentElement.appendChild(sidebarHost);
- const ranges = [];
+ sidebarShadow = sidebarHost.attachShadow({ mode: "open" });
+ const styleEl = document.createElement("style");
+ styleEl.textContent = OVERLAY_STYLES;
+ sidebarShadow.appendChild(styleEl);
- highlights.forEach((item) => {
- const selector = item.target?.selector;
- if (!selector?.exact) return;
+ const container = document.createElement("div");
+ container.className = "margin-overlay";
+ container.id = "margin-overlay-container";
+ sidebarShadow.appendChild(container);
- const searchText = selector.exact;
- const treeWalker = document.createTreeWalker(
- document.body,
- NodeFilter.SHOW_TEXT,
- null,
- false,
- );
+ createTooltip(container);
- let currentNode;
- while ((currentNode = treeWalker.nextNode())) {
- const nodeText = currentNode.textContent;
- const index = nodeText.indexOf(searchText);
-
- if (index !== -1) {
- try {
- const range = document.createRange();
- range.setStart(currentNode, index);
- range.setEnd(currentNode, index + searchText.length);
- ranges.push(range);
- } catch (e) {
- console.warn("Could not create range for highlight:", e);
- }
- break;
- }
- }
+ const observer = new ResizeObserver(() => {
+ sidebarHost.style.height = `${getScrollHeight()}px`;
});
+ if (document.body) observer.observe(document.body);
+ if (document.documentElement) observer.observe(document.documentElement);
- if (ranges.length > 0) {
- const highlight = new Highlight(...ranges);
- CSS.highlights.set("margin-page-highlights", highlight);
- }
+ fetchAnnotations();
+
+ document.addEventListener("mousemove", handleMouseMove);
+ document.addEventListener("click", handleDocumentClick);
}
- chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {
- if (request.type === "GET_SELECTOR_FOR_ANNOTATE_INLINE") {
- const selection = window.getSelection();
- if (!selection || selection.toString().trim().length === 0) {
- sendResponse({ selector: null });
- return true;
+ function createTooltip(container) {
+ tooltipEl = document.createElement("div");
+ tooltipEl.className = "margin-badge";
+ tooltipEl.style.opacity = "0";
+ tooltipEl.style.transition = "opacity 0.1s, transform 0.1s";
+ tooltipEl.style.pointerEvents = "auto";
+
+ tooltipEl.addEventListener("click", (e) => {
+ e.stopPropagation();
+ if (hoveredItems.length > 0) {
+ const firstItem = hoveredItems[0];
+ const rect = activeItems
+ .find((x) => x.item === firstItem)
+ ?.range.getBoundingClientRect();
+ if (rect) {
+ const top = rect.top + window.scrollY;
+ const left = rect.left + window.scrollX;
+ showPopover(hoveredItems, top, left);
+ }
}
+ });
+ container.appendChild(tooltipEl);
+ }
- const selector = buildTextQuoteSelector(selection);
- sendResponse({ selector: selector });
- return true;
+ function handleMouseMove(e) {
+ const x = e.clientX;
+ const y = e.clientY;
+
+ let foundItems = [];
+
+ for (const { range, item } of activeItems) {
+ const rects = range.getClientRects();
+ for (const rect of rects) {
+ const padding = 5;
+ if (
+ x >= rect.left - padding &&
+ x <= rect.right + padding &&
+ y >= rect.top - padding &&
+ y <= rect.bottom + padding
+ ) {
+ if (!foundItems.includes(item)) {
+ foundItems.push(item);
+ }
+ break;
+ }
+ }
}
- if (request.type === "GET_SELECTOR_FOR_ANNOTATE") {
- const selection = window.getSelection();
- if (!selection || selection.toString().trim().length === 0) {
- return;
+ let isOverTooltip = false;
+ if (tooltipEl && tooltipEl.style.opacity === "1") {
+ const rect = tooltipEl.getBoundingClientRect();
+ if (
+ x >= rect.left &&
+ x <= rect.right &&
+ y >= rect.top &&
+ y <= rect.bottom
+ ) {
+ isOverTooltip = true;
}
+ }
- const selector = buildTextQuoteSelector(selection);
- if (selector) {
- chrome.runtime.sendMessage({
- type: "OPEN_COMPOSE",
- data: {
- url: window.location.href,
- selector: selector,
- },
- });
+ if (foundItems.length > 0 || isOverTooltip) {
+ if (hideTimer) {
+ clearTimeout(hideTimer);
+ hideTimer = null;
+ }
+ if (foundItems.length > 0) {
+ const currentIds = hoveredItems
+ .map((i) => i.id || i.cid)
+ .sort()
+ .join(",");
+ const newIds = foundItems
+ .map((i) => i.id || i.cid)
+ .sort()
+ .join(",");
+
+ if (currentIds !== newIds) {
+ hoveredItems = foundItems;
+ updateTooltip();
+ }
}
+ } else {
+ if (!hideTimer && hoveredItems.length > 0) {
+ hideTimer = setTimeout(() => {
+ hoveredItems = [];
+ updateTooltip();
+ hideTimer = null;
+ }, 300);
+ }
+ }
+ }
+
+ function updateTooltip() {
+ if (!tooltipEl) return;
+
+ if (hoveredItems.length === 0) {
+ tooltipEl.style.opacity = "0";
+ tooltipEl.style.transform = "translateY(-105%) scale(0.9)";
+ tooltipEl.style.pointerEvents = "none";
+ return;
}
- if (request.type === "GET_SELECTOR_FOR_HIGHLIGHT") {
- const selection = window.getSelection();
- if (!selection || selection.toString().trim().length === 0) {
- sendResponse({ success: false, error: "No text selected" });
- return true;
+ tooltipEl.style.pointerEvents = "auto";
+
+ const authorsMap = new Map();
+ hoveredItems.forEach((item) => {
+ const author = item.author || item.creator || {};
+ const id = author.did || author.handle;
+ if (id && !authorsMap.has(id)) {
+ authorsMap.set(id, author);
}
+ });
- const selector = buildTextQuoteSelector(selection);
- if (selector) {
- chrome.runtime
- .sendMessage({
- type: "CREATE_HIGHLIGHT",
- data: {
- url: window.location.href,
- title: document.title,
- selector: selector,
- },
- })
- .then((response) => {
- if (response?.success) {
- showNotification("Text highlighted!", "success");
-
- if (CSS.highlights) {
- try {
- const range = selection.getRangeAt(0);
- const highlight = new Highlight(range);
- CSS.highlights.set("margin-highlight-preview", highlight);
- } catch (e) {
- console.warn("Could not visually highlight:", e);
- }
- }
-
- window.getSelection().removeAllRanges();
- } else {
- showNotification(
- "Failed to highlight: " + (response?.error || "Unknown error"),
- "error",
- );
- }
- sendResponse(response);
- })
- .catch((err) => {
- console.error("Highlight error:", err);
- showNotification("Error creating highlight", "error");
- sendResponse({ success: false, error: err.message });
- });
- return true;
+ const uniqueAuthors = Array.from(authorsMap.values());
+ let contentHtml = "";
+
+ if (uniqueAuthors.length === 1) {
+ const author = uniqueAuthors[0] || {};
+ const handle = author.handle || "User";
+ const avatar = author.avatar;
+ const count = hoveredItems.length;
+
+ let avatarHtml = `
-
-
-
- } />
- } />
- } />
- } />
- } />
- } />
- } />
- } />
- {}
- } />
- {}
- } />
- } />
- } />
- }
- />
-
- }
- />
- }
- />
- }
- />
-
- } />
- } />
-
-
+
+
+
+
+
+ } />
+ } />
+ } />
+ } />
+ } />
+ } />
+ } />
+ } />
+ } />
+ } />
+ } />
+ } />
+ }
+ />
+ }
+ />
+ }
+ />
+ }
+ />
+ } />
+ } />
+ } />
+
+
+
+
+
);
}
diff --git a/web/src/api/client.js b/web/src/api/client.js
index 45a28af..e97269d 100644
--- a/web/src/api/client.js
+++ b/web/src/api/client.js
@@ -427,3 +427,6 @@ export async function startLogin(handle, inviteCode) {
body: JSON.stringify({ handle, invite_code: inviteCode }),
});
}
+export async function getTrendingTags(limit = 10) {
+ return request(`${API_BASE}/tags/trending?limit=${limit}`);
+}
diff --git a/web/src/assets/tangled.svg b/web/src/assets/tangled.svg
new file mode 100644
index 0000000..8c5dd86
--- /dev/null
+++ b/web/src/assets/tangled.svg
@@ -0,0 +1,154 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/web/src/components/AnnotationSkeleton.jsx b/web/src/components/AnnotationSkeleton.jsx
new file mode 100644
index 0000000..538b827
--- /dev/null
+++ b/web/src/components/AnnotationSkeleton.jsx
@@ -0,0 +1,28 @@
+import React from "react";
+
+export default function AnnotationSkeleton() {
+ return (
+
+ );
+}
diff --git a/web/src/components/MobileNav.jsx b/web/src/components/MobileNav.jsx
new file mode 100644
index 0000000..2575351
--- /dev/null
+++ b/web/src/components/MobileNav.jsx
@@ -0,0 +1,61 @@
+import { Link, useLocation } from "react-router-dom";
+import { useAuth } from "../context/AuthContext";
+import { Home, Search, Folder, User, PenSquare } from "lucide-react";
+
+export default function MobileNav() {
+ const { user, isAuthenticated } = useAuth();
+ const location = useLocation();
+
+ const isActive = (path) => {
+ if (path === "/") return location.pathname === "/";
+ return location.pathname.startsWith(path);
+ };
+
+ return (
+
+
+
+
+
Home
+
+
+
+
+
Browse
+
+
+ {isAuthenticated ? (
+
+
+
+ ) : (
+
+
+
+ )}
+
+
+
+
Library
+
+
+
+
+
Profile
+
+
+
+ );
+}
diff --git a/web/src/components/Navbar.jsx b/web/src/components/Navbar.jsx
deleted file mode 100644
index 94236b2..0000000
--- a/web/src/components/Navbar.jsx
+++ /dev/null
@@ -1,245 +0,0 @@
-import { useState, useRef, useEffect } from "react";
-import { Link, useLocation } from "react-router-dom";
-import { Folder } from "lucide-react";
-import { useAuth } from "../context/AuthContext";
-import {
- PenIcon,
- BookmarkIcon,
- HighlightIcon,
- SearchIcon,
- LogoutIcon,
- BellIcon,
-} from "./Icons";
-import { getUnreadNotificationCount } from "../api/client";
-import { SiFirefox, SiGooglechrome } from "react-icons/si";
-import { FaEdge } from "react-icons/fa";
-
-import logo from "../assets/logo.svg";
-
-const isFirefox =
- typeof navigator !== "undefined" && /Firefox/i.test(navigator.userAgent);
-const isEdge =
- typeof navigator !== "undefined" && /Edg/i.test(navigator.userAgent);
-const isChrome =
- typeof navigator !== "undefined" &&
- /Chrome/i.test(navigator.userAgent) &&
- !isEdge;
-
-export default function Navbar() {
- const { user, isAuthenticated, logout, loading } = useAuth();
- const location = useLocation();
- const [menuOpen, setMenuOpen] = useState(false);
- const [unreadCount, setUnreadCount] = useState(0);
- const menuRef = useRef(null);
-
- const isActive = (path) => location.pathname === path;
-
- useEffect(() => {
- if (isAuthenticated) {
- getUnreadNotificationCount()
- .then((data) => setUnreadCount(data.count || 0))
- .catch(() => {});
- const interval = setInterval(() => {
- getUnreadNotificationCount()
- .then((data) => setUnreadCount(data.count || 0))
- .catch(() => {});
- }, 60000);
- return () => clearInterval(interval);
- }
- }, [isAuthenticated]);
-
- useEffect(() => {
- const handleClickOutside = (e) => {
- if (menuRef.current && !menuRef.current.contains(e.target)) {
- setMenuOpen(false);
- }
- };
- document.addEventListener("mousedown", handleClickOutside);
- return () => document.removeEventListener("mousedown", handleClickOutside);
- }, []);
-
- const getInitials = () => {
- if (user?.displayName) {
- return user.displayName.substring(0, 2).toUpperCase();
- }
- if (user?.handle) {
- return user.handle.substring(0, 2).toUpperCase();
- }
- return "U";
- };
-
- return (
-
-
- {}
-
-
-
Margin
-
-
- {}
-
-
- {}
-
- {!loading &&
- (isAuthenticated ? (
- <>
-
-
-
-
-
-
-
-
-
-
setUnreadCount(0)}
- >
-
- {unreadCount > 0 && (
-
{unreadCount}
- )}
-
-
-
-
New
-
-
- {}
-
-
setMenuOpen(!menuOpen)}
- title={user?.handle}
- >
- {user?.avatar ? (
-
- ) : (
-
- {getInitials()}
-
- )}
-
-
- {menuOpen && (
-
-
-
- {user?.displayName}
-
-
- @{user?.handle}
-
-
-
-
setMenuOpen(false)}
- >
- View Profile
-
-
{
- logout();
- setMenuOpen(false);
- }}
- className="navbar-dropdown-item navbar-dropdown-logout"
- >
-
- Sign Out
-
-
- )}
-
- >
- ) : (
-
- Sign In
-
- ))}
-
-
-
- );
-}
diff --git a/web/src/components/RightSidebar.jsx b/web/src/components/RightSidebar.jsx
new file mode 100644
index 0000000..8b74227
--- /dev/null
+++ b/web/src/components/RightSidebar.jsx
@@ -0,0 +1,161 @@
+import { useState, useEffect } from "react";
+import { Link } from "react-router-dom";
+import { Download, ExternalLink } from "lucide-react";
+import { SiFirefox, SiGooglechrome, SiGithub, SiBluesky } from "react-icons/si";
+import { FaEdge } from "react-icons/fa";
+import { useAuth } from "../context/AuthContext";
+import { getTrendingTags } from "../api/client";
+import tangledIcon from "../assets/tangled.svg";
+
+const isFirefox =
+ typeof navigator !== "undefined" && /Firefox/i.test(navigator.userAgent);
+const isEdge =
+ typeof navigator !== "undefined" && /Edg/i.test(navigator.userAgent);
+const isChrome =
+ typeof navigator !== "undefined" &&
+ /Chrome/i.test(navigator.userAgent) &&
+ !isEdge;
+
+function getExtensionInfo() {
+ if (isFirefox) {
+ return {
+ url: "https://addons.mozilla.org/en-US/firefox/addon/margin/",
+ icon: SiFirefox,
+ name: "Firefox",
+ };
+ }
+ if (isEdge) {
+ return {
+ url: "https://microsoftedge.microsoft.com/addons/detail/margin/nfjnmllpdgcdnhmmggjihjbidmeadddn",
+ icon: FaEdge,
+ name: "Edge",
+ };
+ }
+ return {
+ url: "https://chromewebstore.google.com/detail/margin/cgpmbiiagnehkikhcbnhiagfomajncpa/",
+ icon: SiGooglechrome,
+ name: "Chrome",
+ };
+}
+
+export default function RightSidebar() {
+ const { isAuthenticated } = useAuth();
+ const ext = getExtensionInfo();
+ const ExtIcon = ext.icon;
+ const [trendingTags, setTrendingTags] = useState([]);
+ const [loading, setLoading] = useState(true);
+
+ useEffect(() => {
+ getTrendingTags()
+ .then((tags) => setTrendingTags(tags))
+ .catch((err) => console.error("Failed to fetch trending tags:", err))
+ .finally(() => setLoading(false));
+ }, []);
+
+ return (
+
+
+
+ {isAuthenticated ? (
+
+
Trending Tags
+
+ {loading ? (
+ Loading...
+ ) : trendingTags.length > 0 ? (
+ trendingTags.map(({ tag, count }) => (
+
+ #{tag}
+
+ {count}
+
+
+ ))
+ ) : (
+ No trending tags yet
+ )}
+
+
+ ) : (
+
+
Explore
+
+
+ Browse by URL
+
+
+ Public Highlights
+
+
+
+ )}
+
+
+
+
+ Privacy
+ ·
+ Terms
+
+
+ );
+}
diff --git a/web/src/components/Sidebar.jsx b/web/src/components/Sidebar.jsx
new file mode 100644
index 0000000..56c46ef
--- /dev/null
+++ b/web/src/components/Sidebar.jsx
@@ -0,0 +1,189 @@
+import { useState, useRef, useEffect } from "react";
+import { Link, useLocation } from "react-router-dom";
+import { useAuth } from "../context/AuthContext";
+import {
+ Home,
+ Search,
+ Folder,
+ Bell,
+ PenSquare,
+ User,
+ LogOut,
+ MoreHorizontal,
+ Highlighter,
+ Bookmark,
+} from "lucide-react";
+import { getUnreadNotificationCount } from "../api/client";
+import logo from "../assets/logo.svg";
+
+export default function Sidebar() {
+ const { user, isAuthenticated, logout, loading } = useAuth();
+ const location = useLocation();
+ const [menuOpen, setMenuOpen] = useState(false);
+ const [unreadCount, setUnreadCount] = useState(0);
+ const menuRef = useRef(null);
+
+ const isActive = (path) => {
+ if (path === "/") return location.pathname === "/";
+ return location.pathname.startsWith(path);
+ };
+
+ useEffect(() => {
+ if (isAuthenticated) {
+ getUnreadNotificationCount()
+ .then((data) => setUnreadCount(data.count || 0))
+ .catch(() => {});
+ const interval = setInterval(() => {
+ getUnreadNotificationCount()
+ .then((data) => setUnreadCount(data.count || 0))
+ .catch(() => {});
+ }, 60000);
+ return () => clearInterval(interval);
+ }
+ }, [isAuthenticated]);
+
+ useEffect(() => {
+ const handleClickOutside = (e) => {
+ if (menuRef.current && !menuRef.current.contains(e.target)) {
+ setMenuOpen(false);
+ }
+ };
+ document.addEventListener("mousedown", handleClickOutside);
+ return () => document.removeEventListener("mousedown", handleClickOutside);
+ }, []);
+
+ const getInitials = () => {
+ if (user?.displayName) {
+ return user.displayName.substring(0, 2).toUpperCase();
+ }
+ if (user?.handle) {
+ return user.handle.substring(0, 2).toUpperCase();
+ }
+ return "U";
+ };
+
+ return (
+
+ );
+}
diff --git a/web/src/css/annotations.css b/web/src/css/annotations.css
new file mode 100644
index 0000000..120dfe9
--- /dev/null
+++ b/web/src/css/annotations.css
@@ -0,0 +1,473 @@
+.annotation-detail-page {
+ max-width: 680px;
+ margin: 0 auto;
+ padding: 24px 16px;
+ min-height: 100vh;
+}
+
+.annotation-detail-header {
+ margin-bottom: 24px;
+}
+
+.back-link {
+ display: inline-flex;
+ align-items: center;
+ color: var(--text-tertiary);
+ text-decoration: none;
+ font-size: 0.9rem;
+ font-weight: 500;
+ transition: color 0.15s;
+}
+
+.back-link:hover {
+ color: var(--text-primary);
+}
+
+.replies-section {
+ margin-top: 32px;
+ border-top: 1px solid var(--border);
+ padding-top: 24px;
+}
+
+.replies-title {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ font-size: 1.1rem;
+ font-weight: 600;
+ color: var(--text-primary);
+ margin-bottom: 20px;
+}
+
+.annotation-card {
+ display: flex;
+ flex-direction: column;
+ gap: 12px;
+ padding: 20px 0;
+ border-bottom: 1px solid var(--border);
+ transition: background 0.15s ease;
+}
+
+.annotation-card:last-child {
+ border-bottom: none;
+}
+
+.annotation-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: flex-start;
+ gap: 12px;
+}
+
+.annotation-header-left {
+ display: flex;
+ align-items: center;
+ gap: 10px;
+ flex: 1;
+ min-width: 0;
+}
+
+.annotation-avatar {
+ width: 36px;
+ height: 36px;
+ min-width: 36px;
+ border-radius: 50%;
+ background: var(--bg-tertiary);
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ font-weight: 600;
+ font-size: 0.85rem;
+ color: var(--text-secondary);
+ overflow: hidden;
+}
+
+.annotation-avatar img {
+ width: 100%;
+ height: 100%;
+ object-fit: cover;
+}
+
+.annotation-meta {
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+ line-height: 1.3;
+}
+
+.annotation-avatar-link {
+ text-decoration: none;
+ border-radius: 50%;
+}
+
+.annotation-author-row {
+ display: flex;
+ align-items: baseline;
+ gap: 6px;
+ flex-wrap: wrap;
+}
+
+.annotation-author {
+ font-weight: 600;
+ color: var(--text-primary);
+ font-size: 0.9rem;
+}
+
+.annotation-handle {
+ font-size: 0.85rem;
+ color: var(--text-tertiary);
+ text-decoration: none;
+}
+
+.annotation-handle:hover {
+ color: var(--text-secondary);
+}
+
+.annotation-time {
+ font-size: 0.75rem;
+ color: var(--text-tertiary);
+}
+
+.annotation-content {
+ display: flex;
+ flex-direction: column;
+ gap: 10px;
+ padding-left: 46px;
+}
+
+.annotation-source {
+ display: inline-flex;
+ align-items: center;
+ gap: 6px;
+ font-size: 0.75rem;
+ color: var(--text-tertiary);
+ text-decoration: none;
+ transition: color 0.15s ease;
+ max-width: 100%;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+
+.annotation-source:hover {
+ color: var(--text-secondary);
+ text-decoration: underline;
+}
+
+.annotation-source-title {
+ color: var(--text-tertiary);
+ opacity: 0.7;
+}
+
+.annotation-highlight {
+ display: block;
+ position: relative;
+ padding-left: 12px;
+ margin: 4px 0;
+ text-decoration: none;
+ border-left: 2px solid var(--border);
+ transition: all 0.15s ease;
+}
+
+.annotation-highlight:hover {
+ border-left-color: var(--text-secondary);
+}
+
+.annotation-highlight mark {
+ background: transparent;
+ color: var(--text-primary);
+ font-style: italic;
+ font-size: 1rem;
+ line-height: 1.6;
+ font-weight: 400;
+ font-family: var(--font-serif, var(--font-sans));
+ display: inline;
+}
+
+.annotation-text {
+ font-size: 0.95rem;
+ line-height: 1.6;
+ color: var(--text-primary);
+ white-space: pre-wrap;
+}
+
+.annotation-tags {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 6px;
+ margin-top: 4px;
+}
+
+.annotation-tag {
+ font-size: 0.8rem;
+ color: var(--accent);
+ text-decoration: none;
+ font-weight: 500;
+ opacity: 0.9;
+ transition: opacity 0.15s;
+}
+
+.annotation-tag:hover {
+ opacity: 1;
+ text-decoration: underline;
+}
+
+.annotation-actions {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ margin-top: 4px;
+ padding-left: 46px;
+}
+
+.annotation-actions-left {
+ display: flex;
+ align-items: center;
+ gap: 16px;
+}
+
+.annotation-action {
+ display: flex;
+ align-items: center;
+ gap: 6px;
+ color: var(--text-tertiary);
+ font-size: 0.8rem;
+ font-weight: 500;
+ padding: 6px;
+ margin-left: -6px;
+ border-radius: var(--radius-sm);
+ transition: all 0.15s ease;
+ background: transparent;
+ cursor: pointer;
+ border: none;
+}
+
+.annotation-action:hover {
+ color: var(--text-secondary);
+ background: var(--bg-tertiary);
+}
+
+.annotation-action.liked {
+ color: #ef4444;
+}
+
+.annotation-action.liked svg {
+ fill: #ef4444;
+}
+
+.annotation-action.active {
+ color: var(--accent);
+}
+
+.action-icon-only {
+ padding: 6px;
+}
+
+.annotation-header-right {
+ opacity: 0;
+ transition: opacity 0.15s;
+}
+
+.annotation-card:hover .annotation-header-right {
+ opacity: 1;
+}
+
+.inline-replies {
+ margin-top: 12px;
+ padding-left: 46px;
+}
+
+@media (max-width: 600px) {
+ .annotation-content,
+ .annotation-actions,
+ .inline-replies {
+ padding-left: 0;
+ }
+
+ .annotation-header-right {
+ opacity: 1;
+ }
+}
+
+.replies-list-threaded {
+ margin-top: 16px;
+ display: flex;
+ flex-direction: column;
+}
+
+.reply-card-threaded {
+ position: relative;
+ padding-left: 0;
+ transition: background 0.15s;
+}
+
+.reply-header {
+ display: flex;
+ align-items: center;
+ gap: 10px;
+ margin-bottom: 6px;
+}
+
+.reply-avatar {
+ width: 28px;
+ height: 28px;
+ border-radius: 50%;
+ background: var(--bg-tertiary);
+ overflow: hidden;
+ flex-shrink: 0;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+}
+
+.reply-avatar img {
+ width: 100%;
+ height: 100%;
+ object-fit: cover;
+}
+
+.reply-avatar span {
+ font-size: 0.7rem;
+ font-weight: 600;
+ color: var(--text-secondary);
+}
+
+.reply-meta {
+ display: flex;
+ align-items: baseline;
+ gap: 6px;
+ flex: 1;
+ min-width: 0;
+}
+
+.reply-author {
+ font-weight: 600;
+ font-size: 0.85rem;
+ color: var(--text-primary);
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+}
+
+.reply-handle {
+ font-size: 0.8rem;
+ color: var(--text-tertiary);
+ text-decoration: none;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+}
+
+.reply-time {
+ font-size: 0.75rem;
+ color: var(--text-tertiary);
+ white-space: nowrap;
+}
+
+.reply-dot {
+ color: var(--text-tertiary);
+ font-size: 0.7rem;
+}
+
+.reply-text {
+ font-size: 0.9rem;
+ line-height: 1.5;
+ color: var(--text-primary);
+ margin: 0;
+ padding-left: 38px;
+}
+
+.reply-actions {
+ display: flex;
+ align-items: center;
+ gap: 4px;
+ opacity: 0;
+ transition: opacity 0.15s;
+}
+
+.reply-card-threaded:hover .reply-actions {
+ opacity: 1;
+}
+
+.reply-action-btn {
+ background: none;
+ border: none;
+ padding: 4px;
+ color: var(--text-tertiary);
+ cursor: pointer;
+ border-radius: 4px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+}
+
+.reply-action-btn:hover {
+ background: var(--bg-tertiary);
+ color: var(--text-secondary);
+}
+
+.reply-action-delete:hover {
+ color: #ef4444;
+ background: rgba(239, 68, 68, 0.1);
+}
+
+.reply-form {
+ border: 1px solid var(--border);
+ border-radius: var(--radius-md);
+ padding: 16px;
+ background: var(--bg-secondary);
+ margin-bottom: 24px;
+}
+
+.replying-to-banner {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ background: var(--bg-tertiary);
+ padding: 8px 12px;
+ border-radius: var(--radius-sm);
+ margin-bottom: 12px;
+ font-size: 0.85rem;
+ color: var(--text-secondary);
+}
+
+.cancel-reply {
+ background: none;
+ border: none;
+ color: var(--text-tertiary);
+ cursor: pointer;
+ font-size: 1.2rem;
+ padding: 0 4px;
+ line-height: 1;
+}
+
+.cancel-reply:hover {
+ color: var(--text-primary);
+}
+
+.reply-input {
+ width: 100%;
+ background: var(--bg-primary);
+ border: 1px solid var(--border);
+ border-radius: var(--radius-sm);
+ padding: 12px;
+ color: var(--text-primary);
+ font-family: inherit;
+ font-size: 0.95rem;
+ resize: vertical;
+ min-height: 80px;
+ transition: border-color 0.15s;
+ display: block;
+ box-sizing: border-box;
+}
+
+.reply-input:focus {
+ outline: none;
+ border-color: var(--accent);
+}
+
+.reply-form-actions {
+ display: flex;
+ justify-content: flex-end;
+ margin-top: 12px;
+}
diff --git a/web/src/css/base.css b/web/src/css/base.css
new file mode 100644
index 0000000..27579f5
--- /dev/null
+++ b/web/src/css/base.css
@@ -0,0 +1,139 @@
+:root {
+ --bg-primary: #09090b;
+ --bg-secondary: #0f0f12;
+ --bg-tertiary: #18181b;
+ --bg-card: #09090b;
+ --bg-elevated: #18181b;
+ --text-primary: #e4e4e7;
+ --text-secondary: #a1a1aa;
+ --text-tertiary: #71717a;
+ --border: #27272a;
+ --border-hover: #3f3f46;
+ --accent: #6366f1;
+ --accent-hover: #4f46e5;
+ --accent-subtle: rgba(99, 102, 241, 0.1);
+ --accent-text: #818cf8;
+ --success: #10b981;
+ --error: #ef4444;
+ --warning: #f59e0b;
+ --info: #3b82f6;
+ --radius-sm: 4px;
+ --radius-md: 6px;
+ --radius-lg: 8px;
+ --radius-full: 9999px;
+ --shadow-sm: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
+ --shadow-md:
+ 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
+ --shadow-lg:
+ 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05);
+ --font-sans:
+ "Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
+ --font-mono:
+ "JetBrains Mono", source-code-pro, Menlo, Monaco, Consolas, monospace;
+}
+
+* {
+ margin: 0;
+ padding: 0;
+ box-sizing: border-box;
+}
+
+html {
+ font-size: 16px;
+ -webkit-text-size-adjust: 100%;
+}
+
+body {
+ font-family: var(--font-sans);
+ background: var(--bg-primary);
+ color: var(--text-primary);
+ line-height: 1.5;
+ min-height: 100vh;
+ -webkit-font-smoothing: antialiased;
+ -moz-osx-font-smoothing: grayscale;
+}
+
+a {
+ color: inherit;
+ text-decoration: none;
+ transition: color 0.15s ease;
+}
+
+h1,
+h2,
+h3,
+h4,
+h5,
+h6 {
+ font-weight: 600;
+ line-height: 1.25;
+ letter-spacing: -0.025em;
+ color: var(--text-primary);
+}
+
+p {
+ color: var(--text-secondary);
+}
+
+button {
+ font-family: inherit;
+ cursor: pointer;
+ border: none;
+ background: none;
+}
+
+input,
+textarea,
+select {
+ font-family: inherit;
+ font-size: inherit;
+ color: var(--text-primary);
+}
+
+::selection {
+ background: var(--accent-subtle);
+ color: var(--accent-text);
+}
+
+.text-sm {
+ font-size: 0.875rem;
+}
+
+.text-xs {
+ font-size: 0.75rem;
+}
+
+.font-medium {
+ font-weight: 500;
+}
+
+.font-semibold {
+ font-weight: 600;
+}
+
+.text-muted {
+ color: var(--text-secondary);
+}
+
+.text-faint {
+ color: var(--text-tertiary);
+}
+
+::-webkit-scrollbar {
+ width: 10px;
+ height: 10px;
+}
+
+::-webkit-scrollbar-track {
+ background: transparent;
+}
+
+::-webkit-scrollbar-thumb {
+ background: var(--border);
+ border-radius: 5px;
+ border: 2px solid var(--bg-primary);
+}
+
+::-webkit-scrollbar-thumb:hover {
+ background: var(--border-hover);
+}
diff --git a/web/src/css/buttons.css b/web/src/css/buttons.css
new file mode 100644
index 0000000..cb435ce
--- /dev/null
+++ b/web/src/css/buttons.css
@@ -0,0 +1,127 @@
+.btn {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ gap: 8px;
+ padding: 10px 20px;
+ font-size: 0.9rem;
+ font-weight: 500;
+ border-radius: var(--radius-md);
+ transition: all 0.15s ease;
+}
+
+.btn-primary {
+ background: var(--accent);
+ color: white;
+}
+
+.btn-primary:hover {
+ background: var(--accent-hover);
+ transform: translateY(-1px);
+ box-shadow: var(--shadow-md);
+}
+
+.btn-secondary {
+ background: var(--bg-tertiary);
+ color: var(--text-primary);
+ border: 1px solid var(--border);
+}
+
+.btn-secondary:hover {
+ background: var(--bg-hover);
+ border-color: var(--border-hover);
+}
+
+.btn-ghost {
+ color: var(--text-secondary);
+ padding: 8px 12px;
+}
+
+.btn-ghost:hover {
+ color: var(--text-primary);
+ background: var(--bg-tertiary);
+}
+
+.btn-bluesky {
+ background: #0085ff;
+ color: white;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ gap: 10px;
+ transition:
+ background 0.2s,
+ transform 0.2s;
+}
+
+.btn-bluesky:hover {
+ background: #0070dd;
+ transform: translateY(-1px);
+}
+
+.btn-sm {
+ padding: 6px 12px;
+ font-size: 0.85rem;
+}
+
+.btn-text {
+ background: none;
+ border: none;
+ color: var(--text-secondary);
+ font-size: 0.9rem;
+ padding: 8px 12px;
+ cursor: pointer;
+ transition: color 0.15s;
+}
+
+.btn-text:hover {
+ color: var(--text-primary);
+}
+
+.btn-block {
+ width: 100%;
+ text-align: left;
+ padding: 8px 12px;
+ color: var(--text-secondary);
+ background: var(--bg-tertiary);
+ border-radius: var(--radius-md);
+ margin-top: 8px;
+ font-size: 0.9rem;
+ cursor: pointer;
+ transition: all 0.2s;
+}
+
+.btn-block:hover {
+ background: var(--border);
+ color: var(--text-primary);
+}
+
+.btn-icon-danger {
+ padding: 8px;
+ background: var(--error);
+ color: white;
+ border: none;
+ border-radius: var(--radius-md);
+ cursor: pointer;
+ box-shadow: var(--shadow-md);
+ transition: all 0.15s ease;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+}
+
+.btn-icon-danger:hover {
+ background: #dc2626;
+ transform: scale(1.05);
+}
+
+.action-buttons {
+ display: flex;
+ gap: 8px;
+}
+
+.action-buttons-end {
+ display: flex;
+ justify-content: flex-end;
+ gap: 8px;
+}
diff --git a/web/src/css/collections.css b/web/src/css/collections.css
new file mode 100644
index 0000000..f3332bf
--- /dev/null
+++ b/web/src/css/collections.css
@@ -0,0 +1,310 @@
+.collections-list {
+ display: flex;
+ flex-direction: column;
+ gap: 2px;
+ background: var(--bg-card);
+ border: 1px solid var(--border);
+ border-radius: var(--radius-lg);
+ overflow: hidden;
+}
+
+.collection-row {
+ display: flex;
+ align-items: center;
+ background: var(--bg-card);
+ transition: background 0.15s ease;
+}
+
+.collection-row:not(:last-child) {
+ border-bottom: 1px solid var(--border);
+}
+
+.collection-row:hover {
+ background: var(--bg-secondary);
+}
+
+.collection-row-content {
+ flex: 1;
+ display: flex;
+ align-items: center;
+ gap: 16px;
+ padding: 16px 20px;
+ text-decoration: none;
+ min-width: 0;
+}
+
+.collection-row-icon {
+ width: 44px;
+ height: 44px;
+ min-width: 44px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ background: linear-gradient(
+ 135deg,
+ rgba(79, 70, 229, 0.1),
+ rgba(168, 85, 247, 0.15)
+ );
+ color: var(--accent);
+ border-radius: var(--radius-md);
+ transition: all 0.2s ease;
+}
+
+.collection-row:hover .collection-row-icon {
+ background: linear-gradient(
+ 135deg,
+ rgba(79, 70, 229, 0.15),
+ rgba(168, 85, 247, 0.2)
+ );
+ transform: scale(1.05);
+}
+
+.collection-row-info {
+ flex: 1;
+ min-width: 0;
+}
+
+.collection-row-name {
+ font-size: 1rem;
+ font-weight: 600;
+ color: var(--text-primary);
+ margin: 0 0 2px 0;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+}
+
+.collection-row:hover .collection-row-name {
+ color: var(--accent);
+}
+
+.collection-row-desc {
+ font-size: 0.85rem;
+ color: var(--text-secondary);
+ margin: 0;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+}
+
+.collection-row-arrow {
+ color: var(--text-tertiary);
+ opacity: 0;
+ transition: all 0.2s ease;
+}
+
+.collection-row:hover .collection-row-arrow {
+ opacity: 1;
+ color: var(--accent);
+ transform: translateX(2px);
+}
+
+.collection-row-edit {
+ padding: 10px;
+ margin-right: 12px;
+ color: var(--text-tertiary);
+ background: none;
+ border: none;
+ border-radius: var(--radius-sm);
+ cursor: pointer;
+ opacity: 0;
+ transition: all 0.15s ease;
+}
+
+.collection-row:hover .collection-row-edit {
+ opacity: 1;
+}
+
+.collection-row-edit:hover {
+ color: var(--text-primary);
+ background: var(--bg-tertiary);
+}
+
+.collection-detail-header {
+ display: flex;
+ gap: 20px;
+ padding: 24px;
+ background: var(--bg-card);
+ border: 1px solid var(--border);
+ border-radius: var(--radius-lg);
+ margin-bottom: 32px;
+ position: relative;
+}
+
+.collection-detail-icon {
+ width: 56px;
+ height: 56px;
+ min-width: 56px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ background: linear-gradient(
+ 135deg,
+ rgba(79, 70, 229, 0.1),
+ rgba(168, 85, 247, 0.1)
+ );
+ color: var(--accent);
+ border-radius: var(--radius-md);
+}
+
+.collection-detail-info {
+ flex: 1;
+ min-width: 0;
+}
+
+.collection-detail-visibility {
+ display: flex;
+ align-items: center;
+ gap: 6px;
+ font-size: 0.8rem;
+ font-weight: 600;
+ color: var(--accent);
+ text-transform: capitalize;
+ margin-bottom: 8px;
+}
+
+.collection-detail-title {
+ font-size: 1.5rem;
+ font-weight: 700;
+ color: var(--text-primary);
+ margin-bottom: 8px;
+ line-height: 1.3;
+}
+
+.collection-detail-desc {
+ color: var(--text-secondary);
+ font-size: 1rem;
+ line-height: 1.5;
+ margin-bottom: 12px;
+ max-width: 600px;
+}
+
+.collection-detail-stats {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ font-size: 0.85rem;
+ color: var(--text-tertiary);
+}
+
+.collection-detail-actions {
+ position: absolute;
+ top: 20px;
+ right: 20px;
+ display: flex;
+ align-items: center;
+ gap: 8px;
+}
+
+.collection-detail-actions .share-menu-container {
+ display: flex;
+ align-items: center;
+}
+
+.collection-detail-actions .annotation-action {
+ padding: 10px;
+ color: var(--text-tertiary);
+ background: none;
+ border: none;
+ border-radius: var(--radius-sm);
+ cursor: pointer;
+ transition: all 0.15s ease;
+}
+
+.collection-detail-actions .annotation-action:hover {
+ color: var(--accent);
+ background: var(--bg-tertiary);
+}
+
+.collection-detail-edit,
+.collection-detail-delete {
+ padding: 10px;
+ color: var(--text-tertiary);
+ background: none;
+ border: none;
+ border-radius: var(--radius-sm);
+ cursor: pointer;
+ transition: all 0.15s ease;
+}
+
+.collection-detail-edit:hover {
+ color: var(--accent);
+ background: var(--bg-tertiary);
+}
+
+.collection-detail-delete:hover {
+ color: var(--error);
+ background: rgba(239, 68, 68, 0.1);
+}
+
+.collection-item-wrapper {
+ position: relative;
+}
+
+.collection-item-remove {
+ position: absolute;
+ top: 12px;
+ left: -40px;
+ z-index: 10;
+ padding: 8px;
+ background: var(--bg-card);
+ border: 1px solid var(--border);
+ border-radius: var(--radius-sm);
+ color: var(--text-tertiary);
+ cursor: pointer;
+ opacity: 0;
+ transition: all 0.15s ease;
+}
+
+.collection-item-wrapper:hover .collection-item-remove {
+ opacity: 1;
+}
+
+.collection-item-remove:hover {
+ color: var(--error);
+ border-color: var(--error);
+ background: rgba(239, 68, 68, 0.05);
+}
+
+.collection-list-item {
+ width: 100%;
+ text-align: left;
+ padding: 12px 16px;
+ border-radius: var(--radius-md);
+ background: var(--bg-primary);
+ border: 1px solid transparent;
+ color: var(--text-primary);
+ transition: all 0.15s ease;
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ cursor: pointer;
+}
+
+.collection-list-item:hover {
+ background: var(--bg-hover);
+ border-color: var(--border);
+}
+
+.collection-list-item:hover .collection-list-item-icon {
+ opacity: 1;
+}
+
+.collection-list-item:disabled {
+ opacity: 0.6;
+ cursor: not-allowed;
+}
+
+.item-delete-overlay {
+ position: absolute;
+ top: 16px;
+ right: 16px;
+ z-index: 10;
+ opacity: 0;
+ transition: opacity 0.15s ease;
+}
+
+.card:hover .item-delete-overlay,
+div:hover > .item-delete-overlay {
+ opacity: 1;
+}
diff --git a/web/src/css/feed.css b/web/src/css/feed.css
new file mode 100644
index 0000000..58c01bc
--- /dev/null
+++ b/web/src/css/feed.css
@@ -0,0 +1,139 @@
+.feed {
+ display: flex;
+ flex-direction: column;
+ gap: 16px;
+}
+
+.feed-header {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ margin-bottom: 8px;
+}
+
+.feed-title {
+ font-size: 1.5rem;
+ font-weight: 700;
+}
+
+.feed-filters {
+ display: flex;
+ gap: 8px;
+ margin-bottom: 24px;
+ padding: 4px;
+ background: var(--bg-tertiary);
+ border-radius: var(--radius-lg);
+ width: fit-content;
+}
+
+.filter-tab {
+ padding: 8px 16px;
+ font-size: 0.9rem;
+ font-weight: 500;
+ color: var(--text-secondary);
+ background: transparent;
+ border: none;
+ border-radius: var(--radius-md);
+ cursor: pointer;
+ transition: all 0.15s ease;
+}
+
+.filter-tab:hover {
+ color: var(--text-primary);
+ background: var(--bg-hover);
+}
+
+.filter-tab.active {
+ color: var(--text-primary);
+ background: var(--bg-card);
+ box-shadow: var(--shadow-sm);
+}
+
+.page-header {
+ margin-bottom: 32px;
+}
+
+.page-title {
+ font-size: 2rem;
+ font-weight: 700;
+ margin-bottom: 8px;
+}
+
+.page-description {
+ color: var(--text-secondary);
+ font-size: 1.1rem;
+}
+
+.url-input-wrapper {
+ margin-bottom: 24px;
+}
+
+.url-input-container {
+ display: flex;
+ gap: 12px;
+}
+
+.url-input {
+ width: 100%;
+ padding: 16px;
+ background: var(--bg-secondary);
+ border: 1px solid var(--border);
+ border-radius: var(--radius-md);
+ color: var(--text-primary);
+ font-size: 1.1rem;
+ transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
+ box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
+}
+
+.url-input:focus {
+ outline: none;
+ border-color: var(--accent);
+ box-shadow: 0 0 0 4px var(--accent-subtle);
+ background: var(--bg-primary);
+}
+
+.url-input::placeholder {
+ color: var(--text-tertiary);
+}
+
+.url-results-header {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ margin-bottom: 16px;
+ flex-wrap: wrap;
+ gap: 12px;
+}
+
+.back-link {
+ display: inline-flex;
+ align-items: center;
+ gap: 8px;
+ color: var(--text-secondary);
+ font-size: 0.9rem;
+ text-decoration: none;
+ margin-bottom: 24px;
+ transition: color 0.15s;
+}
+
+.back-link:hover {
+ color: var(--accent);
+}
+
+.new-page {
+ max-width: 600px;
+ margin: 0 auto;
+ display: flex;
+ flex-direction: column;
+ gap: 32px;
+}
+
+@media (max-width: 640px) {
+ .main-content {
+ padding: 16px 12px;
+ }
+
+ .page-title {
+ font-size: 1.5rem;
+ }
+}
diff --git a/web/src/css/layout.css b/web/src/css/layout.css
new file mode 100644
index 0000000..fa23e00
--- /dev/null
+++ b/web/src/css/layout.css
@@ -0,0 +1,463 @@
+.layout {
+ display: flex;
+ min-height: 100vh;
+ background: var(--bg-primary);
+}
+
+.sidebar {
+ position: fixed;
+ left: 0;
+ top: 0;
+ bottom: 0;
+ width: 240px;
+ background: var(--bg-primary);
+ border-right: 1px solid var(--border);
+ display: flex;
+ flex-direction: column;
+ z-index: 50;
+ padding-bottom: 20px;
+}
+
+.sidebar-header {
+ height: 64px;
+ display: flex;
+ align-items: center;
+ padding: 0 20px;
+ margin-bottom: 12px;
+ text-decoration: none;
+ color: var(--text-primary);
+}
+
+.sidebar-logo {
+ width: 24px;
+ height: 24px;
+ object-fit: contain;
+ margin-right: 12px;
+}
+
+.sidebar-brand {
+ font-size: 1rem;
+ font-weight: 600;
+ color: var(--text-primary);
+ letter-spacing: -0.01em;
+}
+
+.sidebar-nav {
+ flex: 1;
+ display: flex;
+ flex-direction: column;
+ gap: 4px;
+ padding: 0 12px;
+ overflow-y: auto;
+}
+
+.sidebar-link {
+ display: flex;
+ align-items: center;
+ gap: 12px;
+ padding: 8px 12px;
+ border-radius: var(--radius-md);
+ color: var(--text-secondary);
+ text-decoration: none;
+ font-size: 0.9rem;
+ font-weight: 500;
+ transition: all 0.15s ease;
+}
+
+.sidebar-link:hover {
+ background: var(--bg-tertiary);
+ color: var(--text-primary);
+}
+
+.sidebar-link.active {
+ background: var(--bg-tertiary);
+ color: var(--text-primary);
+}
+
+.sidebar-link svg {
+ width: 18px;
+ height: 18px;
+ color: var(--text-tertiary);
+ transition: color 0.15s ease;
+}
+
+.sidebar-link:hover svg,
+.sidebar-link.active svg {
+ color: var(--text-primary);
+}
+
+.sidebar-section-title {
+ padding: 24px 12px 8px;
+ font-size: 0.75rem;
+ font-weight: 600;
+ color: var(--text-tertiary);
+ text-transform: uppercase;
+ letter-spacing: 0.05em;
+}
+
+.notification-badge {
+ background: var(--accent);
+ color: white;
+ font-size: 0.7rem;
+ font-weight: 600;
+ padding: 0 6px;
+ height: 18px;
+ border-radius: 99px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ margin-left: auto;
+}
+
+.sidebar-new-btn {
+ display: flex;
+ align-items: center;
+ gap: 10px;
+ margin: 0 12px 16px;
+ padding: 10px 16px;
+ background: var(--text-primary);
+ color: var(--bg-primary);
+ border-radius: var(--radius-md);
+ font-size: 0.9rem;
+ font-weight: 600;
+ text-decoration: none;
+ transition: opacity 0.15s;
+ justify-content: center;
+}
+
+.sidebar-new-btn:hover {
+ opacity: 0.9;
+}
+
+.sidebar-footer {
+ padding: 0 12px;
+ margin-top: auto;
+}
+
+.sidebar-user {
+ display: flex;
+ align-items: center;
+ gap: 10px;
+ padding: 8px 12px;
+ border-radius: var(--radius-md);
+ cursor: pointer;
+ transition: background 0.15s ease;
+}
+
+.sidebar-user:hover,
+.sidebar-user.active {
+ background: var(--bg-tertiary);
+}
+
+.sidebar-avatar {
+ width: 32px;
+ height: 32px;
+ border-radius: 50%;
+ background: var(--bg-tertiary);
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ color: var(--text-secondary);
+ font-size: 0.8rem;
+ font-weight: 500;
+ overflow: hidden;
+ flex-shrink: 0;
+ border: 1px solid var(--border);
+}
+
+.sidebar-avatar img {
+ width: 100%;
+ height: 100%;
+ object-fit: cover;
+}
+
+.sidebar-user-info {
+ flex: 1;
+ min-width: 0;
+ display: flex;
+ flex-direction: column;
+}
+
+.sidebar-user-name {
+ font-size: 0.85rem;
+ font-weight: 500;
+ color: var(--text-primary);
+}
+
+.sidebar-user-handle {
+ font-size: 0.75rem;
+ color: var(--text-tertiary);
+}
+
+.sidebar-dropdown {
+ position: absolute;
+ bottom: 74px;
+ left: 12px;
+ width: 216px;
+ background: var(--bg-card);
+ border: 1px solid var(--border);
+ border-radius: var(--radius-md);
+ box-shadow: var(--shadow-lg);
+ padding: 4px;
+ z-index: 1000;
+ overflow: hidden;
+ animation: scaleIn 0.1s ease-out;
+ transform-origin: bottom center;
+}
+
+@keyframes scaleIn {
+ from {
+ opacity: 0;
+ transform: scale(0.95);
+ }
+
+ to {
+ opacity: 1;
+ transform: scale(1);
+ }
+}
+
+.sidebar-dropdown-item {
+ display: flex;
+ align-items: center;
+ gap: 10px;
+ width: 100%;
+ padding: 8px 12px;
+ font-size: 0.85rem;
+ color: var(--text-secondary);
+ text-decoration: none;
+ background: transparent;
+ cursor: pointer;
+ border-radius: var(--radius-sm);
+ transition: all 0.15s;
+ border: none;
+}
+
+.sidebar-dropdown-item:hover {
+ background: var(--bg-tertiary);
+ color: var(--text-primary);
+}
+
+.sidebar-dropdown-item.danger:hover {
+ background: rgba(239, 68, 68, 0.1);
+ color: var(--error);
+}
+
+.main-layout {
+ flex: 1;
+ margin-left: 240px;
+ margin-right: 280px;
+ min-height: 100vh;
+}
+
+.main-content-wrapper {
+ max-width: 640px;
+ margin: 0 auto;
+ padding: 40px 24px;
+}
+
+.right-sidebar {
+ position: fixed;
+ right: 0;
+ top: 0;
+ bottom: 0;
+ width: 280px;
+ background: var(--bg-primary);
+ border-left: 1px solid var(--border);
+ padding: 32px 24px;
+ overflow-y: auto;
+ display: flex;
+ flex-direction: column;
+ gap: 32px;
+}
+
+.right-section {
+ display: flex;
+ flex-direction: column;
+ gap: 12px;
+}
+
+.right-section-title {
+ font-size: 0.75rem;
+ font-weight: 600;
+ color: var(--text-primary);
+ margin-bottom: 4px;
+}
+
+.right-section-desc {
+ font-size: 0.85rem;
+ line-height: 1.5;
+ color: var(--text-secondary);
+}
+
+.right-extension-btn {
+ display: inline-flex;
+ align-items: center;
+ gap: 8px;
+ padding: 8px 12px;
+ background: var(--bg-primary);
+ border: 1px solid var(--border);
+ border-radius: var(--radius-md);
+ color: var(--text-primary);
+ font-size: 0.85rem;
+ font-weight: 500;
+ text-decoration: none;
+ transition: all 0.15s ease;
+ width: fit-content;
+}
+
+.right-extension-btn:hover {
+ border-color: var(--text-tertiary);
+ background: var(--bg-tertiary);
+}
+
+.right-links {
+ display: flex;
+ flex-direction: column;
+ gap: 4px;
+}
+
+.right-link {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ padding: 6px 0;
+ color: var(--text-secondary);
+ font-size: 0.9rem;
+ transition: color 0.15s;
+ text-decoration: none;
+}
+
+.right-link:hover {
+ color: var(--text-primary);
+}
+
+.right-link svg {
+ width: 16px;
+ height: 16px;
+ color: var(--text-tertiary);
+ transition: all 0.15s;
+}
+
+.right-link:hover svg {
+ color: var(--text-secondary);
+}
+
+.tangled-icon {
+ width: 16px;
+ height: 16px;
+ background-color: var(--text-tertiary);
+ -webkit-mask: url("../assets/tangled.svg") no-repeat center / contain;
+ mask: url("../assets/tangled.svg") no-repeat center / contain;
+ transition: background-color 0.15s;
+}
+
+.right-link:hover .tangled-icon {
+ background-color: var(--text-secondary);
+}
+
+.right-footer {
+ margin-top: auto;
+ display: flex;
+ flex-wrap: wrap;
+ gap: 12px;
+ font-size: 0.75rem;
+ color: var(--text-tertiary);
+}
+
+.right-footer a {
+ color: var(--text-tertiary);
+}
+
+.right-footer a:hover {
+ color: var(--text-secondary);
+}
+
+.mobile-nav {
+ display: none;
+ position: fixed;
+ bottom: 0;
+ left: 0;
+ right: 0;
+ background: rgba(9, 9, 11, 0.9);
+ backdrop-filter: blur(12px);
+ -webkit-backdrop-filter: blur(12px);
+ border-top: 1px solid var(--border);
+ padding: 8px 16px;
+ padding-bottom: calc(8px + env(safe-area-inset-bottom, 0));
+ z-index: 100;
+}
+
+.mobile-nav-inner {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+}
+
+.mobile-nav-item {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ gap: 4px;
+ color: var(--text-tertiary);
+ text-decoration: none;
+ font-size: 0.65rem;
+ font-weight: 500;
+ width: 60px;
+ transition: color 0.15s;
+}
+
+.mobile-nav-item.active {
+ color: var(--text-primary);
+}
+
+.mobile-nav-item svg {
+ width: 24px;
+ height: 24px;
+}
+
+.mobile-nav-new {
+ width: 48px;
+ height: 36px;
+ border-radius: var(--radius-md);
+ background: var(--text-primary);
+ color: var(--bg-primary);
+ display: flex;
+ align-items: center;
+ justify-content: center;
+}
+
+.mobile-nav-new svg {
+ width: 20px;
+ height: 20px;
+}
+
+@media (max-width: 1200px) {
+ .right-sidebar {
+ display: none;
+ }
+
+ .main-layout {
+ margin-right: 0;
+ }
+}
+
+@media (max-width: 768px) {
+ .sidebar {
+ display: none;
+ }
+
+ .main-layout {
+ margin-left: 0;
+ padding-bottom: 80px;
+ }
+
+ .main-content-wrapper {
+ padding: 20px 16px;
+ }
+
+ .mobile-nav {
+ display: block;
+ }
+}
diff --git a/web/src/css/login.css b/web/src/css/login.css
new file mode 100644
index 0000000..39faaa9
--- /dev/null
+++ b/web/src/css/login.css
@@ -0,0 +1,297 @@
+.login-page {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ min-height: 70vh;
+ padding: 60px 20px;
+ width: 100%;
+ max-width: 500px;
+ margin: 0 auto;
+}
+
+.login-at-logo {
+ font-size: 5rem;
+ font-weight: 800;
+ color: var(--accent);
+ margin-bottom: 24px;
+ line-height: 1;
+}
+
+.login-logo-img {
+ width: 80px;
+ height: 80px;
+ margin-bottom: 24px;
+ object-fit: contain;
+}
+
+.login-heading {
+ font-size: 1.5rem;
+ font-weight: 600;
+ margin-bottom: 32px;
+ display: flex;
+ align-items: center;
+ gap: 10px;
+ text-align: center;
+ line-height: 1.4;
+}
+
+.login-help-btn {
+ background: none;
+ border: none;
+ color: var(--text-tertiary);
+ cursor: pointer;
+ padding: 4px;
+ display: flex;
+ align-items: center;
+ transition: color 0.15s;
+ flex-shrink: 0;
+}
+
+.login-help-btn:hover {
+ color: var(--accent);
+}
+
+.login-help-text {
+ background: var(--bg-elevated);
+ border: 1px solid var(--border);
+ border-radius: var(--radius-md);
+ padding: 16px 20px;
+ margin-bottom: 24px;
+ font-size: 0.95rem;
+ color: var(--text-secondary);
+ line-height: 1.6;
+ text-align: center;
+}
+
+.login-help-text code {
+ background: var(--bg-tertiary);
+ padding: 2px 8px;
+ border-radius: var(--radius-sm);
+ font-size: 0.9rem;
+}
+
+.login-form {
+ display: flex;
+ flex-direction: column;
+ gap: 16px;
+ width: 100%;
+}
+
+.login-input-wrapper {
+ position: relative;
+}
+
+.login-input {
+ width: 100%;
+ padding: 14px 16px;
+ background: var(--bg-elevated);
+ border: 1px solid var(--border);
+ border-radius: var(--radius-md);
+ color: var(--text-primary);
+ font-size: 1rem;
+ transition:
+ border-color 0.15s,
+ box-shadow 0.15s;
+}
+
+.login-input:focus {
+ outline: none;
+ border-color: var(--accent);
+ box-shadow: 0 0 0 3px rgba(99, 102, 241, 0.15);
+}
+
+.login-input::placeholder {
+ color: var(--text-tertiary);
+}
+
+.login-suggestions {
+ position: absolute;
+ top: calc(100% + 4px);
+ left: 0;
+ right: 0;
+ background: var(--bg-card);
+ border: 1px solid var(--border);
+ border-radius: var(--radius-md);
+ box-shadow: var(--shadow-lg);
+ overflow: hidden;
+ z-index: 100;
+}
+
+.login-suggestion {
+ display: flex;
+ align-items: center;
+ gap: 12px;
+ width: 100%;
+ padding: 12px 16px;
+ background: transparent;
+ border: none;
+ cursor: pointer;
+ text-align: left;
+ transition: background 0.1s;
+}
+
+.login-suggestion:hover,
+.login-suggestion.selected {
+ background: var(--bg-elevated);
+}
+
+.login-suggestion-avatar {
+ width: 40px;
+ height: 40px;
+ border-radius: var(--radius-full);
+ background: linear-gradient(135deg, var(--accent), #a855f7);
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ flex-shrink: 0;
+ overflow: hidden;
+ font-size: 0.875rem;
+ font-weight: 600;
+ color: white;
+}
+
+.login-suggestion-avatar img {
+ width: 100%;
+ height: 100%;
+ object-fit: cover;
+}
+
+.login-suggestion-info {
+ display: flex;
+ flex-direction: column;
+ min-width: 0;
+}
+
+.login-suggestion-name {
+ font-weight: 600;
+ color: var(--text-primary);
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+}
+
+.login-suggestion-handle {
+ font-size: 0.875rem;
+ color: var(--text-secondary);
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+}
+
+.login-error {
+ padding: 12px 16px;
+ background: rgba(239, 68, 68, 0.1);
+ border: 1px solid rgba(239, 68, 68, 0.3);
+ border-radius: var(--radius-md);
+ color: #ef4444;
+ font-size: 0.875rem;
+}
+
+.login-legal {
+ font-size: 0.75rem;
+ color: var(--text-tertiary);
+ line-height: 1.5;
+ margin-top: 16px;
+}
+
+.login-brand {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ gap: 12px;
+ margin-bottom: 24px;
+}
+
+.login-brand-icon {
+ width: 48px;
+ height: 48px;
+ background: linear-gradient(135deg, var(--accent), #a855f7);
+ border-radius: var(--radius-lg);
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ font-size: 1.75rem;
+ font-weight: 800;
+ color: white;
+}
+
+.login-brand-name {
+ font-size: 1.75rem;
+ font-weight: 700;
+}
+
+.login-avatar {
+ width: 72px;
+ height: 72px;
+ border-radius: var(--radius-full);
+ background: linear-gradient(135deg, var(--accent), #a855f7);
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ margin: 0 auto 16px;
+ font-weight: 700;
+ font-size: 1.5rem;
+ color: white;
+ overflow: hidden;
+}
+
+.login-avatar img {
+ width: 100%;
+ height: 100%;
+ object-fit: cover;
+}
+
+.login-avatar-large {
+ width: 100px;
+ height: 100px;
+ border-radius: var(--radius-full);
+ background: linear-gradient(135deg, var(--accent), #a855f7);
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ margin-bottom: 20px;
+ font-weight: 700;
+ font-size: 2rem;
+ color: white;
+ overflow: hidden;
+}
+
+.login-avatar-large img {
+ width: 100%;
+ height: 100%;
+ object-fit: cover;
+}
+
+.login-welcome {
+ font-size: 1.5rem;
+ font-weight: 600;
+ margin-bottom: 32px;
+ text-align: center;
+}
+
+.login-welcome-name {
+ font-size: 1.25rem;
+ font-weight: 600;
+ margin-bottom: 24px;
+}
+
+.login-actions {
+ display: flex;
+ flex-direction: column;
+ gap: 12px;
+ width: 100%;
+}
+
+.login-btn {
+ width: 100%;
+ padding: 14px 24px;
+ font-size: 1rem;
+ font-weight: 600;
+}
+
+.login-submit {
+ padding: 18px 32px;
+ font-size: 1.1rem;
+ font-weight: 600;
+}
diff --git a/web/src/css/modals.css b/web/src/css/modals.css
new file mode 100644
index 0000000..8f5f139
--- /dev/null
+++ b/web/src/css/modals.css
@@ -0,0 +1,262 @@
+.modal-overlay {
+ position: fixed;
+ inset: 0;
+ background: rgba(0, 0, 0, 0.5);
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ padding: 16px;
+ z-index: 50;
+ animation: fadeIn 0.2s ease-out;
+}
+
+.modal-container {
+ background: var(--bg-secondary);
+ border-radius: var(--radius-lg);
+ width: 100%;
+ max-width: 28rem;
+ border: 1px solid var(--border);
+ box-shadow: var(--shadow-lg);
+ animation: zoomIn 0.2s ease-out;
+}
+
+.modal-header {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ padding: 16px;
+ border-bottom: 1px solid var(--border);
+}
+
+.modal-title {
+ font-size: 1.25rem;
+ font-weight: 700;
+ color: var(--text-primary);
+}
+
+.modal-close-btn {
+ padding: 8px;
+ color: var(--text-tertiary);
+ border-radius: var(--radius-md);
+ transition: color 0.15s;
+}
+
+.modal-close-btn:hover {
+ color: var(--text-primary);
+ background: var(--bg-hover);
+}
+
+.modal-form {
+ padding: 16px;
+ display: flex;
+ flex-direction: column;
+ gap: 16px;
+}
+
+.icon-picker-tabs {
+ display: flex;
+ gap: 4px;
+ margin-bottom: 12px;
+}
+
+.icon-picker-tab {
+ flex: 1;
+ padding: 8px 12px;
+ background: var(--bg-primary);
+ border: 1px solid var(--border);
+ border-radius: var(--radius-md);
+ color: var(--text-secondary);
+ font-size: 0.85rem;
+ font-weight: 500;
+ cursor: pointer;
+ transition: all 0.15s ease;
+}
+
+.icon-picker-tab:hover {
+ background: var(--bg-tertiary);
+}
+
+.icon-picker-tab.active {
+ background: var(--accent);
+ border-color: var(--accent);
+ color: white;
+}
+
+.emoji-picker-wrapper {
+ display: flex;
+ flex-direction: column;
+ gap: 10px;
+}
+
+.emoji-custom-input input {
+ width: 100%;
+}
+
+.emoji-picker,
+.icon-picker {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 4px;
+ max-height: 120px;
+ overflow-y: auto;
+ padding: 8px;
+ background: var(--bg-primary);
+ border: 1px solid var(--border);
+ border-radius: var(--radius-md);
+}
+
+.emoji-option,
+.icon-option {
+ width: 36px;
+ height: 36px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ font-size: 1.2rem;
+ background: transparent;
+ border: 2px solid transparent;
+ border-radius: var(--radius-sm);
+ cursor: pointer;
+ transition: all 0.15s ease;
+ color: var(--text-secondary);
+}
+
+.emoji-option:hover,
+.icon-option:hover {
+ background: var(--bg-tertiary);
+ transform: scale(1.1);
+ color: var(--text-primary);
+}
+
+.emoji-option.selected,
+.icon-option.selected {
+ border-color: var(--accent);
+ background: var(--accent-subtle);
+ color: var(--accent);
+}
+
+.modal-actions {
+ display: flex;
+ justify-content: flex-end;
+ gap: 12px;
+ padding-top: 8px;
+}
+
+@keyframes fadeIn {
+ from {
+ opacity: 0;
+ }
+
+ to {
+ opacity: 1;
+ }
+}
+
+@keyframes zoomIn {
+ from {
+ opacity: 0;
+ transform: scale(0.95);
+ }
+
+ to {
+ opacity: 1;
+ transform: scale(1);
+ }
+}
+
+.form-group {
+ margin-bottom: 0;
+}
+
+.form-label {
+ display: block;
+ font-size: 0.85rem;
+ font-weight: 600;
+ color: var(--text-secondary);
+ margin-bottom: 6px;
+}
+
+.form-input,
+.form-textarea,
+.form-select {
+ width: 100%;
+ padding: 8px 12px;
+ background: var(--bg-primary);
+ border: 1px solid var(--border);
+ border-radius: var(--radius-md);
+ color: var(--text-primary);
+ transition: all 0.15s;
+}
+
+.form-input:focus,
+.form-textarea:focus,
+.form-select:focus {
+ outline: none;
+ border-color: var(--accent);
+ box-shadow: 0 0 0 2px var(--accent-subtle);
+}
+
+.form-textarea {
+ resize: none;
+}
+
+.input {
+ width: 100%;
+ padding: 12px 14px;
+ font-size: 0.95rem;
+ color: var(--text-primary);
+ background: var(--bg-secondary);
+ border: 1px solid var(--border);
+ border-radius: var(--radius-md);
+ outline: none;
+ transition: all 0.15s ease;
+}
+
+.input:focus {
+ border-color: var(--accent);
+ box-shadow: 0 0 0 3px var(--accent-subtle);
+}
+
+.input::placeholder {
+ color: var(--text-tertiary);
+}
+
+.color-input-container {
+ display: flex;
+ align-items: center;
+ gap: 12px;
+ background: var(--bg-tertiary);
+ padding: 8px 12px;
+ border-radius: var(--radius-md);
+ border: 1px solid var(--border);
+ width: fit-content;
+}
+
+.color-input-wrapper {
+ position: relative;
+ width: 32px;
+ height: 32px;
+ border-radius: var(--radius-full);
+ overflow: hidden;
+ border: 2px solid var(--border);
+ cursor: pointer;
+ transition: transform 0.1s;
+}
+
+.color-input-wrapper:hover {
+ transform: scale(1.1);
+ border-color: var(--accent);
+}
+
+.color-input-wrapper input[type="color"] {
+ position: absolute;
+ top: -50%;
+ left: -50%;
+ width: 200%;
+ height: 200%;
+ padding: 0;
+ margin: 0;
+ border: none;
+ cursor: pointer;
+ opacity: 0;
+}
diff --git a/web/src/css/notifications.css b/web/src/css/notifications.css
new file mode 100644
index 0000000..c08f3bc
--- /dev/null
+++ b/web/src/css/notifications.css
@@ -0,0 +1,65 @@
+.notifications-page {
+ max-width: 680px;
+ margin: 0 auto;
+}
+
+.notifications-list {
+ display: flex;
+ flex-direction: column;
+ gap: 12px;
+}
+
+.notification-item {
+ display: flex;
+ gap: 16px;
+ align-items: flex-start;
+ text-decoration: none;
+ color: inherit;
+}
+
+.notification-item:hover {
+ background: var(--bg-hover);
+}
+
+.notification-icon {
+ width: 36px;
+ height: 36px;
+ border-radius: var(--radius-full);
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ background: var(--bg-tertiary);
+ color: var(--text-secondary);
+ flex-shrink: 0;
+}
+
+.notification-icon[data-type="like"] {
+ color: #ef4444;
+ background: rgba(239, 68, 68, 0.1);
+}
+
+.notification-icon[data-type="reply"] {
+ color: #3b82f6;
+ background: rgba(59, 130, 246, 0.1);
+}
+
+.notification-content {
+ flex: 1;
+ min-width: 0;
+}
+
+.notification-text {
+ font-size: 0.95rem;
+ margin-bottom: 4px;
+ line-height: 1.4;
+ color: var(--text-primary);
+}
+
+.notification-text strong {
+ font-weight: 600;
+}
+
+.notification-time {
+ font-size: 0.85rem;
+ color: var(--text-tertiary);
+}
diff --git a/web/src/css/profile.css b/web/src/css/profile.css
new file mode 100644
index 0000000..8486411
--- /dev/null
+++ b/web/src/css/profile.css
@@ -0,0 +1,250 @@
+.profile-header {
+ display: flex;
+ align-items: center;
+ gap: 24px;
+ margin-bottom: 32px;
+ padding-bottom: 24px;
+ border-bottom: 1px solid var(--border);
+}
+
+.profile-avatar {
+ width: 80px;
+ height: 80px;
+ min-width: 80px;
+ border-radius: 50%;
+ background: var(--bg-tertiary);
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ font-weight: 600;
+ font-size: 2rem;
+ color: var(--text-secondary);
+ overflow: hidden;
+ border: 1px solid var(--border);
+}
+
+.profile-avatar img {
+ width: 100%;
+ height: 100%;
+ object-fit: cover;
+}
+
+.profile-avatar-link {
+ text-decoration: none;
+}
+
+.profile-info {
+ flex: 1;
+ display: flex;
+ flex-direction: column;
+ gap: 4px;
+}
+
+.profile-name {
+ font-size: 1.5rem;
+ font-weight: 700;
+ color: var(--text-primary);
+ line-height: 1.2;
+}
+
+.profile-handle-row {
+ display: flex;
+ align-items: center;
+ gap: 12px;
+ margin-top: 4px;
+ flex-wrap: wrap;
+}
+
+.profile-handle-link {
+ color: var(--text-tertiary);
+ text-decoration: none;
+ font-size: 1rem;
+ transition: color 0.15s;
+}
+
+.profile-handle-link:hover {
+ color: var(--text-secondary);
+}
+
+.profile-bluesky-link {
+ display: inline-flex;
+ align-items: center;
+ gap: 6px;
+ color: #3b82f6;
+ text-decoration: none;
+ font-size: 0.85rem;
+ font-weight: 500;
+ padding: 2px 8px;
+ border-radius: var(--radius-sm);
+ background: rgba(59, 130, 246, 0.1);
+ transition: all 0.15s ease;
+}
+
+.profile-bluesky-link:hover {
+ background: rgba(59, 130, 246, 0.15);
+}
+
+.profile-stats {
+ display: flex;
+ gap: 24px;
+ margin-top: 12px;
+}
+
+.profile-stat {
+ color: var(--text-tertiary);
+ font-size: 0.9rem;
+}
+
+.profile-stat strong {
+ color: var(--text-primary);
+ font-weight: 600;
+}
+
+.profile-tabs {
+ display: flex;
+ gap: 24px;
+ margin-bottom: 24px;
+ border-bottom: 1px solid var(--border);
+}
+
+.profile-tab {
+ padding: 12px 0;
+ font-size: 0.95rem;
+ font-weight: 500;
+ color: var(--text-tertiary);
+ background: transparent;
+ border: none;
+ cursor: pointer;
+ transition: all 0.15s ease;
+ position: relative;
+}
+
+.profile-tab:hover {
+ color: var(--text-primary);
+}
+
+.profile-tab.active {
+ color: var(--text-primary);
+}
+
+.profile-tab.active::after {
+ content: "";
+ position: absolute;
+ bottom: -1px;
+ left: 0;
+ right: 0;
+ height: 2px;
+ background: var(--text-primary);
+}
+
+.profile-badge-wrapper {
+ display: inline-flex;
+ align-items: center;
+}
+
+.profile-badge-clickable {
+ position: relative;
+ display: inline-flex;
+ align-items: center;
+ cursor: pointer;
+ margin-left: 8px;
+}
+
+.badge-info-popover {
+ position: absolute;
+ top: calc(100% + 8px);
+ left: 50%;
+ transform: translateX(-50%);
+ padding: 16px;
+ background: var(--bg-elevated);
+ border: 1px solid var(--border);
+ border-radius: var(--radius-md);
+ box-shadow: var(--shadow-lg);
+ font-size: 0.85rem;
+ white-space: nowrap;
+ z-index: 100;
+ min-width: 200px;
+}
+
+.badge-info-title {
+ font-weight: 600;
+ color: var(--text-primary);
+ margin-bottom: 8px;
+}
+
+.verifier-link {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ padding: 8px;
+ background: var(--bg-tertiary);
+ border-radius: var(--radius-sm);
+ text-decoration: none;
+ transition: background 0.15s ease;
+}
+
+.verifier-link:hover {
+ background: var(--bg-hover);
+}
+
+.verifier-avatar {
+ width: 24px;
+ height: 24px;
+ border-radius: 50%;
+ object-fit: cover;
+}
+
+.verifier-name {
+ color: var(--text-primary);
+ font-size: 0.85rem;
+ font-weight: 500;
+}
+
+.profile-suspended {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ padding: 60px 20px;
+ text-align: center;
+ background: var(--bg-secondary);
+ border-radius: var(--radius-lg);
+ margin-top: 20px;
+ border: 1px solid var(--border);
+}
+
+.suspended-icon {
+ font-size: 40px;
+ margin-bottom: 16px;
+ color: var(--text-tertiary);
+}
+
+.profile-suspended h2 {
+ color: var(--text-primary);
+ margin-bottom: 8px;
+ font-size: 1.25rem;
+}
+
+@media (max-width: 640px) {
+ .profile-header {
+ flex-direction: column;
+ text-align: center;
+ }
+
+ .profile-info {
+ align-items: center;
+ }
+
+ .profile-handle-row {
+ justify-content: center;
+ }
+
+ .profile-stats {
+ justify-content: center;
+ }
+
+ .profile-tabs {
+ justify-content: center;
+ gap: 16px;
+ }
+}
\ No newline at end of file
diff --git a/web/src/css/skeleton.css b/web/src/css/skeleton.css
new file mode 100644
index 0000000..c7dff59
--- /dev/null
+++ b/web/src/css/skeleton.css
@@ -0,0 +1,106 @@
+@keyframes shimmer {
+ 0% {
+ background-position: -200% 0;
+ }
+
+ 100% {
+ background-position: 200% 0;
+ }
+}
+
+.skeleton {
+ background: linear-gradient(
+ 90deg,
+ var(--bg-tertiary) 25%,
+ var(--bg-secondary) 50%,
+ var(--bg-tertiary) 75%
+ );
+ background-size: 200% 100%;
+ animation: shimmer 1.5s infinite;
+ border-radius: var(--radius-sm);
+}
+
+.skeleton-card {
+ padding: 24px 0;
+ border-bottom: 1px solid var(--border);
+ display: flex;
+ flex-direction: column;
+ gap: 16px;
+}
+
+.skeleton-header {
+ display: flex;
+ align-items: center;
+ gap: 12px;
+}
+
+.skeleton-avatar {
+ width: 36px;
+ height: 36px;
+ border-radius: 50%;
+}
+
+.skeleton-meta {
+ display: flex;
+ flex-direction: column;
+ gap: 6px;
+}
+
+.skeleton-name {
+ width: 120px;
+ height: 14px;
+}
+
+.skeleton-handle {
+ width: 80px;
+ height: 12px;
+}
+
+.skeleton-content {
+ display: flex;
+ flex-direction: column;
+ gap: 12px;
+ padding-left: 48px;
+}
+
+.skeleton-source {
+ width: 180px;
+ height: 24px;
+ border-radius: var(--radius-full);
+}
+
+.skeleton-highlight {
+ width: 100%;
+ height: 60px;
+ border-left: 2px solid var(--border);
+}
+
+.skeleton-text-1 {
+ width: 90%;
+ height: 14px;
+}
+
+.skeleton-text-2 {
+ width: 60%;
+ height: 14px;
+}
+
+.skeleton-actions {
+ display: flex;
+ gap: 24px;
+ padding-left: 48px;
+ margin-top: 4px;
+}
+
+.skeleton-action {
+ width: 24px;
+ height: 24px;
+ border-radius: var(--radius-sm);
+}
+
+@media (max-width: 600px) {
+ .skeleton-content,
+ .skeleton-actions {
+ padding-left: 0;
+ }
+}
diff --git a/web/src/css/utilities.css b/web/src/css/utilities.css
new file mode 100644
index 0000000..262d441
--- /dev/null
+++ b/web/src/css/utilities.css
@@ -0,0 +1,730 @@
+.legal-content {
+ max-width: 800px;
+ margin: 0 auto;
+ padding: 20px;
+}
+
+.legal-content h1 {
+ font-size: 2rem;
+ margin-bottom: 8px;
+ color: var(--text-primary);
+}
+
+.legal-content h2 {
+ font-size: 1.4rem;
+ margin-top: 32px;
+ margin-bottom: 12px;
+ color: var(--text-primary);
+}
+
+.legal-content h3 {
+ font-size: 1.1rem;
+ margin-top: 20px;
+ margin-bottom: 8px;
+ color: var(--text-primary);
+}
+
+.legal-content p {
+ color: var(--text-secondary);
+ line-height: 1.7;
+ margin-bottom: 12px;
+}
+
+.legal-content ul {
+ color: var(--text-secondary);
+ line-height: 1.7;
+ margin-left: 24px;
+ margin-bottom: 12px;
+}
+
+.legal-content li {
+ margin-bottom: 6px;
+}
+
+.legal-content a {
+ color: var(--accent);
+ text-decoration: none;
+}
+
+.legal-content a:hover {
+ text-decoration: underline;
+}
+
+.legal-content section {
+ margin-bottom: 24px;
+}
+
+.text-secondary {
+ color: var(--text-secondary);
+}
+
+.text-error {
+ color: var(--error);
+}
+
+.text-center {
+ text-align: center;
+}
+
+.flex {
+ display: flex;
+}
+
+.items-center {
+ align-items: center;
+}
+
+.justify-center {
+ justify-content: center;
+}
+
+.justify-end {
+ justify-content: flex-end;
+}
+
+.gap-2 {
+ gap: 8px;
+}
+
+.gap-3 {
+ gap: 12px;
+}
+
+.mt-3 {
+ margin-top: 12px;
+}
+
+.mb-6 {
+ margin-bottom: 24px;
+}
+
+.composer {
+ margin-bottom: 24px;
+}
+
+.composer-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ margin-bottom: 12px;
+}
+
+.composer-title {
+ font-size: 1.1rem;
+ font-weight: 600;
+ color: var(--text-primary);
+ margin: 0;
+}
+
+.composer-input {
+ width: 100%;
+ min-height: 120px;
+ padding: 16px;
+ background: var(--bg-secondary);
+ border: 1px solid var(--border);
+ border-radius: var(--radius-md);
+ color: var(--text-primary);
+ font-size: 1rem;
+ resize: vertical;
+ transition: all 0.15s ease;
+}
+
+.composer-input:focus {
+ outline: none;
+ border-color: var(--accent);
+ box-shadow: 0 0 0 3px var(--accent-subtle);
+}
+
+.composer-footer {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ margin-top: 12px;
+}
+
+.composer-actions {
+ display: flex;
+ justify-content: flex-end;
+ gap: 8px;
+}
+
+.composer-count {
+ font-size: 0.85rem;
+ color: var(--text-tertiary);
+}
+
+.composer-count.warning {
+ color: var(--warning);
+}
+
+.composer-count.error {
+ color: var(--error);
+}
+
+.composer-char-count.warning {
+ color: var(--warning);
+}
+
+.composer-char-count.error {
+ color: var(--error);
+}
+
+.composer-add-quote {
+ width: 100%;
+ padding: 12px 16px;
+ margin-bottom: 12px;
+ background: var(--bg-tertiary);
+ border: 1px dashed var(--border);
+ border-radius: var(--radius-md);
+ color: var(--text-secondary);
+ font-size: 0.9rem;
+ cursor: pointer;
+ transition: all 0.15s ease;
+}
+
+.composer-add-quote:hover {
+ border-color: var(--accent);
+ color: var(--accent);
+ background: var(--accent-subtle);
+}
+
+.composer-quote-input-wrapper {
+ margin-bottom: 12px;
+}
+
+.composer-quote-input {
+ width: 100%;
+ padding: 12px 16px;
+ background: linear-gradient(
+ 135deg,
+ rgba(79, 70, 229, 0.05),
+ rgba(168, 85, 247, 0.05)
+ );
+ border: 1px solid var(--border);
+ border-left: 3px solid var(--accent);
+ border-radius: 0 var(--radius-md) var(--radius-md) 0;
+ color: var(--text-primary);
+ font-size: 0.95rem;
+ font-style: italic;
+ resize: vertical;
+ font-family: inherit;
+ transition: all 0.15s ease;
+}
+
+.composer-quote-input:focus {
+ outline: none;
+ border-color: var(--accent);
+}
+
+.composer-quote-input::placeholder {
+ color: var(--text-tertiary);
+ font-style: italic;
+}
+
+.composer-quote-remove-btn {
+ margin-top: 8px;
+ padding: 6px 12px;
+ background: none;
+ border: none;
+ color: var(--text-tertiary);
+ font-size: 0.85rem;
+ cursor: pointer;
+}
+
+.composer-quote-remove-btn:hover {
+ color: var(--error);
+}
+
+.composer-error {
+ margin-top: 12px;
+ padding: 12px;
+ background: rgba(239, 68, 68, 0.1);
+ border: 1px solid rgba(239, 68, 68, 0.3);
+ border-radius: var(--radius-md);
+ color: var(--error);
+ font-size: 0.9rem;
+}
+
+.composer-url {
+ font-size: 0.85rem;
+ color: var(--text-secondary);
+ word-break: break-all;
+}
+
+.composer-quote {
+ position: relative;
+ padding: 12px 16px;
+ padding-right: 36px;
+ background: var(--bg-secondary);
+ border-left: 3px solid var(--accent);
+ border-radius: 0 var(--radius-sm) var(--radius-sm) 0;
+ margin-bottom: 16px;
+ font-style: italic;
+ color: var(--text-secondary);
+}
+
+.composer-quote-remove {
+ position: absolute;
+ top: 8px;
+ right: 8px;
+ width: 24px;
+ height: 24px;
+ border-radius: var(--radius-full);
+ background: var(--bg-tertiary);
+ color: var(--text-secondary);
+ font-size: 1rem;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+}
+
+.composer-quote-remove:hover {
+ background: var(--bg-hover);
+ color: var(--text-primary);
+}
+
+.composer-tags {
+ flex: 1;
+}
+
+.composer-meta-row {
+ display: flex;
+ gap: 12px;
+ margin-top: 12px;
+ align-items: flex-start;
+}
+
+.composer-labels-wrapper {
+ position: relative;
+}
+
+.composer-labels-btn {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ width: 42px;
+ height: 42px;
+ background: var(--bg-secondary);
+ border: 1px solid var(--border);
+ border-radius: var(--radius-md);
+ cursor: pointer;
+ color: var(--text-tertiary);
+ transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
+ position: relative;
+}
+
+.composer-labels-btn:hover {
+ color: var(--text-primary);
+ background: var(--bg-hover);
+ border-color: var(--text-tertiary);
+}
+
+.composer-labels-btn.active {
+ color: var(--accent);
+ background: var(--accent-subtle);
+ border-color: var(--accent);
+}
+
+.composer-labels-badge {
+ position: absolute;
+ top: -4px;
+ right: -4px;
+ background: var(--error);
+ color: white;
+ font-size: 0.7rem;
+ width: 18px;
+ height: 18px;
+ border-radius: 50%;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ font-weight: bold;
+ border: 2px solid var(--bg-primary);
+ box-shadow: 0 1px 3px rgba(0, 0, 0, 0.2);
+}
+
+.composer-labels-picker {
+ position: absolute;
+ bottom: 100%;
+ right: 0;
+ margin-bottom: 12px;
+ background: var(--bg-elevated);
+ border: 1px solid var(--border);
+ border-radius: var(--radius-md);
+ padding: 8px 0;
+ min-width: 200px;
+ box-shadow: 0 10px 30px rgba(0, 0, 0, 0.25);
+ z-index: 50;
+ animation: scaleIn 0.2s ease-out forwards;
+ transform-origin: bottom right;
+}
+
+@keyframes scaleIn {
+ from {
+ opacity: 0;
+ transform: scale(0.95) translateY(5px);
+ }
+
+ to {
+ opacity: 1;
+ transform: scale(1) translateY(0);
+ }
+}
+
+.picker-header {
+ font-size: 0.75rem;
+ font-weight: 600;
+ color: var(--text-tertiary);
+ text-transform: uppercase;
+ letter-spacing: 0.05em;
+ margin-bottom: 4px;
+ padding: 4px 12px 8px;
+ border-bottom: 1px solid var(--border);
+}
+
+.picker-item {
+ display: flex;
+ align-items: center;
+ gap: 10px;
+ padding: 10px 14px;
+ cursor: pointer;
+ color: var(--text-secondary);
+ font-size: 0.9rem;
+ transition: all 0.15s ease;
+ user-select: none;
+}
+
+.picker-item:hover {
+ background: var(--bg-hover);
+ color: var(--text-primary);
+}
+
+.picker-checkbox-wrapper {
+ position: relative;
+ width: 18px;
+ height: 18px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+}
+
+.picker-checkbox-wrapper input {
+ position: absolute;
+ opacity: 0;
+ width: 100%;
+ height: 100%;
+ cursor: pointer;
+ z-index: 10;
+}
+
+.picker-checkbox-custom {
+ width: 18px;
+ height: 18px;
+ border: 2px solid var(--text-tertiary);
+ border-radius: 4px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ background: transparent;
+ transition: all 0.2s ease;
+ color: white;
+}
+
+.picker-item:hover .picker-checkbox-custom {
+ border-color: var(--text-secondary);
+}
+
+.picker-checkbox-wrapper input:checked + .picker-checkbox-custom {
+ background: var(--accent);
+ border-color: var(--accent);
+ color: white;
+}
+
+.composer-tags-input {
+ width: 100%;
+ padding: 12px 16px;
+ background: var(--bg-secondary);
+ border: 1px solid var(--border);
+ border-radius: var(--radius-md);
+ color: var(--text-primary);
+ font-size: 0.95rem;
+ transition: all 0.15s ease;
+}
+
+.composer-tags-input:focus {
+ outline: none;
+ border-color: var(--accent);
+ box-shadow: 0 0 0 3px var(--accent-subtle);
+}
+
+.composer-tags-input::placeholder {
+ color: var(--text-tertiary);
+}
+
+.history-panel {
+ background: var(--bg-tertiary);
+ border: 1px solid var(--border);
+ border-radius: var(--radius-md);
+ padding: 1rem;
+ margin-bottom: 1rem;
+ font-size: 0.9rem;
+ animation: fadeIn 0.2s ease-out;
+}
+
+.history-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ margin-bottom: 1rem;
+ padding-bottom: 0.5rem;
+ border-bottom: 1px solid var(--border);
+}
+
+.history-title {
+ font-weight: 600;
+ text-transform: uppercase;
+ letter-spacing: 0.05em;
+ font-size: 0.75rem;
+ color: var(--text-secondary);
+}
+
+.history-list {
+ list-style: none;
+ display: flex;
+ flex-direction: column;
+ gap: 1rem;
+}
+
+.history-item {
+ position: relative;
+ padding-left: 1rem;
+ border-left: 2px solid var(--border);
+}
+
+.history-date {
+ font-size: 0.75rem;
+ color: var(--text-tertiary);
+ margin-bottom: 0.25rem;
+}
+
+.history-content {
+ color: var(--text-secondary);
+ white-space: pre-wrap;
+}
+
+.history-close-btn {
+ color: var(--text-tertiary);
+ padding: 4px;
+ border-radius: var(--radius-sm);
+ transition: all 0.2s;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+}
+
+.history-close-btn:hover {
+ background: var(--bg-hover);
+ color: var(--text-primary);
+}
+
+.history-status {
+ text-align: center;
+ color: var(--text-tertiary);
+ font-style: italic;
+ padding: 1rem;
+}
+
+.share-menu-container {
+ position: relative;
+}
+
+.share-menu {
+ position: absolute;
+ top: 100%;
+ right: 0;
+ margin-top: 8px;
+ background: var(--bg-primary);
+ border: 1px solid var(--border);
+ border-radius: var(--radius-lg);
+ box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15);
+ min-width: 180px;
+ padding: 8px 0;
+ z-index: 100;
+ animation: fadeInUp 0.15s ease;
+}
+
+@keyframes fadeInUp {
+ from {
+ opacity: 0;
+ transform: translateY(-8px);
+ }
+
+ to {
+ opacity: 1;
+ transform: translateY(0);
+ }
+}
+
+.share-menu-section {
+ display: flex;
+ flex-direction: column;
+}
+
+.share-menu-label {
+ padding: 4px 12px 8px;
+ font-size: 0.7rem;
+ font-weight: 600;
+ text-transform: uppercase;
+ letter-spacing: 0.05em;
+ color: var(--text-tertiary);
+}
+
+.share-menu-item {
+ display: flex;
+ align-items: center;
+ gap: 10px;
+ padding: 10px 14px;
+ background: none;
+ border: none;
+ width: 100%;
+ text-align: left;
+ font-size: 0.9rem;
+ color: var(--text-primary);
+ cursor: pointer;
+ transition: all 0.1s ease;
+}
+
+.share-menu-item:hover {
+ background: var(--bg-tertiary);
+}
+
+.share-menu-icon {
+ font-size: 1.1rem;
+ width: 24px;
+ text-align: center;
+}
+
+.share-menu-divider {
+ height: 1px;
+ background: var(--border);
+ margin: 6px 0;
+}
+
+.bookmark-card {
+ display: flex;
+ flex-direction: column;
+ gap: 16px;
+}
+
+.bookmark-preview {
+ display: flex;
+ flex-direction: column;
+ background: var(--bg-secondary);
+ border: 1px solid var(--border);
+ border-radius: var(--radius-md);
+ overflow: hidden;
+ text-decoration: none;
+ transition: all 0.2s ease;
+ position: relative;
+}
+
+.bookmark-preview:hover {
+ border-color: var(--accent);
+ box-shadow: var(--shadow-sm);
+ transform: translateY(-1px);
+}
+
+.bookmark-preview::before {
+ content: "";
+ position: absolute;
+ left: 0;
+ top: 0;
+ bottom: 0;
+ width: 4px;
+ background: var(--accent);
+ opacity: 0.7;
+}
+
+.bookmark-preview-content {
+ padding: 16px 20px;
+ display: flex;
+ flex-direction: column;
+ gap: 8px;
+}
+
+.bookmark-preview-header {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ margin-bottom: 4px;
+}
+
+.bookmark-preview-site {
+ display: flex;
+ align-items: center;
+ gap: 6px;
+ font-size: 0.75rem;
+ font-weight: 600;
+ color: var(--accent);
+ text-transform: uppercase;
+ letter-spacing: 0.03em;
+}
+
+.bookmark-preview-title {
+ font-size: 1rem;
+ font-weight: 600;
+ line-height: 1.4;
+ color: var(--text-primary);
+ margin: 0;
+ display: -webkit-box;
+ -webkit-line-clamp: 2;
+ line-clamp: 2;
+ -webkit-box-orient: vertical;
+ overflow: hidden;
+}
+
+.bookmark-preview-desc {
+ font-size: 0.875rem;
+ color: var(--text-secondary);
+ line-height: 1.5;
+ margin: 0;
+ display: -webkit-box;
+ -webkit-line-clamp: 2;
+ line-clamp: 2;
+ -webkit-box-orient: vertical;
+ overflow: hidden;
+}
+
+.bookmark-preview-arrow {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ color: var(--text-tertiary);
+ padding: 0 4px;
+ transition: all 0.2s ease;
+}
+
+.bookmark-preview:hover .bookmark-preview-arrow {
+ color: var(--accent);
+ transform: translateX(2px);
+}
+
+.bookmark-description {
+ font-size: 0.9rem;
+ color: var(--text-secondary);
+ margin: 0;
+ line-height: 1.5;
+}
+
+.bookmark-meta {
+ display: flex;
+ align-items: center;
+ gap: 12px;
+ margin-top: 12px;
+ font-size: 0.85rem;
+ color: var(--text-tertiary);
+}
+
+.bookmark-time {
+ color: var(--text-tertiary);
+}
diff --git a/web/src/index.css b/web/src/index.css
index 181fde3..6f06a3a 100644
--- a/web/src/index.css
+++ b/web/src/index.css
@@ -1,3424 +1,13 @@
-:root {
- --bg-primary: #0c0a14;
- --bg-secondary: #110e1c;
- --bg-tertiary: #1a1528;
- --bg-card: #14111f;
- --bg-hover: #1e1932;
- --bg-elevated: #1a1528;
-
- --text-primary: #f4f0ff;
- --text-secondary: #a89ec8;
- --text-tertiary: #6b5f8a;
-
- --accent: #a855f7;
- --accent-hover: #c084fc;
- --accent-subtle: rgba(168, 85, 247, 0.15);
-
- --border: #2d2640;
- --border-hover: #3d3560;
-
- --success: #22c55e;
- --error: #ef4444;
- --warning: #f59e0b;
-
- --radius-sm: 6px;
- --radius-md: 10px;
- --radius-lg: 16px;
- --radius-full: 9999px;
-
- --shadow-sm: 0 1px 3px rgba(0, 0, 0, 0.3);
- --shadow-md: 0 4px 12px rgba(0, 0, 0, 0.4);
- --shadow-lg: 0 10px 25px rgba(0, 0, 0, 0.5), 0 0 40px rgba(168, 85, 247, 0.1);
- --shadow-glow: 0 0 20px rgba(168, 85, 247, 0.3);
-
- --font-sans:
- "Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
-}
-
-* {
- margin: 0;
- padding: 0;
- box-sizing: border-box;
-}
-
-html {
- font-size: 16px;
-}
-
-body {
- font-family: var(--font-sans);
- background: var(--bg-primary);
- color: var(--text-primary);
- line-height: 1.6;
- min-height: 100vh;
- -webkit-font-smoothing: antialiased;
- -moz-osx-font-smoothing: grayscale;
-}
-
-a {
- color: var(--accent);
- text-decoration: none;
- transition: color 0.15s ease;
-}
-
-a:hover {
- color: var(--accent-hover);
-}
-
-button {
- font-family: inherit;
- cursor: pointer;
- border: none;
- background: none;
-}
-
-input,
-textarea {
- font-family: inherit;
- font-size: inherit;
-}
-
-.app {
- min-height: 100vh;
- display: flex;
- flex-direction: column;
-}
-
-.main-content {
- flex: 1;
- max-width: 680px;
- width: 100%;
- margin: 0 auto;
- padding: 24px 16px;
-}
-
-.btn {
- display: inline-flex;
- align-items: center;
- justify-content: center;
- gap: 8px;
- padding: 10px 20px;
- font-size: 0.9rem;
- font-weight: 500;
- border-radius: var(--radius-md);
- transition: all 0.15s ease;
-}
-
-.btn-primary {
- background: var(--accent);
- color: white;
-}
-
-.btn-primary:hover {
- background: var(--accent-hover);
- transform: translateY(-1px);
- box-shadow: var(--shadow-md);
-}
-
-.btn-secondary {
- background: var(--bg-tertiary);
- color: var(--text-primary);
- border: 1px solid var(--border);
-}
-
-.btn-secondary:hover {
- background: var(--bg-hover);
- border-color: var(--border-hover);
-}
-
-.btn-ghost {
- color: var(--text-secondary);
- padding: 8px 12px;
-}
-
-.btn-ghost:hover {
- color: var(--text-primary);
- background: var(--bg-tertiary);
-}
-
-.card {
- background: var(--bg-card);
- border: 1px solid var(--border);
- border-radius: var(--radius-lg);
- padding: 24px;
- transition: all 0.2s ease;
- position: relative;
-}
-
-.card:hover {
- border-color: var(--border-hover);
- box-shadow: var(--shadow-md);
- transform: translateY(-1px);
-}
-
-.annotation-card {
- display: flex;
- flex-direction: column;
- gap: 16px;
-}
-
-.annotation-header {
- display: flex;
- justify-content: space-between;
- align-items: flex-start;
- gap: 12px;
-}
-
-.annotation-header-left {
- display: flex;
- align-items: center;
- gap: 12px;
- flex: 1;
- min-width: 0;
-}
-
-.annotation-avatar {
- width: 40px;
- height: 40px;
- min-width: 40px;
- border-radius: var(--radius-full);
- background: linear-gradient(135deg, var(--accent), #a855f7);
- display: flex;
- align-items: center;
- justify-content: center;
- font-weight: 600;
- font-size: 0.95rem;
- color: white;
- overflow: hidden;
- box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);
-}
-
-.annotation-avatar img {
- width: 100%;
- height: 100%;
- object-fit: cover;
-}
-
-.annotation-meta {
- display: flex;
- flex-direction: column;
- justify-content: center;
- line-height: 1.3;
-}
-
-.annotation-avatar-link {
- text-decoration: none;
- border-radius: var(--radius-full);
- transition: transform 0.15s ease;
-}
-
-.annotation-avatar-link:hover {
- transform: scale(1.05);
-}
-
-.annotation-author-row {
- display: flex;
- align-items: center;
- gap: 6px;
- flex-wrap: wrap;
-}
-
-.annotation-author {
- font-weight: 600;
- color: var(--text-primary);
- font-size: 0.95rem;
-}
-
-.annotation-handle {
- font-size: 0.85rem;
- color: var(--text-tertiary);
- text-decoration: none;
- display: flex;
- align-items: center;
- gap: 3px;
-}
-
-.annotation-handle:hover {
- color: var(--accent);
-}
-
-.annotation-time {
- font-size: 0.8rem;
- color: var(--text-tertiary);
-}
-
-.annotation-content {
- display: flex;
- flex-direction: column;
- gap: 12px;
-}
-
-.annotation-source {
- display: inline-flex;
- align-items: center;
- gap: 6px;
- font-size: 0.8rem;
- color: var(--text-tertiary);
- text-decoration: none;
- padding: 4px 10px;
- background: var(--bg-tertiary);
- border-radius: var(--radius-full);
- width: fit-content;
- transition: all 0.15s ease;
- max-width: 100%;
- overflow: hidden;
- text-overflow: ellipsis;
- white-space: nowrap;
-}
-
-.annotation-source:hover {
- color: var(--text-primary);
- background: var(--bg-hover);
-}
-
-.annotation-source-title {
- color: var(--text-secondary);
- opacity: 0.8;
-}
-
-.annotation-highlight {
- display: block;
- position: relative;
- padding: 16px 20px;
- background: linear-gradient(
- 135deg,
- rgba(79, 70, 229, 0.03),
- rgba(168, 85, 247, 0.03)
- );
- border-left: 3px solid var(--accent);
- border-radius: 4px var(--radius-md) var(--radius-md) 4px;
- text-decoration: none;
- transition: all 0.2s ease;
- margin: 4px 0;
-}
-
-.annotation-highlight:hover {
- background: linear-gradient(
- 135deg,
- rgba(79, 70, 229, 0.08),
- rgba(168, 85, 247, 0.08)
- );
- transform: translateX(2px);
-}
-
-.annotation-highlight mark {
- background: transparent;
- color: var(--text-primary);
- font-style: italic;
- font-size: 1.05rem;
- line-height: 1.6;
- font-weight: 400;
- display: inline;
-}
-
-.annotation-text {
- font-size: 1rem;
- line-height: 1.65;
- color: var(--text-primary);
- white-space: pre-wrap;
-}
-
-.annotation-actions {
- display: flex;
- align-items: center;
- justify-content: space-between;
- padding-top: 16px;
- margin-top: 8px;
- border-top: 1px solid rgba(255, 255, 255, 0.03);
-}
-
-.annotation-actions-left {
- display: flex;
- align-items: center;
- gap: 8px;
-}
-
-.annotation-action {
- display: flex;
- align-items: center;
- gap: 6px;
- color: var(--text-tertiary);
- font-size: 0.85rem;
- font-weight: 500;
- padding: 6px 10px;
- border-radius: var(--radius-md);
- transition: all 0.2s ease;
- background: transparent;
- cursor: pointer;
-}
-
-.annotation-action:hover {
- color: var(--text-secondary);
- background: var(--bg-elevated);
-}
-
-.annotation-action.liked {
- color: #ef4444;
- background: rgba(239, 68, 68, 0.05);
-}
-
-.annotation-action.liked:hover {
- background: rgba(239, 68, 68, 0.1);
-}
-
-.annotation-action.active {
- color: var(--accent);
- background: var(--accent-subtle);
-}
-
-.action-icon-only {
- padding: 8px;
-}
-
-.annotation-delete {
- background: none;
- border: none;
- cursor: pointer;
- padding: 8px;
- font-size: 1rem;
- color: var(--text-tertiary);
- transition: all 0.2s ease;
- border-radius: var(--radius-md);
- opacity: 0.6;
-}
-
-.annotation-delete:hover {
- color: var(--error);
- background: rgba(239, 68, 68, 0.1);
- opacity: 1;
-}
-
-.annotation-delete:disabled {
- cursor: not-allowed;
- opacity: 0.3;
-}
-
-.share-menu-container {
- position: relative;
-}
-
-.share-menu {
- position: absolute;
- top: 100%;
- right: 0;
- margin-top: 8px;
- background: var(--bg-primary);
- border: 1px solid var(--border);
- border-radius: var(--radius-lg);
- box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15);
- min-width: 180px;
- padding: 8px 0;
- z-index: 100;
- animation: fadeInUp 0.15s ease;
-}
-
-@keyframes fadeInUp {
- from {
- opacity: 0;
- transform: translateY(-8px);
- }
-
- to {
- opacity: 1;
- transform: translateY(0);
- }
-}
-
-.share-menu-section {
- display: flex;
- flex-direction: column;
-}
-
-.share-menu-label {
- padding: 4px 12px 8px;
- font-size: 0.7rem;
- font-weight: 600;
- text-transform: uppercase;
- letter-spacing: 0.05em;
- color: var(--text-tertiary);
-}
-
-.share-menu-item {
- display: flex;
- align-items: center;
- gap: 10px;
- padding: 10px 14px;
- background: none;
- border: none;
- width: 100%;
- text-align: left;
- font-size: 0.9rem;
- color: var(--text-primary);
- cursor: pointer;
- transition: all 0.1s ease;
-}
-
-.share-menu-item:hover {
- background: var(--bg-tertiary);
-}
-
-.share-menu-icon {
- font-size: 1.1rem;
- width: 24px;
- text-align: center;
-}
-
-.share-menu-divider {
- height: 1px;
- background: var(--border);
- margin: 6px 0;
-}
-
-.feed {
- display: flex;
- flex-direction: column;
- gap: 16px;
-}
-
-.feed-header {
- display: flex;
- align-items: center;
- justify-content: space-between;
- margin-bottom: 8px;
-}
-
-.feed-title {
- font-size: 1.5rem;
- font-weight: 700;
-}
-
-.page-header {
- margin-bottom: 32px;
-}
-
-.page-title {
- font-size: 2rem;
- font-weight: 700;
- margin-bottom: 8px;
-}
-
-.page-description {
- color: var(--text-secondary);
- font-size: 1.1rem;
-}
-
-.url-input-wrapper {
- margin-bottom: 32px;
-}
-
-.url-input-container {
- display: flex;
- gap: 12px;
-}
-
-.url-input {
- flex: 1;
- padding: 14px 18px;
- background: var(--bg-secondary);
- border: 1px solid var(--border);
- border-radius: var(--radius-md);
- color: var(--text-primary);
- font-size: 1rem;
- transition: all 0.15s ease;
-}
-
-.url-input:focus {
- outline: none;
- border-color: var(--accent);
- box-shadow: 0 0 0 3px var(--accent-subtle);
-}
-
-.url-input::placeholder {
- color: var(--text-tertiary);
-}
-
-.empty-state {
- text-align: center;
- padding: 60px 20px;
- color: var(--text-secondary);
-}
-
-.empty-state-icon {
- font-size: 3rem;
- margin-bottom: 16px;
- opacity: 0.5;
-}
-
-.empty-state-title {
- font-size: 1.25rem;
- font-weight: 600;
- color: var(--text-primary);
- margin-bottom: 8px;
-}
-
-.empty-state-text {
- font-size: 1rem;
- max-width: 400px;
- margin: 0 auto;
-}
-
-.feed-filters {
- display: flex;
- gap: 8px;
- margin-bottom: 24px;
- padding: 4px;
- background: var(--bg-tertiary);
- border-radius: var(--radius-lg);
- width: fit-content;
-}
-
-.login-page {
- display: flex;
- flex-direction: column;
- align-items: center;
- justify-content: center;
- min-height: 70vh;
- padding: 60px 20px;
- width: 100%;
- max-width: 500px;
- margin: 0 auto;
-}
-
-.login-at-logo {
- font-size: 5rem;
- font-weight: 800;
- color: var(--accent);
- margin-bottom: 24px;
- line-height: 1;
-}
-
-.login-heading {
- font-size: 1.5rem;
- font-weight: 600;
- margin-bottom: 32px;
- display: flex;
- align-items: center;
- gap: 10px;
- text-align: center;
- line-height: 1.4;
-}
-
-.login-help-btn {
- background: none;
- border: none;
- color: var(--text-tertiary);
- cursor: pointer;
- padding: 4px;
- display: flex;
- align-items: center;
- transition: color 0.15s;
- flex-shrink: 0;
-}
-
-.login-help-btn:hover {
- color: var(--accent);
-}
-
-.login-help-text {
- background: var(--bg-elevated);
- border: 1px solid var(--border);
- border-radius: var(--radius-md);
- padding: 16px 20px;
- margin-bottom: 24px;
- font-size: 0.95rem;
- color: var(--text-secondary);
- line-height: 1.6;
- text-align: center;
-}
-
-.login-help-text code {
- background: var(--bg-tertiary);
- padding: 2px 8px;
- border-radius: var(--radius-sm);
- font-size: 0.9rem;
-}
-
-.login-form {
- display: flex;
- flex-direction: column;
- gap: 20px;
- width: 100%;
-}
-
-.login-input-wrapper {
- position: relative;
-}
-
-.login-input {
- width: 100%;
- padding: 18px 20px;
- background: var(--bg-elevated);
- border: 2px solid var(--border);
- border-radius: var(--radius-lg);
- color: var(--text-primary);
- font-size: 1.1rem;
- transition:
- border-color 0.15s,
- box-shadow 0.15s;
-}
-
-.login-input:focus {
- outline: none;
- border-color: var(--accent);
- box-shadow: 0 0 0 4px rgba(99, 102, 241, 0.15);
-}
-
-.login-input::placeholder {
- color: var(--text-tertiary);
-}
-
-.login-suggestions {
- position: absolute;
- top: calc(100% + 8px);
- left: 0;
- right: 0;
- background: var(--bg-card);
- border: 1px solid var(--border);
- border-radius: var(--radius-lg);
- box-shadow: var(--shadow-lg);
- overflow: hidden;
- z-index: 100;
-}
-
-.login-suggestion {
- display: flex;
- align-items: center;
- gap: 14px;
- width: 100%;
- padding: 14px 18px;
- background: transparent;
- border: none;
- cursor: pointer;
- text-align: left;
- color: var(--text-primary);
- transition: background 0.1s;
-}
-
-.login-suggestion:hover,
-.login-suggestion.selected {
- background: var(--bg-elevated);
-}
-
-.login-suggestion-avatar {
- width: 44px;
- height: 44px;
- border-radius: var(--radius-full);
- background: linear-gradient(135deg, var(--accent), #a855f7);
- display: flex;
- align-items: center;
- justify-content: center;
- flex-shrink: 0;
- overflow: hidden;
- font-size: 0.9rem;
- font-weight: 600;
- color: white;
-}
-
-.login-suggestion-avatar img {
- width: 100%;
- height: 100%;
- object-fit: cover;
-}
-
-.login-suggestion-info {
- display: flex;
- flex-direction: column;
- gap: 2px;
- min-width: 0;
-}
-
-.login-suggestion-name {
- font-weight: 600;
- font-size: 1rem;
- color: var(--text-primary);
- white-space: nowrap;
- overflow: hidden;
- text-overflow: ellipsis;
-}
-
-.login-suggestion-handle {
- font-size: 0.9rem;
- color: var(--text-secondary);
- white-space: nowrap;
- overflow: hidden;
- text-overflow: ellipsis;
-}
-
-.login-error {
- padding: 12px 16px;
- background: rgba(239, 68, 68, 0.1);
- border: 1px solid rgba(239, 68, 68, 0.3);
- border-radius: var(--radius-md);
- color: #ef4444;
- font-size: 0.9rem;
- text-align: center;
-}
-
-.login-submit {
- padding: 18px 32px;
- font-size: 1.1rem;
- font-weight: 600;
-}
-
-.login-avatar-large {
- width: 100px;
- height: 100px;
- border-radius: var(--radius-full);
- background: linear-gradient(135deg, var(--accent), #a855f7);
- display: flex;
- align-items: center;
- justify-content: center;
- margin-bottom: 20px;
- font-weight: 700;
- font-size: 2rem;
- color: white;
- overflow: hidden;
-}
-
-.login-avatar-large img {
- width: 100%;
- height: 100%;
- object-fit: cover;
-}
-
-.login-welcome {
- font-size: 1.5rem;
- font-weight: 600;
- margin-bottom: 32px;
- text-align: center;
-}
-
-.login-actions {
- display: flex;
- flex-direction: column;
- gap: 12px;
- width: 100%;
-}
-
-.login-avatar {
- width: 72px;
- height: 72px;
- border-radius: var(--radius-full);
- background: linear-gradient(135deg, var(--accent), #a855f7);
- display: flex;
- align-items: center;
- justify-content: center;
- margin: 0 auto 16px;
- font-weight: 700;
- font-size: 1.5rem;
- color: white;
- overflow: hidden;
-}
-
-.login-avatar img {
- width: 100%;
- height: 100%;
- object-fit: cover;
-}
-
-.login-welcome-name {
- font-size: 1.25rem;
- font-weight: 600;
- margin-bottom: 24px;
-}
-
-.login-actions {
- display: flex;
- flex-direction: column;
- gap: 12px;
-}
-
-.btn-bluesky {
- background: #0085ff;
- color: white;
- display: flex;
- align-items: center;
- justify-content: center;
- gap: 10px;
- transition:
- background 0.2s,
- transform 0.2s;
-}
-
-.btn-bluesky:hover {
- background: #0070dd;
- transform: translateY(-1px);
-}
-
-.login-btn {
- width: 100%;
- padding: 14px 24px;
- font-size: 1rem;
- font-weight: 600;
-}
-
-.login-brand {
- display: flex;
- align-items: center;
- justify-content: center;
- gap: 12px;
- margin-bottom: 24px;
-}
-
-.login-brand-icon {
- width: 48px;
- height: 48px;
- background: linear-gradient(135deg, var(--accent), #a855f7);
- border-radius: var(--radius-lg);
- display: flex;
- align-items: center;
- justify-content: center;
- font-size: 1.75rem;
- font-weight: 800;
- color: white;
-}
-
-.login-brand-name {
- font-size: 1.75rem;
- font-weight: 700;
-}
-
-.login-form {
- display: flex;
- flex-direction: column;
- gap: 16px;
-}
-
-.login-input-wrapper {
- position: relative;
-}
-
-.login-input {
- width: 100%;
- padding: 14px 16px;
- background: var(--bg-elevated);
- border: 1px solid var(--border);
- border-radius: var(--radius-md);
- color: var(--text-primary);
- font-size: 1rem;
- transition:
- border-color 0.15s,
- box-shadow 0.15s;
-}
-
-.login-input:focus {
- outline: none;
- border-color: var(--accent);
- box-shadow: 0 0 0 3px rgba(99, 102, 241, 0.15);
-}
-
-.login-input::placeholder {
- color: var(--text-tertiary);
-}
-
-.login-suggestions {
- position: absolute;
- top: calc(100% + 4px);
- left: 0;
- right: 0;
- background: var(--bg-card);
- border: 1px solid var(--border);
- border-radius: var(--radius-md);
- box-shadow: var(--shadow-lg);
- overflow: hidden;
- z-index: 100;
-}
-
-.login-suggestion {
- display: flex;
- align-items: center;
- gap: 12px;
- width: 100%;
- padding: 12px 16px;
- background: transparent;
- border: none;
- cursor: pointer;
- text-align: left;
- transition: background 0.1s;
-}
-
-.login-suggestion:hover,
-.login-suggestion.selected {
- background: var(--bg-elevated);
-}
-
-.login-suggestion-avatar {
- width: 40px;
- height: 40px;
- border-radius: var(--radius-full);
- background: linear-gradient(135deg, var(--accent), #a855f7);
- display: flex;
- align-items: center;
- justify-content: center;
- flex-shrink: 0;
- overflow: hidden;
- font-size: 0.875rem;
- font-weight: 600;
- color: white;
-}
-
-.login-suggestion-avatar img {
- width: 100%;
- height: 100%;
- object-fit: cover;
-}
-
-.login-suggestion-info {
- display: flex;
- flex-direction: column;
- min-width: 0;
-}
-
-.login-suggestion-name {
- font-weight: 600;
- color: var(--text-primary);
- white-space: nowrap;
- overflow: hidden;
- text-overflow: ellipsis;
-}
-
-.login-suggestion-handle {
- font-size: 0.875rem;
- color: var(--text-secondary);
- white-space: nowrap;
- overflow: hidden;
- text-overflow: ellipsis;
-}
-
-.login-error {
- padding: 12px 16px;
- background: rgba(239, 68, 68, 0.1);
- border: 1px solid rgba(239, 68, 68, 0.3);
- border-radius: var(--radius-md);
- color: #ef4444;
- font-size: 0.875rem;
-}
-
-.login-legal {
- font-size: 0.75rem;
- color: var(--text-tertiary);
- line-height: 1.5;
- margin-top: 16px;
-}
-
-.profile-header {
- display: flex;
- align-items: center;
- gap: 20px;
- margin-bottom: 32px;
- padding-bottom: 24px;
- border-bottom: 1px solid var(--border);
-}
-
-.profile-avatar {
- width: 80px;
- height: 80px;
- min-width: 80px;
- border-radius: var(--radius-full);
- background: linear-gradient(135deg, var(--accent), #a855f7);
- display: flex;
- align-items: center;
- justify-content: center;
- font-weight: 700;
- font-size: 2rem;
- color: white;
- overflow: hidden;
-}
-
-.profile-avatar img {
- width: 100%;
- height: 100%;
- object-fit: cover;
-}
-
-.profile-avatar-link {
- text-decoration: none;
-}
-
-.profile-info {
- flex: 1;
-}
-
-.profile-name {
- font-size: 1.5rem;
- font-weight: 700;
-}
-
-.profile-handle-link {
- color: var(--text-secondary);
- text-decoration: none;
-}
-
-.profile-handle-link:hover {
- color: var(--accent);
- text-decoration: underline;
-}
-
-.profile-bluesky-link {
- display: inline-flex;
- align-items: center;
- gap: 6px;
- color: #0085ff;
- text-decoration: none;
- font-size: 0.95rem;
- padding: 4px 10px;
- border-radius: var(--radius-md);
- background: rgba(0, 133, 255, 0.1);
- transition: all 0.15s ease;
-}
-
-.profile-bluesky-link:hover {
- background: rgba(0, 133, 255, 0.2);
- color: #0070dd;
-}
-
-.profile-stats {
- display: flex;
- gap: 24px;
- margin-top: 8px;
-}
-
-.profile-stat {
- color: var(--text-secondary);
- font-size: 0.9rem;
-}
-
-.profile-stat strong {
- color: var(--text-primary);
-}
-
-.profile-tabs {
- display: flex;
- gap: 0;
- margin-bottom: 24px;
- border-bottom: 1px solid var(--border);
-}
-
-.profile-tab {
- padding: 12px 20px;
- font-size: 0.9rem;
- font-weight: 500;
- color: var(--text-secondary);
- background: transparent;
- border: none;
- border-bottom: 2px solid transparent;
- cursor: pointer;
- transition: all 0.15s ease;
- margin-bottom: -1px;
-}
-
-.profile-tab:hover {
- color: var(--text-primary);
- background: var(--bg-tertiary);
-}
-
-.profile-tab.active {
- color: var(--accent);
- border-bottom-color: var(--accent);
-}
-
-.bookmark-description {
- font-size: 0.9rem;
- color: var(--text-secondary);
- margin: 0;
- line-height: 1.5;
-}
-
-.bookmark-meta {
- display: flex;
- align-items: center;
- gap: 12px;
- margin-top: 12px;
- font-size: 0.85rem;
- color: var(--text-tertiary);
-}
-
-.bookmark-time {
- color: var(--text-tertiary);
-}
-
-.composer {
- margin-bottom: 24px;
-}
-
-.composer-textarea {
- width: 100%;
- min-height: 120px;
- padding: 16px;
- background: var(--bg-secondary);
- border: 1px solid var(--border);
- border-radius: var(--radius-md);
- color: var(--text-primary);
- font-size: 1rem;
- resize: vertical;
- transition: all 0.15s ease;
-}
-
-.composer-textarea:focus {
- outline: none;
- border-color: var(--accent);
- box-shadow: 0 0 0 3px var(--accent-subtle);
-}
-
-.composer-footer {
- display: flex;
- justify-content: space-between;
- align-items: center;
- margin-top: 12px;
-}
-
-.composer-char-count {
- font-size: 0.85rem;
- color: var(--text-tertiary);
-}
-
-.composer-char-count.warning {
- color: var(--warning);
-}
-
-.composer-char-count.error {
- color: var(--error);
-}
-
-.composer-add-quote {
- width: 100%;
- padding: 12px 16px;
- margin-bottom: 12px;
- background: var(--bg-tertiary);
- border: 1px dashed var(--border);
- border-radius: var(--radius-md);
- color: var(--text-secondary);
- font-size: 0.9rem;
- cursor: pointer;
- transition: all 0.15s ease;
-}
-
-.composer-add-quote:hover {
- border-color: var(--accent);
- color: var(--accent);
- background: var(--accent-subtle);
-}
-
-.composer-quote-input-wrapper {
- margin-bottom: 12px;
-}
-
-.composer-quote-input {
- width: 100%;
- padding: 12px 16px;
- background: linear-gradient(
- 135deg,
- rgba(79, 70, 229, 0.05),
- rgba(168, 85, 247, 0.05)
- );
- border: 1px solid var(--border);
- border-left: 3px solid var(--accent);
- border-radius: 0 var(--radius-md) var(--radius-md) 0;
- color: var(--text-primary);
- font-size: 0.95rem;
- font-style: italic;
- resize: vertical;
- font-family: inherit;
- transition: all 0.15s ease;
-}
-
-.composer-quote-input:focus {
- outline: none;
- border-color: var(--accent);
-}
-
-.composer-quote-input::placeholder {
- color: var(--text-tertiary);
- font-style: italic;
-}
-
-.composer-quote-remove-btn {
- margin-top: 8px;
- padding: 6px 12px;
- background: none;
- border: none;
- color: var(--text-tertiary);
- font-size: 0.85rem;
- cursor: pointer;
-}
-
-.composer-quote-remove-btn:hover {
- color: var(--error);
-}
-
-@keyframes shimmer {
- 0% {
- background-position: -200% 0;
- }
-
- 100% {
- background-position: 200% 0;
- }
-}
-
-.skeleton {
- background: linear-gradient(
- 90deg,
- var(--bg-tertiary) 25%,
- var(--bg-hover) 50%,
- var(--bg-tertiary) 75%
- );
- background-size: 200% 100%;
- animation: shimmer 1.5s infinite;
- border-radius: var(--radius-sm);
-}
-
-.skeleton-text {
- height: 1em;
- margin-bottom: 8px;
-}
-
-.skeleton-text:last-child {
- width: 60%;
-}
-
-@media (max-width: 640px) {
- .main-content {
- padding: 16px 12px;
- }
-
- .navbar-inner {
- padding: 0 16px;
- }
-
- .page-title {
- font-size: 1.5rem;
- }
-
- .url-input-container {
- flex-direction: column;
- }
-
- .profile-header {
- flex-direction: column;
- text-align: center;
- }
-
- .profile-stats {
- justify-content: center;
- }
-}
-
-.main {
- flex: 1;
- width: 100%;
-}
-
-.page-container {
- max-width: 680px;
- margin: 0 auto;
- padding: 24px 16px;
-}
-
-.navbar-logo {
- width: 32px;
- height: 32px;
- background: linear-gradient(135deg, var(--accent), #8b5cf6);
- border-radius: var(--radius-sm);
- display: flex;
- align-items: center;
- justify-content: center;
- font-weight: 700;
- font-size: 1rem;
- color: white;
-}
-
-.navbar-user {
- display: flex;
- align-items: center;
- gap: 8px;
-}
-
-.navbar-avatar {
- width: 36px;
- height: 36px;
- border-radius: var(--radius-full);
- background: linear-gradient(135deg, var(--accent), #a855f7);
- display: flex;
- align-items: center;
- justify-content: center;
- font-weight: 600;
- font-size: 0.85rem;
- color: white;
- text-decoration: none;
-}
-
-.btn-sm {
- padding: 6px 12px;
- font-size: 0.85rem;
-}
-
-.composer-url {
- font-size: 0.85rem;
- color: var(--text-secondary);
- word-break: break-all;
-}
-
-.composer-quote {
- position: relative;
- padding: 12px 16px;
- padding-right: 36px;
- background: var(--bg-secondary);
- border-left: 3px solid var(--accent);
- border-radius: 0 var(--radius-sm) var(--radius-sm) 0;
- margin-bottom: 16px;
- font-style: italic;
- color: var(--text-secondary);
-}
-
-.composer-quote-remove {
- position: absolute;
- top: 8px;
- right: 8px;
- width: 24px;
- height: 24px;
- border-radius: var(--radius-full);
- background: var(--bg-tertiary);
- color: var(--text-secondary);
- font-size: 1rem;
- display: flex;
- align-items: center;
- justify-content: center;
-}
-
-.composer-quote-remove:hover {
- background: var(--bg-hover);
- color: var(--text-primary);
-}
-
-.composer-input {
- width: 100%;
- min-height: 120px;
- padding: 16px;
- background: var(--bg-secondary);
- border: 1px solid var(--border);
- border-radius: var(--radius-md);
- color: var(--text-primary);
- font-size: 1rem;
- resize: vertical;
- transition: all 0.15s ease;
-}
-
-.composer-input:focus {
- outline: none;
- border-color: var(--accent);
- box-shadow: 0 0 0 3px var(--accent-subtle);
-}
-
-.composer-input::placeholder {
- color: var(--text-tertiary);
-}
-
-.composer-tags {
- margin-top: 12px;
-}
-
-.composer-tags-input {
- width: 100%;
- padding: 12px 16px;
- background: var(--bg-secondary);
- border: 1px solid var(--border);
- border-radius: var(--radius-md);
- color: var(--text-primary);
- font-size: 0.95rem;
- transition: all 0.15s ease;
-}
-
-.composer-tags-input:focus {
- outline: none;
- border-color: var(--accent);
- box-shadow: 0 0 0 3px var(--accent-subtle);
-}
-
-.composer-tags-input::placeholder {
- color: var(--text-tertiary);
-}
-
-.composer-footer {
- display: flex;
- justify-content: space-between;
- align-items: center;
- margin-top: 12px;
-}
-
-.composer-count {
- font-size: 0.85rem;
- color: var(--text-tertiary);
-}
-
-.composer-actions {
- display: flex;
- gap: 8px;
-}
-
-.composer-error {
- margin-top: 12px;
- padding: 12px;
- background: rgba(239, 68, 68, 0.1);
- border: 1px solid rgba(239, 68, 68, 0.3);
- border-radius: var(--radius-md);
- color: var(--error);
- font-size: 0.9rem;
-}
-
-.annotation-tags {
- display: flex;
- flex-wrap: wrap;
- gap: 6px;
- margin-top: 12px;
- margin-bottom: 8px;
-}
-
-.annotation-tag {
- display: inline-flex;
- align-items: center;
- padding: 4px 10px;
- background: var(--bg-tertiary);
- color: var(--text-secondary);
- font-size: 0.8rem;
- font-weight: 500;
- border-radius: var(--radius-full);
- transition: all 0.15s ease;
- border: 1px solid transparent;
- text-decoration: none;
-}
-
-.annotation-tag:hover {
- background: var(--bg-hover);
- color: var(--text-primary);
- border-color: var(--border);
- transform: translateY(-1px);
-}
-
-.url-input-wrapper {
- margin-bottom: 24px;
-}
-
-.url-input {
- width: 100%;
- padding: 16px;
- background: var(--bg-secondary);
- border: 1px solid var(--border);
- border-radius: var(--radius-md);
- color: var(--text-primary);
- font-size: 1.1rem;
- transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
- box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
-}
-
-.url-input:focus {
- outline: none;
- border-color: var(--accent);
- box-shadow: 0 0 0 4px var(--accent-subtle);
- background: var(--bg-primary);
-}
-
-.url-input::placeholder {
- color: var(--text-tertiary);
-}
-
-.annotation-detail-page {
- max-width: 680px;
- margin: 0 auto;
- padding: 24px 16px;
-}
-
-.annotation-detail-header {
- margin-bottom: 24px;
-}
-
-.back-link {
- color: var(--text-secondary);
- text-decoration: none;
- font-size: 0.9rem;
-}
-
-.back-link:hover {
- color: var(--accent);
-}
-
-.replies-section {
- margin-top: 32px;
-}
-
-.replies-title {
- font-size: 1.1rem;
- font-weight: 600;
- margin-bottom: 16px;
- color: var(--text-primary);
-}
-
-.reply-form {
- margin-bottom: 24px;
-}
-
-.reply-input {
- width: 100%;
- padding: 12px;
- border: 1px solid var(--border);
- border-radius: var(--radius-md);
- font-size: 0.95rem;
- resize: vertical;
- margin-bottom: 12px;
- font-family: inherit;
-}
-
-.reply-input:focus {
- outline: none;
- border-color: var(--accent);
- box-shadow: 0 0 0 3px var(--accent-subtle);
-}
-
-.replies-list {
- display: flex;
- flex-direction: column;
- gap: 12px;
-}
-
-.reply-card {
- padding: 16px;
- background: var(--bg-secondary);
- border-radius: var(--radius-md);
- border: 1px solid var(--border);
-}
-
-.reply-header {
- display: flex;
- align-items: center;
- gap: 12px;
- margin-bottom: 12px;
-}
-
-.reply-avatar-link {
- text-decoration: none;
-}
-
-.reply-avatar {
- width: 36px;
- height: 36px;
- min-width: 36px;
- border-radius: var(--radius-full);
- background: linear-gradient(135deg, var(--accent), #a855f7);
- display: flex;
- align-items: center;
- justify-content: center;
- font-weight: 600;
- font-size: 0.85rem;
- color: white;
- overflow: hidden;
-}
-
-.reply-avatar img {
- width: 100%;
- height: 100%;
- object-fit: cover;
-}
-
-.reply-meta {
- flex: 1;
- min-width: 0;
-}
-
-.reply-author {
- font-weight: 600;
- color: var(--text-primary);
-}
-
-.reply-handle {
- font-size: 0.85rem;
- color: var(--text-tertiary);
- text-decoration: none;
- margin-left: 6px;
-}
-
-.reply-handle:hover {
- color: var(--accent);
- text-decoration: underline;
-}
-
-.reply-time {
- font-size: 0.85rem;
- color: var(--text-tertiary);
- white-space: nowrap;
-}
-
-.reply-text {
- color: var(--text-primary);
- line-height: 1.5;
- margin: 0;
-}
-
-.replies-title {
- display: flex;
- align-items: center;
- gap: 8px;
-}
-
-.replies-title svg {
- color: var(--accent);
-}
-
-.replies-list-threaded {
- display: flex;
- flex-direction: column;
- gap: 8px;
-}
-
-.reply-card-threaded {
- padding: 16px;
- transition: background 0.15s ease;
-}
-
-.reply-card-threaded .reply-header {
- margin-bottom: 8px;
-}
-
-.reply-card-threaded .reply-meta {
- display: flex;
- align-items: center;
- gap: 6px;
- flex-wrap: wrap;
-}
-
-.reply-dot {
- color: var(--text-tertiary);
- font-size: 0.75rem;
-}
-
-.reply-actions {
- display: flex;
- gap: 4px;
- margin-left: auto;
-}
-
-.reply-action-btn {
- background: none;
- border: none;
- padding: 4px 8px;
- color: var(--text-tertiary);
- cursor: pointer;
- border-radius: var(--radius-sm);
- transition: all 0.15s ease;
- display: flex;
- align-items: center;
- justify-content: center;
-}
-
-.reply-action-btn:hover {
- color: var(--accent);
- background: var(--accent-subtle);
-}
-
-.reply-action-delete:hover {
- color: var(--error);
- background: rgba(239, 68, 68, 0.1);
-}
-
-.replying-to-banner {
- display: flex;
- align-items: center;
- justify-content: space-between;
- padding: 8px 12px;
- margin-bottom: 12px;
- background: var(--accent-subtle);
- border-radius: var(--radius-sm);
- font-size: 0.85rem;
- color: var(--text-secondary);
-}
-
-.cancel-reply {
- background: none;
- border: none;
- font-size: 1.2rem;
- color: var(--text-tertiary);
- cursor: pointer;
- padding: 0 4px;
- line-height: 1;
-}
-
-.cancel-reply:hover {
- color: var(--text-primary);
-}
-
-.reply-form.card {
- padding: 16px;
- margin-bottom: 16px;
-}
-
-.reply-form-actions {
- display: flex;
- justify-content: flex-end;
-}
-
-.inline-replies {
- margin-top: 16px;
- padding-top: 16px;
- border-top: 1px solid var(--border);
- display: flex;
- flex-direction: column;
- gap: 16px;
-}
-
-.main-reply-composer {
- margin-top: 16px;
- background: var(--bg-secondary);
- padding: 12px;
- border-radius: var(--radius-md);
-}
-
-.reply-input {
- width: 100%;
- min-height: 80px;
- padding: 12px;
- border: 1px solid var(--border);
- border-radius: var(--radius-md);
- background: var(--bg-card);
- color: var(--text-primary);
- font-family: inherit;
- font-size: 0.95rem;
- resize: vertical;
- display: block;
-}
-
-.reply-input:focus {
- border-color: var(--accent);
- outline: none;
-}
-
-.reply-input.small {
- min-height: 60px;
- font-size: 0.9rem;
- margin-bottom: 8px;
-}
-
-.composer-actions {
- display: flex;
- justify-content: flex-end;
-}
-
-.btn-block {
- width: 100%;
- text-align: left;
- padding: 8px 12px;
- color: var(--text-secondary);
- background: var(--bg-tertiary);
- border-radius: var(--radius-md);
- margin-top: 8px;
- font-size: 0.9rem;
- cursor: pointer;
- transition: all 0.2s;
-}
-
-.btn-block:hover {
- background: var(--border);
- color: var(--text-primary);
-}
-
-.annotation-action.active {
- color: var(--accent);
-}
-
-.new-page {
- max-width: 600px;
- margin: 0 auto;
- display: flex;
- flex-direction: column;
- gap: 32px;
-}
-
-.loading-spinner {
- width: 32px;
- height: 32px;
- border: 3px solid var(--border);
- border-top-color: var(--accent);
- border-radius: 50%;
- animation: spin 0.8s linear infinite;
- margin: 60px auto;
-}
-
-@keyframes spin {
- to {
- transform: rotate(360deg);
- }
-}
-
-.navbar {
- position: sticky;
- top: 0;
- z-index: 1000;
- background: rgba(12, 10, 20, 0.95);
- backdrop-filter: blur(12px);
- -webkit-backdrop-filter: blur(12px);
- border-bottom: 1px solid var(--border);
-}
-
-.navbar-inner {
- max-width: 1200px;
- margin: 0 auto;
- padding: 12px 24px;
- display: flex;
- align-items: center;
- justify-content: space-between;
- gap: 24px;
-}
-
-.navbar-brand {
- display: flex;
- align-items: center;
- gap: 10px;
- text-decoration: none;
- flex-shrink: 0;
-}
-
-.navbar-logo {
- width: 32px;
- height: 32px;
- background: linear-gradient(135deg, var(--accent), #8b5cf6);
- border-radius: 8px;
- display: flex;
- align-items: center;
- justify-content: center;
- font-weight: 700;
- font-size: 1rem;
- color: white;
-}
-
-.navbar-title {
- font-weight: 700;
- font-size: 1.25rem;
- color: var(--text-primary);
-}
-
-.navbar-center {
- display: flex;
- align-items: center;
- gap: 8px;
- background: var(--bg-tertiary);
- padding: 4px;
- border-radius: var(--radius-lg);
-}
-
-.navbar-link {
- display: flex;
- align-items: center;
- gap: 6px;
- padding: 8px 16px;
- font-size: 0.9rem;
- font-weight: 500;
- color: var(--text-secondary);
- text-decoration: none;
- border-radius: var(--radius-md);
- transition: all 0.15s ease;
-}
-
-.navbar-link:hover {
- color: var(--text-primary);
- background: var(--bg-hover);
-}
-
-.navbar-link.active {
- color: var(--text-primary);
- background: var(--bg-card);
- box-shadow: var(--shadow-sm);
-}
-
-.navbar-right {
- display: flex;
- align-items: center;
- gap: 12px;
- flex-shrink: 0;
-}
-
-.navbar-icon-link {
- display: flex;
- align-items: center;
- justify-content: center;
- width: 36px;
- height: 36px;
- color: var(--text-tertiary);
- border-radius: var(--radius-md);
- transition: all 0.15s ease;
-}
-
-.navbar-icon-link:hover {
- color: var(--text-primary);
- background: var(--bg-tertiary);
-}
-
-.navbar-icon-link.active {
- color: var(--accent);
- background: var(--accent-subtle);
-}
-
-.navbar-new-btn {
- display: flex;
- align-items: center;
- gap: 6px;
- padding: 8px 14px;
- background: linear-gradient(135deg, var(--accent), #8b5cf6);
- color: white;
- font-size: 0.85rem;
- font-weight: 600;
- text-decoration: none;
- border-radius: var(--radius-full);
- transition: all 0.2s ease;
-}
-
-.navbar-new-btn:hover {
- transform: translateY(-1px);
- box-shadow: 0 4px 12px rgba(79, 70, 229, 0.3);
- color: white;
-}
-
-.navbar-user-section {
- display: flex;
- align-items: center;
- gap: 4px;
-}
-
-.navbar-avatar {
- width: 32px;
- height: 32px;
- border-radius: var(--radius-full);
- background: linear-gradient(135deg, var(--accent), #a855f7);
- display: flex;
- align-items: center;
- justify-content: center;
- font-weight: 600;
- font-size: 0.75rem;
- color: white;
- text-decoration: none;
- transition: transform 0.15s ease;
-}
-
-.navbar-avatar:hover {
- transform: scale(1.05);
-}
-
-.navbar-logout {
- width: 24px;
- height: 24px;
- border: none;
- background: transparent;
- color: var(--text-tertiary);
- font-size: 1.25rem;
- cursor: pointer;
- border-radius: var(--radius-sm);
- transition: all 0.15s ease;
- display: flex;
- align-items: center;
- justify-content: center;
-}
-
-.navbar-logout:hover {
- color: var(--error);
- background: rgba(239, 68, 68, 0.1);
-}
-
-.navbar-signin {
- padding: 8px 16px;
- background: var(--accent);
- color: white;
- font-size: 0.9rem;
- font-weight: 500;
- text-decoration: none;
- border-radius: var(--radius-full);
- transition: all 0.15s ease;
-}
-
-.navbar-signin:hover {
- background: var(--accent-hover);
- color: white;
-}
-
-.navbar-user-menu {
- position: relative;
-}
-
-.navbar-avatar-btn {
- width: 36px;
- height: 36px;
- border-radius: var(--radius-full);
- background: linear-gradient(135deg, var(--accent), #a855f7);
- border: none;
- cursor: pointer;
- overflow: hidden;
- display: flex;
- align-items: center;
- justify-content: center;
- transition:
- transform 0.15s ease,
- box-shadow 0.15s ease;
-}
-
-.navbar-avatar-btn:hover {
- transform: scale(1.05);
- box-shadow: 0 4px 12px rgba(79, 70, 229, 0.3);
-}
-
-.navbar-avatar-img {
- width: 100%;
- height: 100%;
- object-fit: cover;
-}
-
-.navbar-avatar-text {
- font-weight: 600;
- font-size: 0.75rem;
- color: white;
-}
-
-.navbar-dropdown {
- position: absolute;
- top: calc(100% + 8px);
- right: 0;
- min-width: 200px;
- background: var(--bg-card);
- border: 1px solid var(--border);
- border-radius: var(--radius-lg);
- box-shadow: var(--shadow-lg);
- overflow: hidden;
- z-index: 1001;
- animation: dropdownFade 0.15s ease;
-}
-
-@keyframes dropdownFade {
- from {
- opacity: 0;
- transform: translateY(-8px);
- }
-
- to {
- opacity: 1;
- transform: translateY(0);
- }
-}
-
-.navbar-dropdown-header {
- padding: 12px 16px;
- background: var(--bg-secondary);
-}
-
-.navbar-dropdown-name {
- display: block;
- font-weight: 600;
- color: var(--text-primary);
- font-size: 0.9rem;
-}
-
-.navbar-dropdown-handle {
- display: block;
- color: var(--text-tertiary);
- font-size: 0.8rem;
- margin-top: 2px;
-}
-
-.navbar-dropdown-divider {
- height: 1px;
- background: var(--border);
-}
-
-.navbar-dropdown-item {
- display: flex;
- align-items: center;
- gap: 10px;
- width: 100%;
- padding: 12px 16px;
- font-size: 0.9rem;
- color: var(--text-primary);
- text-decoration: none;
- background: none;
- border: none;
- cursor: pointer;
- transition: background 0.15s ease;
- text-align: left;
-}
-
-.navbar-dropdown-item:hover {
- background: var(--bg-tertiary);
-}
-
-.navbar-dropdown-logout {
- color: var(--error);
- border-top: 1px solid var(--border);
-}
-
-.navbar-dropdown-logout:hover {
- background: rgba(239, 68, 68, 0.1);
-}
-
-@media (max-width: 768px) {
- .navbar-inner {
- padding: 10px 16px;
- }
-
- .navbar-title {
- display: none;
- }
-
- .navbar-center {
- display: none;
- }
-
- .navbar-new-btn span {
- display: none;
- }
-
- .navbar-new-btn {
- width: 36px;
- height: 36px;
- padding: 0;
- justify-content: center;
- }
-}
-
-.collections-list {
- display: flex;
- flex-direction: column;
- gap: 2px;
- background: var(--bg-card);
- border: 1px solid var(--border);
- border-radius: var(--radius-lg);
- overflow: hidden;
-}
-
-.collection-row {
- display: flex;
- align-items: center;
- background: var(--bg-card);
- transition: background 0.15s ease;
-}
-
-.collection-row:not(:last-child) {
- border-bottom: 1px solid var(--border);
-}
-
-.collection-row:hover {
- background: var(--bg-secondary);
-}
-
-.collection-row-content {
- flex: 1;
- display: flex;
- align-items: center;
- gap: 16px;
- padding: 16px 20px;
- text-decoration: none;
- min-width: 0;
-}
-
-.collection-row-icon {
- width: 44px;
- height: 44px;
- min-width: 44px;
- display: flex;
- align-items: center;
- justify-content: center;
- background: linear-gradient(
- 135deg,
- rgba(79, 70, 229, 0.1),
- rgba(168, 85, 247, 0.15)
- );
- color: var(--accent);
- border-radius: var(--radius-md);
- transition: all 0.2s ease;
-}
-
-.collection-row:hover .collection-row-icon {
- background: linear-gradient(
- 135deg,
- rgba(79, 70, 229, 0.15),
- rgba(168, 85, 247, 0.2)
- );
- transform: scale(1.05);
-}
-
-.collection-row-info {
- flex: 1;
- min-width: 0;
-}
-
-.collection-row-name {
- font-size: 1rem;
- font-weight: 600;
- color: var(--text-primary);
- margin: 0 0 2px 0;
- white-space: nowrap;
- overflow: hidden;
- text-overflow: ellipsis;
-}
-
-.collection-row:hover .collection-row-name {
- color: var(--accent);
-}
-
-.collection-row-desc {
- font-size: 0.85rem;
- color: var(--text-secondary);
- margin: 0;
- white-space: nowrap;
- overflow: hidden;
- text-overflow: ellipsis;
-}
-
-.collection-row-arrow {
- color: var(--text-tertiary);
- opacity: 0;
- transition: all 0.2s ease;
-}
-
-.collection-row:hover .collection-row-arrow {
- opacity: 1;
- color: var(--accent);
- transform: translateX(2px);
-}
-
-.collection-row-edit {
- padding: 10px;
- margin-right: 12px;
- color: var(--text-tertiary);
- background: none;
- border: none;
- border-radius: var(--radius-sm);
- cursor: pointer;
- opacity: 0;
- transition: all 0.15s ease;
-}
-
-.collection-row:hover .collection-row-edit {
- opacity: 1;
-}
-
-.collection-row-edit:hover {
- color: var(--text-primary);
- background: var(--bg-tertiary);
-}
-
-.back-link {
- display: inline-flex;
- align-items: center;
- gap: 6px;
- color: var(--text-tertiary);
- font-size: 0.9rem;
- font-weight: 500;
- text-decoration: none;
- margin-bottom: 24px;
- transition: color 0.15s ease;
-}
-
-.back-link:hover {
- color: var(--accent);
-}
-
-.collection-detail-header {
- display: flex;
- gap: 20px;
- padding: 24px;
- background: var(--bg-card);
- border: 1px solid var(--border);
- border-radius: var(--radius-lg);
- margin-bottom: 32px;
- position: relative;
-}
-
-.collection-detail-icon {
- width: 56px;
- height: 56px;
- min-width: 56px;
- display: flex;
- align-items: center;
- justify-content: center;
- background: linear-gradient(
- 135deg,
- rgba(79, 70, 229, 0.1),
- rgba(168, 85, 247, 0.1)
- );
- color: var(--accent);
- border-radius: var(--radius-md);
-}
-
-.collection-detail-info {
- flex: 1;
- min-width: 0;
-}
-
-.collection-detail-visibility {
- display: flex;
- align-items: center;
- gap: 6px;
- font-size: 0.8rem;
- font-weight: 600;
- color: var(--accent);
- text-transform: capitalize;
- margin-bottom: 8px;
-}
-
-.collection-detail-title {
- font-size: 1.5rem;
- font-weight: 700;
- color: var(--text-primary);
- margin-bottom: 8px;
- line-height: 1.3;
-}
-
-.collection-detail-desc {
- color: var(--text-secondary);
- font-size: 1rem;
- line-height: 1.5;
- margin-bottom: 12px;
- max-width: 600px;
-}
-
-.collection-detail-stats {
- display: flex;
- align-items: center;
- gap: 8px;
- font-size: 0.85rem;
- color: var(--text-tertiary);
-}
-
-.collection-detail-actions {
- position: absolute;
- top: 20px;
- right: 20px;
- display: flex;
- align-items: center;
- gap: 8px;
-}
-
-.collection-detail-actions .share-menu-container {
- display: flex;
- align-items: center;
-}
-
-.collection-detail-actions .annotation-action {
- padding: 10px;
- color: var(--text-tertiary);
- background: none;
- border: none;
- border-radius: var(--radius-sm);
- cursor: pointer;
- transition: all 0.15s ease;
-}
-
-.collection-detail-actions .annotation-action:hover {
- color: var(--accent);
- background: var(--bg-tertiary);
-}
-
-.collection-detail-edit,
-.collection-detail-delete {
- padding: 10px;
- color: var(--text-tertiary);
- background: none;
- border: none;
- border-radius: var(--radius-sm);
- cursor: pointer;
- transition: all 0.15s ease;
-}
-
-.collection-detail-edit:hover {
- color: var(--accent);
- background: var(--bg-tertiary);
-}
-
-.collection-detail-delete:hover {
- color: var(--error);
- background: rgba(239, 68, 68, 0.1);
-}
-
-.collection-item-wrapper {
- position: relative;
-}
-
-.collection-item-remove {
- position: absolute;
- top: 12px;
- left: -40px;
- z-index: 10;
- padding: 8px;
- background: var(--bg-card);
- border: 1px solid var(--border);
- border-radius: var(--radius-sm);
- color: var(--text-tertiary);
- cursor: pointer;
- opacity: 0;
- transition: all 0.15s ease;
-}
-
-.collection-item-wrapper:hover .collection-item-remove {
- opacity: 1;
-}
-
-.collection-item-remove:hover {
- color: var(--error);
- border-color: var(--error);
- background: rgba(239, 68, 68, 0.05);
-}
-
-.modal-overlay {
- position: fixed;
- inset: 0;
- background: rgba(0, 0, 0, 0.5);
- display: flex;
- align-items: center;
- justify-content: center;
- padding: 16px;
- z-index: 50;
- animation: fadeIn 0.2s ease-out;
-}
-
-.modal-container {
- background: var(--bg-secondary);
- border-radius: var(--radius-lg);
- width: 100%;
- max-width: 28rem;
- border: 1px solid var(--border);
- box-shadow: var(--shadow-lg);
- animation: zoomIn 0.2s ease-out;
-}
-
-.modal-header {
- display: flex;
- align-items: center;
- justify-content: space-between;
- padding: 16px;
- border-bottom: 1px solid var(--border);
-}
-
-.modal-title {
- font-size: 1.25rem;
- font-weight: 700;
- color: var(--text-primary);
-}
-
-.modal-close-btn {
- padding: 8px;
- color: var(--text-tertiary);
- border-radius: var(--radius-md);
- transition: color 0.15s;
-}
-
-.modal-close-btn:hover {
- color: var(--text-primary);
- background: var(--bg-hover);
-}
-
-.modal-form {
- padding: 16px;
- display: flex;
- flex-direction: column;
- gap: 16px;
-}
-
-.icon-picker-tabs {
- display: flex;
- gap: 4px;
- margin-bottom: 12px;
-}
-
-.icon-picker-tab {
- flex: 1;
- padding: 8px 12px;
- background: var(--bg-primary);
- border: 1px solid var(--border);
- border-radius: var(--radius-md);
- color: var(--text-secondary);
- font-size: 0.85rem;
- font-weight: 500;
- cursor: pointer;
- transition: all 0.15s ease;
-}
-
-.icon-picker-tab:hover {
- background: var(--bg-tertiary);
-}
-
-.icon-picker-tab.active {
- background: var(--accent);
- border-color: var(--accent);
- color: white;
-}
-
-.emoji-picker-wrapper {
- display: flex;
- flex-direction: column;
- gap: 10px;
-}
-
-.emoji-custom-input input {
- width: 100%;
-}
-
-.emoji-picker,
-.icon-picker {
- display: flex;
- flex-wrap: wrap;
- gap: 4px;
- max-height: 120px;
- overflow-y: auto;
- padding: 8px;
- background: var(--bg-primary);
- border: 1px solid var(--border);
- border-radius: var(--radius-md);
-}
-
-.emoji-option,
-.icon-option {
- width: 36px;
- height: 36px;
- display: flex;
- align-items: center;
- justify-content: center;
- font-size: 1.2rem;
- background: transparent;
- border: 2px solid transparent;
- border-radius: var(--radius-sm);
- cursor: pointer;
- transition: all 0.15s ease;
- color: var(--text-secondary);
-}
-
-.emoji-option:hover,
-.icon-option:hover {
- background: var(--bg-tertiary);
- transform: scale(1.1);
- color: var(--text-primary);
-}
-
-.emoji-option.selected,
-.icon-option.selected {
- border-color: var(--accent);
- background: var(--accent-subtle);
- color: var(--accent);
-}
-
-.form-group {
- margin-bottom: 0;
-}
-
-.form-label {
- display: block;
- font-size: 0.875rem;
- font-weight: 500;
- color: var(--text-secondary);
- margin-bottom: 4px;
-}
-
-.form-input,
-.form-textarea,
-.form-select {
- width: 100%;
- padding: 8px 12px;
- background: var(--bg-primary);
- border: 1px solid var(--border);
- border-radius: var(--radius-md);
- color: var(--text-primary);
- transition: all 0.15s;
-}
-
-.form-input:focus,
-.form-textarea:focus,
-.form-select:focus {
- outline: none;
- border-color: var(--accent);
- box-shadow: 0 0 0 2px var(--accent-subtle);
-}
-
-.form-textarea {
- resize: none;
-}
-
-.modal-actions {
- display: flex;
- justify-content: flex-end;
- gap: 12px;
- padding-top: 8px;
-}
-
-@keyframes fadeIn {
- from {
- opacity: 0;
- }
-
- to {
- opacity: 1;
- }
-}
-
-@keyframes zoomIn {
- from {
- opacity: 0;
- transform: scale(0.95);
- }
-
- to {
- opacity: 1;
- transform: scale(1);
- }
-}
-
-.annotation-detail-page {
- max-width: 680px;
- margin: 0 auto;
- padding: 24px 16px;
-}
-
-.annotation-detail-header {
- margin-bottom: 24px;
-}
-
-.back-link {
- display: inline-flex;
- align-items: center;
- gap: 8px;
- color: var(--text-secondary);
- font-size: 0.9rem;
- transition: color 0.15s;
-}
-
-.back-link:hover {
- color: var(--text-primary);
-}
-
-.text-secondary {
- color: var(--text-secondary);
-}
-
-.text-error {
- color: var(--error);
-}
-
-.text-center {
- text-align: center;
-}
-
-.flex {
- display: flex;
-}
-
-.items-center {
- align-items: center;
-}
-
-.justify-center {
- justify-content: center;
-}
-
-.justify-end {
- justify-content: flex-end;
-}
-
-.gap-2 {
- gap: 8px;
-}
-
-.gap-3 {
- gap: 12px;
-}
-
-.mt-3 {
- margin-top: 12px;
-}
-
-.mb-6 {
- margin-bottom: 24px;
-}
-
-.btn-text {
- background: none;
- border: none;
- color: var(--text-secondary);
- font-size: 0.9rem;
- padding: 8px 12px;
- cursor: pointer;
- transition: color 0.15s;
-}
-
-.btn-text:hover {
- color: var(--text-primary);
-}
-
-.btn-sm {
- padding: 6px 12px;
- font-size: 0.85rem;
-}
-
-.annotation-edit-btn {
- background: none;
- border: none;
- cursor: pointer;
- padding: 6px 8px;
- color: var(--text-tertiary);
- border-radius: var(--radius-sm);
- transition: all 0.15s ease;
-}
-
-.annotation-edit-btn:hover {
- color: var(--accent);
- background: var(--accent-subtle);
-}
-
-.spinner {
- width: 32px;
- height: 32px;
- border: 3px solid var(--border);
- border-top-color: var(--accent);
- border-radius: 50%;
- animation: spin 0.8s linear infinite;
-}
-
-.spinner-sm {
- width: 16px;
- height: 16px;
- border-width: 2px;
-}
-
-@keyframes spin {
- to {
- transform: rotate(360deg);
- }
-}
-
-.collection-list-item {
- width: 100%;
- text-align: left;
- padding: 12px 16px;
- border-radius: var(--radius-md);
- background: var(--bg-primary);
- border: 1px solid transparent;
- color: var(--text-primary);
- transition: all 0.15s ease;
- display: flex;
- align-items: center;
- justify-content: space-between;
- cursor: pointer;
-}
-
-.collection-list-item:hover {
- background: var(--bg-hover);
- border-color: var(--border);
-}
-
-.collection-list-item:hover .collection-list-item-icon {
- opacity: 1;
-}
-
-.collection-list-item:disabled {
- opacity: 0.6;
- cursor: not-allowed;
-}
-
-.item-delete-overlay {
- position: absolute;
- top: 16px;
- right: 16px;
- z-index: 10;
- opacity: 0;
- transition: opacity 0.15s ease;
-}
-
-.card:hover .item-delete-overlay,
-div:hover > .item-delete-overlay {
- opacity: 1;
-}
-
-.btn-icon-danger {
- padding: 8px;
- background: var(--error);
- color: white;
- border: none;
- border-radius: var(--radius-md);
- cursor: pointer;
- box-shadow: var(--shadow-md);
- transition: all 0.15s ease;
- display: flex;
- align-items: center;
- justify-content: center;
-}
-
-.btn-icon-danger:hover {
- background: #dc2626;
- transform: scale(1.05);
-}
-
-.action-buttons {
- display: flex;
- gap: 8px;
-}
-
-.action-buttons-end {
- display: flex;
- justify-content: flex-end;
- gap: 8px;
-}
-
-.filter-tab {
- padding: 8px 16px;
- font-size: 0.9rem;
- font-weight: 500;
- color: var(--text-secondary);
- background: transparent;
- border: none;
- border-radius: var(--radius-md);
- cursor: pointer;
- transition: all 0.15s ease;
-}
-
-.filter-tab:hover {
- color: var(--text-primary);
- background: var(--bg-hover);
-}
-
-.filter-tab.active {
- color: var(--text-primary);
- background: var(--bg-card);
- box-shadow: var(--shadow-sm);
-}
-
-.inline-reply {
- padding: 12px 16px;
- border-bottom: 1px solid var(--border);
-}
-
-.inline-reply:last-child {
- border-bottom: none;
-}
-
-.inline-reply-avatar {
- width: 28px;
- height: 28px;
- min-width: 28px;
- border-radius: var(--radius-full);
- background: linear-gradient(135deg, var(--accent), #a855f7);
- display: flex;
- align-items: center;
- justify-content: center;
- font-weight: 600;
- font-size: 0.7rem;
- color: white;
- overflow: hidden;
-}
-
-.inline-reply-avatar img,
-.inline-reply-avatar-placeholder {
- width: 100%;
- height: 100%;
- object-fit: cover;
-}
-
-.inline-reply-avatar-placeholder {
- display: flex;
- align-items: center;
- justify-content: center;
- font-weight: 600;
- font-size: 0.7rem;
- color: white;
-}
-
-.inline-reply-content {
- flex: 1;
- min-width: 0;
-}
-
-.inline-reply-header {
- display: flex;
- align-items: center;
- gap: 8px;
- margin-bottom: 4px;
-}
-
-.inline-reply-author {
- font-weight: 600;
- font-size: 0.85rem;
- color: var(--text-primary);
-}
-
-.inline-reply-handle {
- color: var(--text-tertiary);
- font-size: 0.8rem;
- text-decoration: none;
-}
-
-.inline-reply-time {
- color: var(--text-tertiary);
- font-size: 0.75rem;
- margin-left: auto;
-}
-
-.inline-reply-text {
- font-size: 0.9rem;
- color: var(--text-primary);
- line-height: 1.5;
-}
-
-.inline-reply-action {
- display: flex;
- align-items: center;
- gap: 4px;
- padding: 4px 8px;
- font-size: 0.8rem;
- color: var(--text-tertiary);
- background: none;
- border: none;
- border-radius: var(--radius-sm);
- cursor: pointer;
- transition: all 0.15s ease;
-}
-
-.inline-reply-action:hover {
- color: var(--text-secondary);
- background: var(--bg-hover);
-}
-
-.inline-reply-composer {
- display: flex;
- align-items: flex-start;
- gap: 12px;
- padding: 12px 16px;
-}
-
-.history-panel {
- background: var(--bg-tertiary);
- border: 1px solid var(--border);
- border-radius: var(--radius-md);
- padding: 1rem;
- margin-bottom: 1rem;
- font-size: 0.9rem;
- animation: fadeIn 0.2s ease-out;
-}
-
-.history-header {
- display: flex;
- justify-content: space-between;
- align-items: center;
- margin-bottom: 1rem;
- padding-bottom: 0.5rem;
- border-bottom: 1px solid var(--border);
-}
-
-.history-title {
- font-weight: 600;
- text-transform: uppercase;
- letter-spacing: 0.05em;
- font-size: 0.75rem;
- color: var(--text-secondary);
-}
-
-.history-list {
- list-style: none;
- display: flex;
- flex-direction: column;
- gap: 1rem;
-}
-
-.history-item {
- position: relative;
- padding-left: 1rem;
- border-left: 2px solid var(--border);
-}
-
-.history-date {
- font-size: 0.75rem;
- color: var(--text-tertiary);
- margin-bottom: 0.25rem;
-}
-
-.history-content {
- color: var(--text-secondary);
- white-space: pre-wrap;
-}
-
-.history-close-btn {
- color: var(--text-tertiary);
- padding: 4px;
- border-radius: var(--radius-sm);
- transition: all 0.2s;
- display: flex;
- align-items: center;
- justify-content: center;
-}
-
-.history-close-btn:hover {
- background: var(--bg-hover);
- color: var(--text-primary);
-}
-
-.history-status {
- text-align: center;
- color: var(--text-tertiary);
- font-style: italic;
- padding: 1rem;
-}
-
-.form-label {
- display: block;
- font-size: 0.85rem;
- font-weight: 600;
- color: var(--text-secondary);
- margin-bottom: 6px;
-}
-
-.color-input-container {
- display: flex;
- align-items: center;
- gap: 12px;
- background: var(--bg-tertiary);
- padding: 8px 12px;
- border-radius: var(--radius-md);
- border: 1px solid var(--border);
- width: fit-content;
-}
-
-.color-input-wrapper {
- position: relative;
- width: 32px;
- height: 32px;
- border-radius: var(--radius-full);
- overflow: hidden;
- border: 2px solid var(--border);
- cursor: pointer;
- transition: transform 0.1s;
-}
-
-.color-input-wrapper:hover {
- transform: scale(1.1);
- border-color: var(--accent);
-}
-
-.color-input-wrapper input[type="color"] {
- position: absolute;
- top: -50%;
- left: -50%;
- width: 200%;
- height: 200%;
- padding: 0;
- margin: 0;
- border: none;
- cursor: pointer;
- opacity: 0;
-}
-
-.bookmark-card {
- display: flex;
- flex-direction: column;
- gap: 16px;
-}
-
-.bookmark-preview {
- display: flex;
- flex-direction: column;
- background: var(--bg-secondary);
- border: 1px solid var(--border);
- border-radius: var(--radius-md);
- overflow: hidden;
- text-decoration: none;
- transition: all 0.2s ease;
- position: relative;
-}
-
-.bookmark-preview:hover {
- border-color: var(--accent);
- box-shadow: var(--shadow-sm);
- transform: translateY(-1px);
-}
-
-.bookmark-preview::before {
- content: "";
- position: absolute;
- left: 0;
- top: 0;
- bottom: 0;
- width: 4px;
- background: var(--accent);
- opacity: 0.7;
-}
-
-.bookmark-preview-content {
- padding: 16px 20px;
- display: flex;
- flex-direction: column;
- gap: 8px;
-}
-
-.bookmark-preview-header {
- display: flex;
- align-items: center;
- gap: 8px;
- margin-bottom: 4px;
-}
-
-.bookmark-preview-site {
- font-size: 0.75rem;
- color: var(--accent);
- text-transform: uppercase;
- letter-spacing: 0.05em;
- font-weight: 700;
- display: flex;
- align-items: center;
- gap: 6px;
-}
-
-.bookmark-preview-title {
- font-size: 1.15rem;
- font-weight: 700;
- color: var(--text-primary);
- line-height: 1.4;
-}
-
-.bookmark-preview-desc {
- font-size: 0.95rem;
- color: var(--text-secondary);
- line-height: 1.6;
-}
-
-.bookmark-preview-arrow {
- display: none;
-}
-
-.bookmark-preview:hover {
- background: var(--bg-tertiary);
- border-color: var(--accent-subtle);
- transform: translateY(-1px);
-}
-
-.bookmark-preview-content {
- flex: 1;
- min-width: 0;
- display: flex;
- flex-direction: column;
- gap: 6px;
-}
-
-.bookmark-preview-site {
- display: flex;
- align-items: center;
- gap: 6px;
- font-size: 0.75rem;
- font-weight: 600;
- color: var(--accent);
- text-transform: uppercase;
- letter-spacing: 0.03em;
-}
-
-.bookmark-preview-title {
- font-size: 1rem;
- font-weight: 600;
- line-height: 1.4;
- color: var(--text-primary);
- margin: 0;
- display: -webkit-box;
- -webkit-line-clamp: 2;
- line-clamp: 2;
- -webkit-box-orient: vertical;
- overflow: hidden;
-}
-
-.bookmark-preview-desc {
- font-size: 0.875rem;
- color: var(--text-secondary);
- line-height: 1.5;
- margin: 0;
- display: -webkit-box;
- -webkit-line-clamp: 2;
- line-clamp: 2;
- -webkit-box-orient: vertical;
- overflow: hidden;
-}
-
-.bookmark-preview-arrow {
- display: flex;
- align-items: center;
- justify-content: center;
- color: var(--text-tertiary);
- padding: 0 4px;
- transition: all 0.2s ease;
-}
-
-.bookmark-preview:hover .bookmark-preview-arrow {
- color: var(--accent);
- transform: translateX(2px);
-}
-
-.navbar-logo-img {
- width: 24px;
- height: 24px;
- object-fit: contain;
-}
-
-.login-logo-img {
- width: 80px;
- height: 80px;
- margin-bottom: 24px;
- object-fit: contain;
-}
-
-.legal-content {
- max-width: 800px;
- margin: 0 auto;
- padding: 20px;
-}
-
-.legal-content h1 {
- font-size: 2rem;
- margin-bottom: 8px;
- color: var(--text-primary);
-}
-
-.legal-content h2 {
- font-size: 1.4rem;
- margin-top: 32px;
- margin-bottom: 12px;
- color: var(--text-primary);
-}
-
-.legal-content h3 {
- font-size: 1.1rem;
- margin-top: 20px;
- margin-bottom: 8px;
- color: var(--text-primary);
-}
-
-.legal-content p {
- color: var(--text-secondary);
- line-height: 1.7;
- margin-bottom: 12px;
-}
-
-.legal-content ul {
- color: var(--text-secondary);
- line-height: 1.7;
- margin-left: 24px;
- margin-bottom: 12px;
-}
-
-.legal-content li {
- margin-bottom: 6px;
-}
-
-.legal-content a {
- color: var(--accent);
- text-decoration: none;
-}
-
-.legal-content a:hover {
- text-decoration: underline;
-}
-
-.legal-content section {
- margin-bottom: 24px;
-}
-
-.input {
- width: 100%;
- padding: 12px 14px;
- font-size: 0.95rem;
- color: var(--text-primary);
- background: var(--bg-secondary);
- border: 1px solid var(--border);
- border-radius: var(--radius-md);
- outline: none;
- transition: all 0.15s ease;
-}
-
-.input:focus {
- border-color: var(--accent);
- box-shadow: 0 0 0 3px var(--accent-subtle);
-}
-
-.input::placeholder {
- color: var(--text-tertiary);
-}
-
-.notifications-page {
- max-width: 680px;
- margin: 0 auto;
-}
-
-.notifications-list {
- display: flex;
- flex-direction: column;
- gap: 12px;
-}
-
-.notification-item {
- display: flex;
- gap: 16px;
- align-items: flex-start;
- text-decoration: none;
- color: inherit;
-}
-
-.notification-item:hover {
- background: var(--bg-hover);
-}
-
-.notification-icon {
- width: 36px;
- height: 36px;
- border-radius: var(--radius-full);
- display: flex;
- align-items: center;
- justify-content: center;
- background: var(--bg-tertiary);
- color: var(--text-secondary);
- flex-shrink: 0;
-}
-
-.notification-icon[data-type="like"] {
- color: #ef4444;
- background: rgba(239, 68, 68, 0.1);
-}
-
-.notification-icon[data-type="reply"] {
- color: #3b82f6;
- background: rgba(59, 130, 246, 0.1);
-}
-
-.notification-content {
- flex: 1;
- min-width: 0;
-}
-
-.notification-text {
- font-size: 0.95rem;
- margin-bottom: 4px;
- line-height: 1.4;
- color: var(--text-primary);
-}
-
-.notification-text strong {
- font-weight: 600;
-}
-
-.notification-time {
- font-size: 0.85rem;
- color: var(--text-tertiary);
-}
-
-.notification-link {
- position: relative;
-}
-
-.notification-badge {
- position: absolute;
- top: -2px;
- right: -2px;
- background: var(--error);
- color: white;
- font-size: 0.7rem;
- font-weight: 700;
- min-width: 16px;
- height: 16px;
- border-radius: var(--radius-full);
- display: flex;
- align-items: center;
- justify-content: center;
- padding: 0 4px;
- border: 2px solid var(--bg-primary);
-}
+@import "./css/layout.css";
+@import "./css/base.css";
+@import "./css/buttons.css";
+@import "./css/buttons.css";
+@import "./css/feed.css";
+@import "./css/profile.css";
+@import "./css/login.css";
+@import "./css/annotations.css";
+@import "./css/collections.css";
+@import "./css/modals.css";
+@import "./css/notifications.css";
+@import "./css/skeleton.css";
+@import "./css/utilities.css";
diff --git a/web/src/pages/Feed.jsx b/web/src/pages/Feed.jsx
index a74b4e6..44e7d3d 100644
--- a/web/src/pages/Feed.jsx
+++ b/web/src/pages/Feed.jsx
@@ -3,6 +3,7 @@ import { useSearchParams } from "react-router-dom";
import AnnotationCard, { HighlightCard } from "../components/AnnotationCard";
import BookmarkCard from "../components/BookmarkCard";
import CollectionItemCard from "../components/CollectionItemCard";
+import AnnotationSkeleton from "../components/AnnotationSkeleton";
import { getAnnotationFeed, deleteHighlight } from "../api/client";
import { AlertIcon, InboxIcon } from "../components/Icons";
import { useAuth } from "../context/AuthContext";
@@ -151,107 +152,101 @@ export default function Feed() {
- {loading && (
+ {loading ? (