diff --git a/scripts/verify-extension-ui.mjs b/scripts/verify-extension-ui.mjs
index 4cfae94..c9c3062 100644
--- a/scripts/verify-extension-ui.mjs
+++ b/scripts/verify-extension-ui.mjs
@@ -306,7 +306,6 @@ async function collectPopupState(popup) {
};
};
const button = document.getElementById("scanBtn");
- const scanSection = document.getElementById("scanSection");
const toast = document.getElementById("toast");
const customSection = document.getElementById("customContextSection");
const customInput = document.getElementById("customInput");
@@ -355,14 +354,6 @@ async function collectPopupState(popup) {
rect: rectOf(button),
}
: null,
- scanSection: scanSection
- ? {
- title: scanSection.title,
- className: scanSection.className,
- rect: rectOf(scanSection),
- hidden: scanSection.classList.contains("hidden"),
- }
- : null,
toast: toast
? {
className: toast.className,
@@ -390,7 +381,7 @@ async function collectPopupState(popup) {
maxHeight: getComputedStyle(results).maxHeight,
},
eventCards: document.querySelectorAll(".event-card").length,
- skeletonCards: document.querySelectorAll(".skeleton-card").length,
+ skeletonCards: document.querySelectorAll(".skeleton-row").length,
upcomingCards: document.querySelectorAll("#upcomingEventsList .event-card").length,
pastCards: document.querySelectorAll("#pastEventsList .event-card").length,
selectedCards: document.querySelectorAll(".event-card.selected").length,
@@ -603,7 +594,7 @@ async function main() {
const before = await captureEvidence(popup, reportDir, "protected-page-before");
assert.equal(before.state.scanButton.disabled, true);
assert.equal(before.state.scanButton.title, "Chrome pages cannot be scanned.");
- assert.match(before.state.scanSection.className, /\bscan-unavailable\b/);
+ assert.match(before.state.scanButton.className, /\bscan-unavailable\b/);
assert.equal(before.state.toast.hidden, true);
assert.equal(before.state.results.eventCards, 0);
@@ -637,8 +628,7 @@ async function main() {
const evidence = await captureEvidence(popup, reportDir, "regular-page-ready");
assert.equal(evidence.state.scanButton.disabled, false);
- assert.equal(evidence.state.scanButton.title, "Scan Page");
- assert.equal(evidence.state.scanSection.title, "");
+ assert.equal(evidence.state.scanButton.title, "Scan this page");
assert.equal(evidence.state.toast.hidden, true);
assertNoPopupOverflow(evidence.state);
assertNoUnexpectedErrors(diagnostics);
@@ -660,7 +650,7 @@ async function main() {
await popup.click("#scanBtn");
await popup.waitForFunction(
- () => document.querySelectorAll(".skeleton-card").length > 0,
+ () => document.querySelectorAll(".skeleton-row").length > 0,
{ timeout: options.timeoutMs }
);
const scanning = await captureEvidence(popup, reportDir, "page-scan-scanning");
@@ -670,8 +660,11 @@ async function main() {
await waitForVisibleResults(popup);
const resultsEvidence = await captureEvidence(popup, reportDir, "page-scan-results");
assert.equal(resultsEvidence.state.results.eventCards, 10);
- assert.equal(resultsEvidence.state.results.pastCards, 10);
- assert.equal(resultsEvidence.state.results.upcomingCards, 0);
+ assert.equal(
+ resultsEvidence.state.results.pastCards + resultsEvidence.state.results.upcomingCards,
+ 10,
+ "all mock events should render across the upcoming/past lists"
+ );
assert.equal(resultsEvidence.state.addSelected.disabled, true);
assertFirstCardVisible(resultsEvidence.state, "page scan");
assertNoPopupOverflow(resultsEvidence.state);
@@ -740,7 +733,7 @@ async function main() {
);
const openEvidence = await captureEvidence(popup, reportDir, "custom-context-open");
assert.equal(openEvidence.state.customContext.hidden, false);
- assert.equal(openEvidence.state.scanSection.hidden, true);
+ assert.ok(openEvidence.state.scanButton, "scan button stays in the toolbar while custom context is open");
assert.match(openEvidence.state.customContext.textareaValue, /Neighborhood potluck/);
assertNoPopupOverflow(openEvidence.state);
await closePage(popup);
diff --git a/src/mocks/sample-events.json b/src/mocks/sample-events.json
index fc57ab7..7d58831 100644
--- a/src/mocks/sample-events.json
+++ b/src/mocks/sample-events.json
@@ -2,9 +2,9 @@
{
"title": "Team Sync Meeting",
"preview": "Team Sync",
- "startDate": "2025-10-01",
+ "startDate": "2026-06-25",
"startTime": "09:00",
- "endDate": "2025-10-01",
+ "endDate": "2026-06-25",
"endTime": "10:00",
"location": "Zoom",
"description": "Weekly team sync. Review progress and blockers.",
@@ -13,9 +13,9 @@
{
"title": "Product Launch Webinar",
"preview": "Launch Webinar",
- "startDate": "2025-10-03",
+ "startDate": "2026-06-26",
"startTime": "13:30",
- "endDate": "2025-10-03",
+ "endDate": "2026-06-26",
"endTime": "14:30",
"location": "YouTube Live",
"description": "Launch overview and live Q&A"
@@ -23,7 +23,7 @@
{
"title": "Coffee with Alex",
"preview": "Coffee w/ Alex",
- "startDate": "2025-10-05",
+ "startDate": "2026-06-27",
"startTime": "08:15",
"location": "Blue Bottle, 3rd St.",
"description": "Catch-up"
@@ -31,9 +31,9 @@
{
"title": "Design Review",
"preview": "Design Review",
- "startDate": "2025-10-06",
+ "startDate": "2026-06-28",
"startTime": "15:00",
- "endDate": "2025-10-06",
+ "endDate": "2026-06-28",
"endTime": "16:00",
"location": "Conference Room B",
"description": "Review new UI mockups with the design team."
@@ -41,7 +41,7 @@
{
"title": "Dentist Appointment",
"preview": "Dentist",
- "startDate": "2025-10-07",
+ "startDate": "2026-06-28",
"startTime": "11:30",
"location": "Downtown Dental Clinic",
"description": "Routine cleaning and checkup"
@@ -49,9 +49,9 @@
{
"title": "Yoga Class",
"preview": "Yoga",
- "startDate": "2025-10-08",
+ "startDate": "2026-06-29",
"startTime": "18:00",
- "endDate": "2025-10-08",
+ "endDate": "2026-06-29",
"endTime": "19:00",
"location": "Sunrise Yoga Studio",
"description": "Vinyasa flow class"
@@ -59,7 +59,7 @@
{
"title": "Board Game Night",
"preview": "Board Games",
- "startDate": "2025-10-09",
+ "startDate": "2026-06-30",
"startTime": "19:30",
"location": "Sam's House",
"description": "Bring your favorite board game!"
@@ -67,9 +67,9 @@
{
"title": "Marketing Strategy Session",
"preview": "Marketing Strategy",
- "startDate": "2025-10-10",
+ "startDate": "2026-07-01",
"startTime": "10:00",
- "endDate": "2025-10-10",
+ "endDate": "2026-07-01",
"endTime": "12:00",
"location": "Office 2A",
"description": "Plan Q4 campaigns"
@@ -77,9 +77,9 @@
{
"title": "Parent-Teacher Conference",
"preview": "Parent-Teacher Conf",
- "startDate": "2025-10-11",
+ "startDate": "2026-07-02",
"startTime": "16:00",
- "endDate": "2025-10-11",
+ "endDate": "2026-07-02",
"endTime": "16:30",
"location": "Lincoln Elementary",
"description": "Discuss progress with Ms. Lee"
@@ -87,9 +87,9 @@
{
"title": "Birthday Party: Jamie",
"preview": "Jamie Birthday",
- "startDate": "2025-10-12",
+ "startDate": "2026-07-05",
"startTime": "14:00",
- "endDate": "2025-10-12",
+ "endDate": "2026-07-05",
"endTime": "17:00",
"location": "Central Park, Picnic Area 3",
"description": "Potluck and games"
diff --git a/src/popup.js b/src/popup.js
index e8a62b4..0f2a046 100644
--- a/src/popup.js
+++ b/src/popup.js
@@ -17,14 +17,13 @@ import {
KEY_CUSTOM_VIEW_ACTIVE,
RESULTS_BUTTON_APPEAR_DELAY_MS,
SCAN_AVAILABILITY_CHECKING_MESSAGE,
- SKELETON_APPEARANCE_STAGGER_MS,
SKELETON_FADE_DURATION_MS,
SKELETON_STAGGER_DELAY_MS,
UI_STATE,
} from "./popup/constants.js";
import { loadCache, saveCache } from "./popup/cacheStore.js";
import {
- formatDateTime as formatPopupDateTime,
+ formatTimeOnly as formatPopupTimeOnly,
isEventPast as isPopupEventPast,
} from "./popup/dateTime.js";
import {
@@ -32,12 +31,16 @@ import {
renderEventLists,
restoreSelection as restoreEventSelection,
scrollToCard,
+ resetColorIndex,
} from "./popup/eventCards.js";
import { createScanPoller } from "./popup/scanPolling.js";
import { createPopupSettingsStore } from "./popup/settingsStore.js";
import { captureActiveTabHtml, getActiveTab } from "./popup/tabCapture.js";
import { hideToast, showToast } from "./popup/toast.js";
+// ===========================================
+// DOM elements
+// ===========================================
const scanBtn = document.getElementById("scanBtn");
const upcomingEventsListEl = document.getElementById("upcomingEventsList");
const pastEventsListEl = document.getElementById("pastEventsList");
@@ -47,18 +50,37 @@ const addSelectedBtn = document.getElementById("addSelectedBtn");
const settingsBtn = document.getElementById("settingsBtn");
const customContextBtn = document.getElementById("customContextBtn");
const customContextSection = document.getElementById("customContextSection");
-const scanSection = document.getElementById("scanSection");
const customInput = document.getElementById("customInput");
const scanMediaBtn = document.getElementById("scanMediaBtn");
+const tabUpcoming = document.getElementById("tabUpcoming");
+const tabPast = document.getElementById("tabPast");
+const footerSelectedEl = document.getElementById("footerSelected");
+const spiralBindingEl = document.getElementById("spiralBinding");
+const ruledListEl = document.getElementById("ruledList");
+
const popupSettingsStore = createPopupSettingsStore();
const ensureSettingsLoaded = popupSettingsStore.ensureSettingsLoaded;
const getTimeFormatPreference = popupSettingsStore.getTimeFormatPreference;
-// Kick off settings load early to minimize race conditions for UI rendering
ensureSettingsLoaded();
+// ===========================================
+// Generate spiral coils
+// ===========================================
+function generateSpiral() {
+ if (!spiralBindingEl) return;
+ for (let i = 0; i < 18; i++) {
+ const coil = document.createElement("div");
+ coil.className = "coil";
+ spiralBindingEl.appendChild(coil);
+ }
+}
+generateSpiral();
+
+// ===========================================
// Restore persistent state
+// ===========================================
async function restoreCustomContextState() {
try {
const result = await chrome.storage.local.get([
@@ -69,7 +91,6 @@ async function restoreCustomContextState() {
if (result[KEY_CUSTOM_VIEW_ACTIVE]) {
customContextSection?.classList.remove("hidden");
- scanSection?.classList.add("hidden");
resultsEl?.classList.remove("open", "has-results");
}
@@ -78,21 +99,8 @@ async function restoreCustomContextState() {
}
if (result[KEY_CUSTOM_FILES] && Array.isArray(result[KEY_CUSTOM_FILES])) {
- // Rehydrate files from base64
- // For preview purposes, we can use the base64 string directly as src
- // For sending, we can also use them directly since we already use base64 for transport
-
- // We need to reconstruct currentImageFiles for the logic to work
- // Since we can't easily turn base64 back to File objects with original names/types accurately without storing metadata,
- // we will store them as objects { base64, type, name } if possible, or just accept base64 strings in our internal array.
-
- // Let's assume currentImageFiles can handle objects or strings.
- // But existing logic uses URL.createObjectURL(file).
- // We need to adapt updateImageUI to handle { type: 'base64', data: ... }
-
currentImageFiles = result[KEY_CUSTOM_FILES].map(item => {
- // If it's a simple string (legacy or simple save), wrap it
- if (typeof item === 'string') return { data: item, type: 'image/png' }; // fallback
+ if (typeof item === 'string') return { data: item, type: 'image/png' };
return item;
});
updateImageUI();
@@ -102,14 +110,17 @@ async function restoreCustomContextState() {
}
}
-// Call restore
restoreCustomContextState();
let currentState = UI_STATE.IDLE;
-let stateCleanupTimeout = null; // Track cleanup timeout to prevent race conditions
+let stateCleanupTimeout = null;
let currentScanBlockReason = null;
let scanAvailabilityReady = false;
+let activeTab = "upcoming";
+// ===========================================
+// Scan availability
+// ===========================================
function applyScanButtonAvailability() {
if (!scanBtn) return;
@@ -121,34 +132,27 @@ function applyScanButtonAvailability() {
? reason
: isChecking
? SCAN_AVAILABILITY_CHECKING_MESSAGE
- : "Scan Page";
+ : "Scan this page";
scanBtn.disabled = isBusy || isUnavailable || isChecking;
scanBtn.title = title;
scanBtn.classList.toggle("scan-unavailable", isUnavailable || isChecking);
- scanSection?.classList.toggle("scan-unavailable", isUnavailable || isChecking);
- if (scanSection) {
- scanSection.title = isUnavailable || isChecking ? title : "";
- }
}
function setScanAvailabilityReason(reason) {
scanAvailabilityReady = true;
currentScanBlockReason = reason || null;
applyScanButtonAvailability();
- if (currentScanBlockReason) {
- hideToast();
- }
+ if (currentScanBlockReason) hideToast();
}
-async function refreshScanAvailability(activeTab = null) {
+async function refreshScanAvailability(activeTabObj = null) {
let reason = null;
-
scanAvailabilityReady = false;
applyScanButtonAvailability();
try {
- const tab = activeTab || await getActiveTab();
+ const tab = activeTabObj || await getActiveTab();
reason = getRequiredTabScanBlockReason(tab?.url);
} catch (err) {
DEBUG && debug("[Eventy][Popup] Failed to check scan availability:", err);
@@ -159,58 +163,136 @@ async function refreshScanAvailability(activeTab = null) {
return reason;
}
-/**
- * Transition to scanning state
- * Shows skeleton cards and smoothly expands the container
- */
+// ===========================================
+// Segmented toggle
+// ===========================================
+function switchTab(tab) {
+ activeTab = tab;
+ tabUpcoming?.classList.toggle("active", tab === "upcoming");
+ tabPast?.classList.toggle("active", tab === "past");
+ upcomingEventsListEl?.classList.toggle("hidden", tab !== "upcoming");
+ pastEventsListEl?.classList.toggle("hidden", tab !== "past");
+}
+
+tabUpcoming?.addEventListener("click", () => switchTab("upcoming"));
+tabPast?.addEventListener("click", () => switchTab("past"));
+
+// ===========================================
+// Week strip
+// ===========================================
+function updateWeekStrip(events) {
+ const weekStripEl = document.getElementById("weekStrip");
+ if (!weekStripEl) return;
+ weekStripEl.innerHTML = "";
+
+ // Find the Monday of the current week
+ const now = new Date();
+ const dayOfWeek = now.getDay();
+ const mondayOffset = dayOfWeek === 0 ? -6 : 1 - dayOfWeek;
+ const monday = new Date(now.getFullYear(), now.getMonth(), now.getDate() + mondayOffset);
+
+ const dayLetters = ["M", "T", "W", "T", "F", "S", "S"];
+
+ // Build a map of date -> color for events this week
+ const dateColorMap = {};
+ if (events) {
+ const allCards = resultsEl?.querySelectorAll(".event-card");
+ if (allCards) {
+ allCards.forEach(c => {
+ const idx = Number(c.dataset.idx);
+ const ev = events[idx];
+ if (ev?.startDate && c.dataset.color) {
+ dateColorMap[ev.startDate] = c.dataset.color;
+ }
+ });
+ }
+ }
+
+ for (let i = 0; i < 7; i++) {
+ const d = new Date(monday);
+ d.setDate(monday.getDate() + i);
+ const dateStr = `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}-${String(d.getDate()).padStart(2, "0")}`;
+ const isSunday = d.getDay() === 0;
+ const dotColor = dateColorMap[dateStr];
+
+ const col = document.createElement("div");
+ col.className = "week-day";
+ col.innerHTML = `
+
-
${escapeHtml(previewText)}
-
-
-
- ${recurrenceHtml}
- ${escapeHtml(startStr)}
-
- ${endStr
- ? `
${escapeHtml(
- endStr
- )}
`
- : ""
- }
-
- ${locationHtml}
-
+
+
${escapeHtml(monthAbbr)}
+
${escapeHtml(dayNum)}
+
+
+
${titleHtml}
+ ${noteHtml ? `
${noteHtml}
` : ''}
+ ${badgeHtml}${multiDayHtml}
`;
- wrapper.addEventListener("click", () => {
- wrapper.classList.toggle("selected");
+ const toggleSelected = () => {
+ const selected = wrapper.classList.toggle('selected');
+ wrapper.setAttribute('aria-pressed', String(selected));
onToggle(wrapper);
+ };
+
+ wrapper.addEventListener('click', toggleSelected);
+ wrapper.addEventListener('keydown', (e) => {
+ if (e.key === 'Enter' || e.key === ' ') {
+ e.preventDefault();
+ toggleSelected();
+ }
});
return wrapper;
}
+function formatMonthAbbr(dateStr) {
+ if (!dateStr) return '???';
+ try {
+ const d = new Date(`${dateStr}T00:00:00`);
+ if (isNaN(d.getTime())) return '???';
+ return ['JAN', 'FEB', 'MAR', 'APR', 'MAY', 'JUN',
+ 'JUL', 'AUG', 'SEP', 'OCT', 'NOV', 'DEC'][d.getMonth()];
+ } catch { return '???'; }
+}
+
+function formatDayNum(dateStr) {
+ if (!dateStr) return '??';
+ try {
+ const d = new Date(`${dateStr}T00:00:00`);
+ if (isNaN(d.getTime())) return '??';
+ return String(d.getDate()).padStart(2, '0');
+ } catch { return '??'; }
+}
+
+const MONTHS_FULL = ['January', 'February', 'March', 'April', 'May', 'June',
+ 'July', 'August', 'September', 'October', 'November', 'December'];
+
+function monthKeyFromEvent(ev) {
+ const s = ev?.startDate;
+ if (!s) return 'no-date';
+ return String(s).slice(0, 7);
+}
+
+function createMonthSectionHeader(ev) {
+ const header = document.createElement('div');
+ header.className = 'month-section-header';
+ const s = ev?.startDate;
+ let label = 'No date';
+ let yearHtml = '';
+ if (s) {
+ const d = new Date(`${s}T00:00:00`);
+ if (!isNaN(d.getTime())) {
+ label = MONTHS_FULL[d.getMonth()].toUpperCase();
+ yearHtml = `
${d.getFullYear()}`;
+ }
+ }
+ header.innerHTML = `${label}${yearHtml}`;
+ return header;
+}
+
+function parseTime(timeStr) {
+ if (!timeStr) return 0;
+ const match = timeStr.match(/(\d{1,2}):(\d{2})\s*([AP]M)?/i);
+ if (!match) return 0;
+ let h = parseInt(match[1], 10);
+ const m = parseInt(match[2], 10);
+ if (match[3]) {
+ const pm = match[3].toUpperCase() === 'PM';
+ if (pm && h !== 12) h += 12;
+ else if (!pm && h === 12) h = 0;
+ }
+ return h * 60 + m;
+}
+
export async function renderEventLists(
events,
{
@@ -73,47 +178,77 @@ export async function renderEventLists(
) {
if (!upcomingEventsListEl || !pastEventsListEl) return;
- upcomingEventsListEl.innerHTML = "";
- pastEventsListEl.innerHTML = "";
+ upcomingEventsListEl.innerHTML = '';
+ pastEventsListEl.innerHTML = '';
+
+ resetColorIndex();
const upcomingEvents = [];
const pastEvents = [];
events.forEach((ev, idx) => {
- if (isEventPast(ev)) {
- pastEvents.push({ event: ev, originalIndex: idx });
+ const past = isEventPast(ev);
+ const color = assignEventColor(past);
+ if (past) {
+ pastEvents.push({ event: ev, originalIndex: idx, color });
} else {
- upcomingEvents.push({ event: ev, originalIndex: idx });
+ upcomingEvents.push({ event: ev, originalIndex: idx, color });
}
});
- upcomingEvents.forEach(({ event: ev, originalIndex: idx }) => {
- upcomingEventsListEl.appendChild(createCard(ev, idx));
- });
+ // Sort by date then time
+ const sortItems = (items, asc) => {
+ items.sort((a, b) => {
+ const dateComp = asc
+ ? (a.event.startDate || '').localeCompare(b.event.startDate || '')
+ : (b.event.startDate || '').localeCompare(a.event.startDate || '');
+ if (dateComp !== 0) return dateComp;
+ return parseTime(a.event.startTime) - parseTime(b.event.startTime);
+ });
+ };
- pastEvents.forEach(({ event: ev, originalIndex: idx }) => {
- pastEventsListEl.appendChild(createCard(ev, idx));
- });
+ sortItems(upcomingEvents, true);
+ sortItems(pastEvents, false);
+
+ let lastUpcomingKey = null;
+ for (const { event, originalIndex, color } of upcomingEvents) {
+ const key = monthKeyFromEvent(event);
+ if (key !== lastUpcomingKey) {
+ upcomingEventsListEl.appendChild(createMonthSectionHeader(event));
+ lastUpcomingKey = key;
+ }
+ upcomingEventsListEl.appendChild(createCard(event, originalIndex, color, false));
+ }
+
+ let lastPastKey = null;
+ for (const { event, originalIndex, color } of pastEvents) {
+ const key = monthKeyFromEvent(event);
+ if (key !== lastPastKey) {
+ pastEventsListEl.appendChild(createMonthSectionHeader(event));
+ lastPastKey = key;
+ }
+ pastEventsListEl.appendChild(createCard(event, originalIndex, color, true));
+ }
updateButtonStates();
+
+ return { upcomingCount: upcomingEvents.length, pastCount: pastEvents.length, upcomingEvents, pastEvents };
}
export function scrollToCard(card) {
- const scrollContainer = card.closest(".events-list");
+ const scrollContainer = card.closest('.ruled-list');
if (scrollContainer) {
requestAnimationFrame(() => {
setTimeout(() => {
const containerRect = scrollContainer.getBoundingClientRect();
const cardRect = card.getBoundingClientRect();
const currentScroll = scrollContainer.scrollTop;
-
const cardCenter = cardRect.top + cardRect.height / 2;
const containerCenter = containerRect.top + containerRect.height / 2;
const diff = cardCenter - containerCenter;
-
scrollContainer.scrollTo({
top: Math.max(0, currentScroll + diff),
- behavior: "smooth",
+ behavior: 'smooth',
});
}, 200);
});
@@ -122,14 +257,18 @@ export function scrollToCard(card) {
export function restoreSelection(selectedIdxs, { resultsEl, updateButtonStates }) {
if (!selectedIdxs?.length) return null;
- const cards = resultsEl?.querySelectorAll(".event-card");
+ const cards = resultsEl?.querySelectorAll('.event-card');
let firstSelected = null;
if (cards) {
- cards.forEach((el) => el.classList.remove("selected"));
+ cards.forEach((el) => {
+ el.classList.remove('selected');
+ el.setAttribute('aria-pressed', 'false');
+ });
selectedIdxs.forEach((i) => {
const el = resultsEl?.querySelector(`.event-card[data-idx="${i}"]`);
if (el) {
- el.classList.add("selected");
+ el.classList.add('selected');
+ el.setAttribute('aria-pressed', 'true');
if (!firstSelected) firstSelected = el;
}
});
diff --git a/src/ui/base.css b/src/ui/base.css
index 2c620cf..45f0937 100644
--- a/src/ui/base.css
+++ b/src/ui/base.css
@@ -1,6 +1,159 @@
-/* Minimalist theme with orange accent */
+/* ===========================================
+ Font faces — bundled woff2 (no external requests in MV3)
+ =========================================== */
+@font-face {
+ font-family: 'Hanken Grotesk';
+ font-style: normal;
+ font-weight: 400 800;
+ font-display: swap;
+ src: url(./fonts/hanken-grotesk-latin.woff2) format('woff2');
+ unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
+}
+
+@font-face {
+ font-family: 'Hanken Grotesk';
+ font-style: normal;
+ font-weight: 400 800;
+ font-display: swap;
+ src: url(./fonts/hanken-grotesk-latin-ext.woff2) format('woff2');
+ unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
+}
+
+@font-face {
+ font-family: 'Kalam';
+ font-style: normal;
+ font-weight: 300;
+ font-display: swap;
+ src: url(./fonts/kalam-latin-300.woff2) format('woff2');
+ unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
+}
+
+@font-face {
+ font-family: 'Kalam';
+ font-style: normal;
+ font-weight: 300;
+ font-display: swap;
+ src: url(./fonts/kalam-latin-ext-300.woff2) format('woff2');
+ unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
+}
+
+@font-face {
+ font-family: 'Kalam';
+ font-style: normal;
+ font-weight: 400;
+ font-display: swap;
+ src: url(./fonts/kalam-latin-400.woff2) format('woff2');
+ unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
+}
+
+@font-face {
+ font-family: 'Kalam';
+ font-style: normal;
+ font-weight: 400;
+ font-display: swap;
+ src: url(./fonts/kalam-latin-ext-400.woff2) format('woff2');
+ unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
+}
+
+@font-face {
+ font-family: 'Kalam';
+ font-style: normal;
+ font-weight: 700;
+ font-display: swap;
+ src: url(./fonts/kalam-latin-700.woff2) format('woff2');
+ unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
+}
+
+@font-face {
+ font-family: 'Kalam';
+ font-style: normal;
+ font-weight: 700;
+ font-display: swap;
+ src: url(./fonts/kalam-latin-ext-700.woff2) format('woff2');
+ unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
+}
+
+/* ===========================================
+ CSS custom properties — Light mode (default)
+ =========================================== */
:root {
- /* Light mode colors */
+ /* Brand */
+ --brand: #F4691F;
+ --brand-deep: #C25E15;
+ --brand-bright: #FF9A52;
+
+ /* Desk & Page */
+ --desk: linear-gradient(155deg, #B0BFB4, #96AA9C 65%, #869a8d);
+ --paper: #FAF7EE;
+ --ruled-line: rgba(122, 150, 168, .28);
+
+ /* Ink */
+ --ink: #29343A;
+ --ink-muted: #6E7C84;
+ --ink-secondary: #7E8A82;
+
+ /* Controls */
+ --tab-track: #ECE7DA;
+ --tab-active: #fff;
+ --tab-active-text: #29343A;
+ --tab-inactive-text: #94A097;
+ --tab-shadow: 0 1px 2px rgba(0, 0, 0, .12);
+
+ /* Checkbox */
+ --checkbox-bg: #fff;
+ --checkbox-border: #B9C2BC;
+
+ /* Date chip */
+ --chip-cell-bg: #fff;
+ --chip-cell-text: #29343A;
+ --chip-border: rgba(0, 0, 0, .10);
+ --chip-shadow: 0 2px 4px rgba(0, 0, 0, .12);
+
+ /* Ghost buttons */
+ --ghost-bg: #fff;
+ --ghost-border: #C9D0C9;
+ --ghost-text: #5d6b62;
+
+ /* Scan button (primary icon) */
+ --scan-bg: #29343A;
+ --scan-text: #FAF7EE;
+
+ /* Footer */
+ --footer-bg: rgba(250, 247, 238, .96);
+ --footer-border: rgba(122, 150, 168, .30);
+ --footer-text: #41514A;
+ --footer-muted: #7E8A82;
+
+ /* Page shadows */
+ --page-shadow: 0 16px 30px rgba(35, 50, 45, .30), 0 3px 8px rgba(0, 0, 0, .18);
+ --cta-shadow: 0 2px 0 rgba(0, 0, 0, .20);
+
+ /* Week strip */
+ --week-divider: 1px dashed rgba(122, 150, 168, .45);
+ --weekday-text: #9AA7A0;
+ --sunday-text: #C5908F;
+
+ /* Coil gradient */
+ --coil-gradient: linear-gradient(180deg, #ffc287 0%, #f4842f 45%, #c25e15 100%);
+ --coil-shadow: inset -1.5px 0 1.5px rgba(255, 255, 255, .75),
+ inset 1.5px 0 2px rgba(0, 0, 0, .25),
+ 0 2px 3px rgba(0, 0, 0, .22);
+
+ /* Badge */
+ --badge-text: #7E8A82;
+ --badge-border: #CBD3CC;
+
+ /* Desk inset */
+ --desk-inset: inset 0 1px 0 rgba(255, 255, 255, .22);
+
+ /* Past opacity */
+ --past-opacity: .72;
+
+ /* Transitions */
+ --transition-fast: 150ms ease;
+ --transition-base: 200ms ease;
+
+ /* Legacy vars (for settings page, toast, etc.) */
--bg: #ffffff;
--bg-secondary: #fafafa;
--text: #1a1a1a;
@@ -8,28 +161,56 @@
--border: #e5e5e5;
--border-light: #f5f5f5;
--bg-hover: #f0f0f0;
-
- /* Orange accent colors */
- --accent-primary: #ff6b35;
- --accent-hover: #ff5722;
- --accent-light: #fff4f0;
- --accent-lighter: #fffaf8;
- --accent-date: #e64a19;
-
- /* Shadows and effects */
- --shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.04);
- --shadow-md: 0 2px 4px rgba(0, 0, 0, 0.06);
- --shadow-lg: 0 4px 8px rgba(0, 0, 0, 0.08);
-
- /* Transitions */
- --transition-fast: 150ms ease;
- --transition-base: 200ms ease;
- --transition-slow: 250ms ease;
+ --accent-primary: #F4691F;
+ --accent-hover: #e05a12;
+ --highlight: rgba(255, 190, 50, .22);
+ --shadow-sm: 0 1px 2px rgba(0, 0, 0, .04);
}
-/* Dark mode colors */
+/* ===========================================
+ Dark mode
+ =========================================== */
@media (prefers-color-scheme: dark) {
- body.auto-mode {
+ body.auto-mode:not(.settings-page) {
+ --desk: linear-gradient(155deg, #232B2F, #1A2023 65%, #141A1C);
+ --paper: #232B2F;
+ --ruled-line: rgba(255, 255, 255, .07);
+ --ink: #EAEEEA;
+ --ink-muted: #8C9A93;
+ --ink-secondary: #7E8C85;
+ --tab-track: #2C353A;
+ --tab-active: #3C474D;
+ --tab-active-text: #EAEEEA;
+ --tab-inactive-text: #7E8C85;
+ --tab-shadow: 0 1px 2px rgba(0, 0, 0, .3);
+ --checkbox-bg: #2A3338;
+ --checkbox-border: rgba(255, 255, 255, .22);
+ --chip-cell-bg: #ECEFE8;
+ --chip-cell-text: #242B30;
+ --chip-border: rgba(255, 255, 255, .08);
+ --chip-shadow: 0 2px 5px rgba(0, 0, 0, .35);
+ --ghost-bg: #2C353A;
+ --ghost-border: rgba(255, 255, 255, .12);
+ --ghost-text: #9AA8A1;
+ --scan-bg: var(--brand);
+ --scan-text: #fff;
+ --footer-bg: #1E2528;
+ --footer-border: rgba(255, 255, 255, .10);
+ --footer-text: #BFC9C3;
+ --footer-muted: #8C9A93;
+ --page-shadow: 0 16px 32px rgba(0, 0, 0, .55), 0 3px 8px rgba(0, 0, 0, .4);
+ --cta-shadow: 0 2px 0 rgba(0, 0, 0, .35);
+ --week-divider: 1px dashed rgba(255, 255, 255, .12);
+ --weekday-text: #7E8C85;
+ --coil-gradient: linear-gradient(180deg, #ffb877, #f4842f, #b0530f);
+ --coil-shadow: inset -1.5px 0 1.5px rgba(255, 255, 255, .55),
+ inset 1.5px 0 2px rgba(0, 0, 0, .4),
+ 0 2px 4px rgba(0, 0, 0, .45);
+ --badge-text: #8C9A93;
+ --badge-border: rgba(255, 255, 255, .16);
+ --desk-inset: inset 0 1px 0 rgba(255, 255, 255, .06);
+ --past-opacity: .6;
+ --brand-bright: #FF9A52;
--bg: #171717;
--bg-secondary: #262626;
--bg-hover: #333333;
@@ -37,16 +218,52 @@
--text-secondary: #a3a3a3;
--border: #404040;
--border-light: #2a2a2a;
- --accent-light: rgba(255, 107, 53, 0.15);
- --accent-lighter: rgba(255, 107, 53, 0.08);
- --accent-date: #ff8a65;
- --shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.5);
- --shadow-md: 0 2px 4px rgba(0, 0, 0, 0.6);
- --shadow-lg: 0 4px 8px rgba(0, 0, 0, 0.7);
+ --accent-primary: #F4691F;
+ --accent-hover: #e05a12;
+ --highlight: rgba(255, 180, 50, .12);
}
}
-body.dark-mode {
+body.dark-mode:not(.settings-page) {
+ --desk: linear-gradient(155deg, #232B2F, #1A2023 65%, #141A1C);
+ --paper: #232B2F;
+ --ruled-line: rgba(255, 255, 255, .07);
+ --ink: #EAEEEA;
+ --ink-muted: #8C9A93;
+ --ink-secondary: #7E8C85;
+ --tab-track: #2C353A;
+ --tab-active: #3C474D;
+ --tab-active-text: #EAEEEA;
+ --tab-inactive-text: #7E8C85;
+ --tab-shadow: 0 1px 2px rgba(0, 0, 0, .3);
+ --checkbox-bg: #2A3338;
+ --checkbox-border: rgba(255, 255, 255, .22);
+ --chip-cell-bg: #ECEFE8;
+ --chip-cell-text: #242B30;
+ --chip-border: rgba(255, 255, 255, .08);
+ --chip-shadow: 0 2px 5px rgba(0, 0, 0, .35);
+ --ghost-bg: #2C353A;
+ --ghost-border: rgba(255, 255, 255, .12);
+ --ghost-text: #9AA8A1;
+ --scan-bg: var(--brand);
+ --scan-text: #fff;
+ --footer-bg: #1E2528;
+ --footer-border: rgba(255, 255, 255, .10);
+ --footer-text: #BFC9C3;
+ --footer-muted: #8C9A93;
+ --page-shadow: 0 16px 32px rgba(0, 0, 0, .55), 0 3px 8px rgba(0, 0, 0, .4);
+ --cta-shadow: 0 2px 0 rgba(0, 0, 0, .35);
+ --week-divider: 1px dashed rgba(255, 255, 255, .12);
+ --weekday-text: #7E8C85;
+ --coil-gradient: linear-gradient(180deg, #ffb877, #f4842f, #b0530f);
+ --coil-shadow: inset -1.5px 0 1.5px rgba(255, 255, 255, .55),
+ inset 1.5px 0 2px rgba(0, 0, 0, .4),
+ 0 2px 4px rgba(0, 0, 0, .45);
+ --badge-text: #8C9A93;
+ --badge-border: rgba(255, 255, 255, .16);
+ --desk-inset: inset 0 1px 0 rgba(255, 255, 255, .06);
+ --past-opacity: .6;
+ --brand-bright: #FF9A52;
--bg: #171717;
--bg-secondary: #262626;
--bg-hover: #333333;
@@ -54,32 +271,53 @@ body.dark-mode {
--text-secondary: #a3a3a3;
--border: #404040;
--border-light: #2a2a2a;
- --accent-light: rgba(255, 107, 53, 0.15);
- --accent-lighter: rgba(255, 107, 53, 0.08);
- --accent-date: #ff8a65;
- --shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.5);
- --shadow-md: 0 2px 4px rgba(0, 0, 0, 0.6);
- --shadow-lg: 0 4px 8px rgba(0, 0, 0, 0.7);
+ --accent-primary: #F4691F;
+ --accent-hover: #e05a12;
+ --highlight: rgba(255, 180, 50, .12);
}
-body.light-mode {
+/* Settings page keeps the old simple variables */
+body.light-mode.settings-page,
+body.auto-mode.settings-page {
--bg: #ffffff;
--bg-secondary: #fafafa;
--text: #1a1a1a;
--text-secondary: #737373;
--border: #e5e5e5;
--border-light: #f5f5f5;
- --accent-light: #fff4f0;
- --accent-lighter: #fffaf8;
+ --accent-primary: #F4691F;
+ --accent-hover: #e05a12;
}
-/* Base styles */
-* {
- box-sizing: border-box;
+@media (prefers-color-scheme: dark) {
+ body.auto-mode.settings-page {
+ --bg: #171717;
+ --bg-secondary: #262626;
+ --bg-hover: #333333;
+ --text: #fafafa;
+ --text-secondary: #a3a3a3;
+ --border: #404040;
+ --border-light: #2a2a2a;
+ }
+}
+
+body.dark-mode.settings-page {
+ --bg: #171717;
+ --bg-secondary: #262626;
+ --bg-hover: #333333;
+ --text: #fafafa;
+ --text-secondary: #a3a3a3;
+ --border: #404040;
+ --border-light: #2a2a2a;
}
-html {
- scroll-behavior: smooth;
+/* ===========================================
+ Base reset & typography
+ =========================================== */
+*,
+*::before,
+*::after {
+ box-sizing: border-box;
}
html,
@@ -89,10 +327,8 @@ body {
}
body {
- font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,
- "Helvetica Neue", Arial, sans-serif;
- color: var(--text);
- background: var(--bg);
+ font-family: 'Hanken Grotesk', -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
+ color: var(--ink);
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
@@ -107,16 +343,13 @@ body.theme-loaded {
transition: opacity 200ms ease-in;
}
-/* Typography */
h1 {
- font-size: 17px;
- font-weight: 600;
+ font-size: 15px;
+ font-weight: 800;
margin: 0;
- color: var(--text);
- letter-spacing: -0.01em;
+ letter-spacing: -.2px;
}
-/* Button styles */
button {
display: inline-flex;
align-items: center;
@@ -142,7 +375,7 @@ button:not(:disabled):active {
}
button:focus-visible {
- outline: 2px solid var(--accent-primary);
+ outline: 2px solid var(--brand);
outline-offset: 2px;
}
@@ -151,39 +384,10 @@ button svg {
}
@keyframes spin {
- from {
- transform: translate(-50%, -50%) rotate(0deg);
- }
-
- to {
- transform: translate(-50%, -50%) rotate(360deg);
- }
-}
-
-/* Smooth theme transition */
-body:not(.theme-loading) {
- transition: background-color var(--transition-slow),
- color var(--transition-slow);
-}
-
-/* Fade in animation for initial load */
-@keyframes fadeIn {
- from {
- opacity: 0;
- transform: translateY(-10px);
- }
-
- to {
- opacity: 1;
- transform: translateY(0);
- }
-}
-
-body:not(.theme-loading) {
- animation: fadeIn var(--transition-slow) ease-out;
+ from { transform: translate(-50%, -50%) rotate(0deg); }
+ to { transform: translate(-50%, -50%) rotate(360deg); }
}
-/* Generic hidden class */
.hidden {
display: none !important;
}
diff --git a/src/ui/fonts/hanken-grotesk-latin-ext.woff2 b/src/ui/fonts/hanken-grotesk-latin-ext.woff2
new file mode 100644
index 0000000..404fa65
Binary files /dev/null and b/src/ui/fonts/hanken-grotesk-latin-ext.woff2 differ
diff --git a/src/ui/fonts/hanken-grotesk-latin.woff2 b/src/ui/fonts/hanken-grotesk-latin.woff2
new file mode 100644
index 0000000..e71775f
Binary files /dev/null and b/src/ui/fonts/hanken-grotesk-latin.woff2 differ
diff --git a/src/ui/fonts/kalam-latin-300.woff2 b/src/ui/fonts/kalam-latin-300.woff2
new file mode 100644
index 0000000..ef188f7
Binary files /dev/null and b/src/ui/fonts/kalam-latin-300.woff2 differ
diff --git a/src/ui/fonts/kalam-latin-400.woff2 b/src/ui/fonts/kalam-latin-400.woff2
new file mode 100644
index 0000000..d3fec05
Binary files /dev/null and b/src/ui/fonts/kalam-latin-400.woff2 differ
diff --git a/src/ui/fonts/kalam-latin-700.woff2 b/src/ui/fonts/kalam-latin-700.woff2
new file mode 100644
index 0000000..71e687f
Binary files /dev/null and b/src/ui/fonts/kalam-latin-700.woff2 differ
diff --git a/src/ui/fonts/kalam-latin-ext-300.woff2 b/src/ui/fonts/kalam-latin-ext-300.woff2
new file mode 100644
index 0000000..ec7b872
Binary files /dev/null and b/src/ui/fonts/kalam-latin-ext-300.woff2 differ
diff --git a/src/ui/fonts/kalam-latin-ext-400.woff2 b/src/ui/fonts/kalam-latin-ext-400.woff2
new file mode 100644
index 0000000..240c420
Binary files /dev/null and b/src/ui/fonts/kalam-latin-ext-400.woff2 differ
diff --git a/src/ui/fonts/kalam-latin-ext-700.woff2 b/src/ui/fonts/kalam-latin-ext-700.woff2
new file mode 100644
index 0000000..dc7d012
Binary files /dev/null and b/src/ui/fonts/kalam-latin-ext-700.woff2 differ
diff --git a/src/ui/index.html b/src/ui/index.html
index f8514c9..7d45b29 100644
--- a/src/ui/index.html
+++ b/src/ui/index.html
@@ -10,120 +10,160 @@
-
+
+
+
-
-
-
+
+
+
+
+
+
-
-
diff --git a/src/ui/popup.css b/src/ui/popup.css
index 7333117..174cf06 100644
--- a/src/ui/popup.css
+++ b/src/ui/popup.css
@@ -1,760 +1,657 @@
-/* Popup-specific body styles */
+/* ===========================================
+ Popup body — spiral planner
+ =========================================== */
body:not(.settings-page) {
- min-width: 320px;
- width: 320px;
+ width: 428px;
+ min-width: 428px;
min-height: 80px;
- max-height: 600px;
- overflow-y: hidden;
- overflow-x: hidden;
+ max-height: 660px;
+ overflow: hidden;
+ background: var(--desk);
position: relative;
- transition: max-height 350ms cubic-bezier(0.4, 0, 0.2, 1);
- border: 1px solid var(--border);
- display: flex;
- flex-direction: column;
-}
-
-body:not(.settings-page):has(.results-section.scanning) {
- min-height: 330px;
-}
-
-body:not(.settings-page):has(.results-section.has-results) {
- min-height: 390px;
}
-body:not(.settings-page):has(.custom-context-section:not(.hidden)) {
- min-height: 244px;
-}
-
-/* Popup header */
-.popup-header {
- display: flex;
- justify-content: space-between;
- align-items: center;
- padding: 8px 16px;
- background: var(--bg);
- border-bottom: 1px solid var(--border-light);
+/* ===========================================
+ Desk mat
+ =========================================== */
+.desk-mat {
+ padding: 24px;
position: relative;
- z-index: 10;
- flex-shrink: 0;
}
-.logo-section {
- display: flex;
- align-items: center;
- gap: 8px;
-}
-
-.logo-icon {
- transition: transform 250ms cubic-bezier(0.4, 0, 0.2, 1);
-}
-
-.logo-section:hover .logo-icon {
- transform: rotate(5deg) scale(1.05);
-}
-
-/* Icon button */
-.icon-button {
+/* ===========================================
+ Spiral binding — row of coils across top
+ =========================================== */
+.spiral-binding {
+ position: absolute;
+ top: 12px;
+ left: 24px;
+ right: 24px;
display: flex;
- align-items: center;
justify-content: center;
- width: 36px;
- height: 36px;
- padding: 0;
- background: transparent;
- border: none;
- border-radius: 8px;
- color: var(--text-secondary);
- cursor: pointer;
- transition: all var(--transition-base);
+ gap: 7px;
+ z-index: 3;
+ pointer-events: none;
}
-.icon-button:hover {
- background: var(--bg-secondary);
- color: var(--accent-primary);
+.coil {
+ width: 12px;
+ height: 25px;
+ border-radius: 6px;
+ background: var(--coil-gradient);
+ transform: rotate(22deg);
+ box-shadow: var(--coil-shadow);
+ flex-shrink: 0;
}
-.icon-button:active {
- opacity: 0.7;
+/* ===========================================
+ Page
+ =========================================== */
+.planner-page {
+ position: relative;
+ background: var(--paper);
+ border-radius: 4px;
+ overflow: hidden;
+ box-shadow: var(--page-shadow);
}
-/* Scan section */
-.scan-section {
- padding: 12px 16px;
- transition: padding 300ms ease;
- flex-shrink: 0;
+.page-top-shadow {
+ height: 14px;
+ background: linear-gradient(180deg, rgba(0, 0, 0, .06), rgba(0, 0, 0, 0));
}
-.scan-section .primary-button {
- width: 100%;
- box-shadow: 0 2px 4px rgba(255, 107, 53, 0.15);
+body.dark-mode .page-top-shadow,
+body.auto-mode .page-top-shadow {
+ background: linear-gradient(180deg, rgba(0, 0, 0, .30), rgba(0, 0, 0, 0));
}
-.scan-section .primary-button:hover:not(:disabled) {
- background: var(--accent-hover);
- box-shadow: 0 4px 12px rgba(255, 107, 53, 0.3);
+@media (prefers-color-scheme: dark) {
+ body.auto-mode .page-top-shadow {
+ background: linear-gradient(180deg, rgba(0, 0, 0, .30), rgba(0, 0, 0, 0));
+ }
}
-.scan-section.scan-unavailable {
- cursor: not-allowed;
+/* ===========================================
+ Header / toolbar
+ =========================================== */
+.popup-header {
+ padding: 6px 16px 0;
}
-.scan-section.scan-unavailable .primary-button {
- pointer-events: none;
+.header-row {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
}
-/* Results section - completely hidden by default */
-.results-section {
- display: grid;
- grid-template-rows: 0fr 0fr 0fr;
- gap: 0;
- padding: 0;
- max-height: 0;
- overflow: hidden;
- transition: grid-template-rows 400ms cubic-bezier(0.4, 0, 0.2, 1),
- padding 400ms cubic-bezier(0.4, 0, 0.2, 1),
- gap 400ms cubic-bezier(0.4, 0, 0.2, 1),
- max-height 400ms cubic-bezier(0.4, 0, 0.2, 1), opacity 300ms ease;
- flex: 1 1 auto;
- min-height: 0;
+.logo-section {
+ display: flex;
+ align-items: center;
+ gap: 7px;
}
-.results-section:not(.open) {
- max-height: 0;
- padding: 0;
- margin: 0;
+.logo-mark {
+ width: 17px;
+ height: 17px;
+ border-radius: 4px;
+ object-fit: contain;
+ flex-shrink: 0;
}
-.results-section>* {
- min-height: 0;
- overflow: hidden;
+.logo-wordmark {
+ font-weight: 800;
+ font-size: 15px;
+ letter-spacing: -.2px;
+ color: var(--ink);
}
-/* Open state: expand grid and show content */
-.results-section.open {
- grid-template-rows: auto auto 0fr;
- padding: 0px 16px 12px 16px;
- gap: 0;
- max-height: 100%;
- overflow-y: hidden;
+.toolbar-buttons {
+ display: flex;
+ align-items: center;
+ gap: 7px;
}
-/* Results loaded state: show button */
-.results-section.open.has-results {
- grid-template-rows: auto auto auto;
- gap: 0;
- max-height: 100%;
- overflow-y: hidden;
+.toolbar-btn {
+ width: 34px;
+ height: 34px;
+ border-radius: 9px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ cursor: pointer;
+ padding: 0;
+ border: none;
+ transition: opacity var(--transition-fast);
}
-/* Add gap between non-collapsed sections only */
-.results-section.open.has-results .events-section:not(:has(.events-list:empty)) {
- margin-bottom: 8px;
+.toolbar-btn:hover {
+ opacity: 0.85;
}
-.results-section.open.has-results .action-button {
- margin-top: 8px;
+.toolbar-btn:active {
+ opacity: 0.7;
}
-/* Scanning state: only show events container */
-.results-section.open.scanning {
- grid-template-rows: auto 0fr 0fr;
- gap: 0;
- max-height: none;
+.toolbar-btn.primary {
+ background: var(--scan-bg);
+ color: var(--scan-text);
+ box-shadow: 0 2px 0 rgba(0, 0, 0, .28);
}
-/* Primary button (Scan) */
-.primary-button {
- background: var(--accent-primary);
- color: white;
- transition: all 250ms cubic-bezier(0.4, 0, 0.2, 1);
+body.dark-mode .toolbar-btn.primary,
+body.auto-mode .toolbar-btn.primary {
+ box-shadow: 0 2px 0 rgba(0, 0, 0, .4);
}
-.primary-button:hover:not(:disabled) {
- background: var(--accent-hover);
+@media (prefers-color-scheme: dark) {
+ body.auto-mode .toolbar-btn.primary {
+ box-shadow: 0 2px 0 rgba(0, 0, 0, .4);
+ }
}
-.primary-button:active:not(:disabled) {
- transform: scale(0.98);
+.toolbar-btn.ghost {
+ background: var(--ghost-bg);
+ border: 1.4px solid var(--ghost-border);
+ color: var(--ghost-text);
}
-/* Scanning state */
-.primary-button.scanning {
+.toolbar-btn.scanning {
position: relative;
color: transparent;
pointer-events: none;
}
-.primary-button.scanning svg {
- opacity: 0;
-}
-
-.primary-button.scanning::after {
+.toolbar-btn.scanning::after {
content: "";
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
- width: 16px;
- height: 16px;
- border: 2px solid rgba(255, 255, 255, 0.25);
+ width: 14px;
+ height: 14px;
+ border: 2px solid rgba(255, 255, 255, .25);
border-top-color: white;
border-radius: 50%;
animation: spin 0.8s cubic-bezier(0.4, 0.2, 0.2, 1) infinite;
}
-/* Action Buttons Container */
-.action-buttons-container {
- display: flex;
- gap: 8px;
- width: 100%;
- transform-origin: top;
- transition: transform 350ms cubic-bezier(0.4, 0, 0.2, 1),
- opacity 350ms cubic-bezier(0.4, 0, 0.2, 1);
-}
-
-/* Default hidden state for container */
-.results-section:not(.has-results) .action-buttons-container {
- transform: scaleY(0);
- opacity: 0;
- pointer-events: none;
- height: 0;
- margin: 0;
-}
-
-/* Visible state for container */
-.results-section.has-results .action-buttons-container {
- transform: scaleY(1);
- opacity: 1;
- pointer-events: auto;
- height: auto;
- margin-top: 8px;
-}
-
-/* Action button (Add to Calendar) */
-.action-button {
- flex: 1;
- background: var(--accent-primary);
- color: white;
- font-size: 13px;
- font-weight: 500;
- padding: 8px 14px;
- transition: background var(--transition-base), box-shadow var(--transition-base);
-}
-
-.action-button.secondary {
- background: var(--bg-secondary);
- color: var(--text);
- border: 1px solid var(--border);
+.toolbar-btn:disabled {
+ opacity: 0.4;
+ cursor: not-allowed;
}
-.action-button.secondary:hover:not(:disabled) {
- background: var(--bg);
- border-color: var(--accent-primary);
- color: var(--accent-primary);
- box-shadow: var(--shadow-sm);
+.toolbar-btn.scan-unavailable {
+ opacity: 0.4;
+ cursor: not-allowed;
}
-.action-button.hidden {
- display: none;
+/* ===========================================
+ Month section header (group inside the list)
+ =========================================== */
+.month-section-header {
+ font-weight: 800;
+ font-size: 19px;
+ letter-spacing: 1.5px;
+ color: var(--ink);
+ margin: 14px 0 4px;
+ padding: 0;
+ text-transform: uppercase;
}
-/* Default hidden state */
-.results-section:not(.has-results) .action-button {
- /* Handled by container now */
+.month-section-header:first-child {
+ margin-top: 2px;
}
-/* Visible state */
-.results-section.has-results .action-button {
- /* Handled by container now */
+.month-year {
+ color: var(--brand);
}
-.action-button:hover:not(:disabled) {
- background: var(--accent-hover);
- box-shadow: 0 4px 12px rgba(255, 107, 53, 0.3);
-}
+body.dark-mode .month-year { color: var(--brand-bright); }
-.action-button:active:not(:disabled) {
- box-shadow: 0 2px 4px rgba(255, 107, 53, 0.2);
+@media (prefers-color-scheme: dark) {
+ body.auto-mode .month-year { color: var(--brand-bright); }
}
-.action-button:disabled {
- background: var(--bg-secondary);
- color: var(--text-secondary);
- opacity: 0.5;
- cursor: not-allowed;
- box-shadow: none;
+/* ===========================================
+ Segmented toggle
+ =========================================== */
+.segment-toggle {
+ display: inline-flex;
+ margin: 12px 16px 0;
+ background: var(--tab-track);
+ border-radius: 16px;
+ padding: 3px;
}
-/* Toast notifications */
-#toast {
- position: relative;
- width: calc(100% - 32px);
- box-sizing: border-box;
- margin: 0 16px 12px;
- transform: translateY(6px);
- background: var(--bg);
- border: 1px solid var(--border);
- padding: 9px 11px;
- border-radius: 8px;
- box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08), 0 1px 3px rgba(0, 0, 0, 0.04);
- z-index: 9999;
- font-size: 12px;
- font-weight: 500;
- display: flex;
- align-items: center;
- gap: 8px;
- opacity: 0;
- pointer-events: none;
- transition: opacity 250ms cubic-bezier(0.4, 0, 0.2, 1),
- transform 250ms cubic-bezier(0.4, 0, 0.2, 1);
+.segment-pill {
+ font-weight: 700;
+ font-size: 11px;
+ letter-spacing: .8px;
+ text-transform: uppercase;
+ padding: 5px 14px;
+ border-radius: 13px;
+ cursor: pointer;
+ transition: all var(--transition-fast);
+ border: none;
+ background: transparent;
+ color: var(--tab-inactive-text);
+ font-family: 'Hanken Grotesk', sans-serif;
+}
+
+.segment-pill.active {
+ background: var(--tab-active);
+ color: var(--tab-active-text);
+ box-shadow: var(--tab-shadow);
+}
+
+/* ===========================================
+ Ruled event list
+ =========================================== */
+.ruled-list {
+ background-image: repeating-linear-gradient(180deg,
+ transparent 0, transparent 31px,
+ var(--ruled-line) 31px, var(--ruled-line) 32px);
+ background-attachment: local;
+ padding: 2px 16px 6px;
+ min-height: 120px;
+ max-height: 300px;
+ overflow-y: auto;
+ overflow-x: hidden;
}
-#toast.visible {
- opacity: 1;
- transform: translateY(0);
- pointer-events: auto;
+.ruled-list::-webkit-scrollbar {
+ width: 5px;
}
-#toast:not(.visible) {
- transform: translateY(6px);
+.ruled-list::-webkit-scrollbar-track {
+ background: transparent;
}
-.toast-content {
- display: flex;
- align-items: flex-start;
- gap: 8px;
- width: 100%;
- line-height: 1.35;
+.ruled-list::-webkit-scrollbar-thumb {
+ background: var(--ruled-line);
+ border-radius: 3px;
}
-.toast-icon {
- display: flex;
- align-items: center;
- justify-content: center;
- flex-shrink: 0;
+.ruled-list:hover::-webkit-scrollbar-thumb {
+ background: var(--ink-muted);
}
-.toast-icon svg {
- width: 13px;
- height: 13px;
+/* ===========================================
+ Event row — 3-column grid
+ =========================================== */
+@keyframes slideIn {
+ from { opacity: 0; transform: translateY(3px); }
+ to { opacity: 1; transform: translateY(0); }
}
-/* Success toast - subtle green tint */
-#toast.toast-success {
- background: rgba(16, 185, 129, 0.08);
- border-color: rgba(16, 185, 129, 0.2);
- color: #047857;
+.event-card {
+ display: grid;
+ grid-template-columns: 44px 1fr;
+ gap: 10px;
+ align-items: start;
+ padding: 9px 2px 7px;
+ position: relative;
+ animation: slideIn 250ms cubic-bezier(0.4, 0, 0.2, 1) both;
+ cursor: pointer;
+ border-radius: 3px;
+ transition: background var(--transition-fast);
}
-#toast.toast-success .toast-icon svg {
- color: #10b981;
+.event-card:hover {
+ background: rgba(0, 0, 0, .03);
}
-body.dark-mode #toast.toast-success,
-body.auto-mode #toast.toast-success {
- background: rgba(16, 185, 129, 0.12);
- border-color: rgba(16, 185, 129, 0.25);
- color: #6ee7b7;
+body.dark-mode .event-card:hover,
+body.auto-mode .event-card:hover {
+ background: rgba(255, 255, 255, .03);
}
-body.dark-mode #toast.toast-success .toast-icon svg,
-body.auto-mode #toast.toast-success .toast-icon svg {
- color: #34d399;
+@media (prefers-color-scheme: dark) {
+ body.auto-mode .event-card:hover { background: rgba(255, 255, 255, .03); }
}
-/* Error toast - subtle red tint */
-#toast.toast-error {
- background: rgba(239, 68, 68, 0.06);
- border-color: rgba(239, 68, 68, 0.15);
- color: #b91c1c;
+/* Past list */
+.past-list .event-card {
+ opacity: var(--past-opacity);
}
-#toast.toast-error .toast-icon svg {
- color: #ef4444;
+/* ---- Selection: highlighter wash ---- */
+:root {
+ --hl-wash: linear-gradient(178deg,
+ transparent 6%,
+ rgba(255, 160, 60, .55) 18%,
+ rgba(255, 160, 60, .55) 84%,
+ transparent 96%);
}
-body.dark-mode #toast.toast-error,
-body.auto-mode #toast.toast-error {
- background: rgba(239, 68, 68, 0.1);
- border-color: rgba(239, 68, 68, 0.2);
- color: #fca5a5;
+/* keep the chip + text above the wash */
+.event-card > * {
+ position: relative;
+ z-index: 1;
}
-body.dark-mode #toast.toast-error .toast-icon svg,
-body.auto-mode #toast.toast-error .toast-icon svg {
- color: #f87171;
+/* the highlighter streak */
+.event-card::before {
+ content: "";
+ position: absolute;
+ inset: 6px -6px;
+ z-index: 0;
+ border-radius: 4px;
+ background: var(--hl-wash);
+ transform: rotate(-0.4deg);
+ mix-blend-mode: multiply;
+ opacity: 0;
+ transition: opacity .12s ease;
+ pointer-events: none;
}
-/* Info toast - subtle blue tint (for future use) */
-#toast.toast-info {
- background: rgba(59, 130, 246, 0.06);
- border-color: rgba(59, 130, 246, 0.15);
- color: #1d4ed8;
-}
+.event-card.selected::before { opacity: 1; }
-#toast.toast-info .toast-icon svg {
- color: #3b82f6;
-}
+/* on a selected row, drop the per-title swipe (avoid double-marking) */
+.event-card.selected .ev-title-highlight { background: none; }
-body.dark-mode #toast.toast-info,
-body.auto-mode #toast.toast-info {
- background: rgba(59, 130, 246, 0.1);
- border-color: rgba(59, 130, 246, 0.2);
- color: #93c5fd;
-}
+/* deepen the handwritten meta so it reads over orange */
+.event-card.selected .ev-note { color: #7E3A0F !important; }
-body.dark-mode #toast.toast-info .toast-icon svg,
-body.auto-mode #toast.toast-info .toast-icon svg {
- color: #60a5fa;
+/* dark mode: multiply would muddy a dark page — switch to screen + lower alpha */
+body.dark-mode .event-card::before,
+body.auto-mode .event-card::before {
+ mix-blend-mode: screen;
+ background: linear-gradient(178deg,
+ transparent 6%,
+ rgba(255, 160, 60, .26) 18%,
+ rgba(255, 160, 60, .26) 84%,
+ transparent 96%);
}
-/* Events section styling */
-.events-section {
- margin-top: 0;
- flex: 0 1 auto;
- min-height: 0;
- display: flex;
- flex-direction: column;
+@media (prefers-color-scheme: dark) {
+ body.auto-mode .event-card::before {
+ mix-blend-mode: screen;
+ background: linear-gradient(178deg,
+ transparent 6%,
+ rgba(255, 160, 60, .26) 18%,
+ rgba(255, 160, 60, .26) 84%,
+ transparent 96%);
+ }
}
-.section-title {
- font-size: 11px;
- font-weight: 600;
- color: var(--text-secondary);
- margin: 8px 0 6px 0;
- padding: 0 4px;
- text-transform: uppercase;
- letter-spacing: 0.5px;
+/* ---- Date chip ---- */
+.date-chip {
+ width: 44px;
+ border-radius: 7px;
+ overflow: hidden;
+ border: 1px solid var(--chip-border);
+ box-shadow: var(--chip-shadow);
+ text-align: center;
flex-shrink: 0;
}
-/* Collapse section if no events - keep in grid but make it 0 height */
-.events-section:has(.events-list:empty) {
- min-height: 0;
- height: 0;
- overflow: hidden;
- margin: 0;
- padding: 0;
- opacity: 0;
- flex: 0 0 0;
+.date-chip-month {
+ color: #fff;
+ font-weight: 800;
+ font-size: 9px;
+ letter-spacing: 1px;
+ padding: 3px 0;
+ text-transform: uppercase;
}
-.events-section:has(.events-list:empty) .section-title {
- display: none;
+.date-chip-day {
+ background: var(--chip-cell-bg);
+ color: var(--chip-cell-text);
+ font-weight: 800;
+ font-size: 18px;
+ line-height: 1.2;
+ padding: 3px 0;
}
-.events-section:has(.events-list:empty) .events-list {
- display: none;
+/* ---- Event content ---- */
+.ev-content {
+ min-width: 0;
}
-/* Events list scrolling container */
-.events-list {
- position: relative;
- max-height: 180px;
- overflow-y: auto;
- overflow-x: hidden;
- padding: 4px 0;
- flex: 0 1 auto;
- min-height: 0;
+.ev-title {
+ font-weight: 600;
+ font-size: 15px;
+ color: var(--ink);
+ line-height: 1.25;
}
-/* Custom scrollbar for events list */
-.events-list::-webkit-scrollbar {
- width: 8px;
+.ev-title-highlight {
+ padding: 0 2px;
}
-.events-list::-webkit-scrollbar-track {
- background: transparent;
+.ev-note {
+ font-family: 'Kalam', cursive;
+ font-size: 14px;
+ line-height: 1.15;
+ margin-top: 3px;
}
-.events-list::-webkit-scrollbar-thumb {
- background: var(--border);
- border-radius: 4px;
- transition: background var(--transition-base);
+.ev-time {
+ font-weight: 700;
}
-.events-list:hover::-webkit-scrollbar-thumb {
- background: var(--text-secondary);
+.ev-badge {
+ display: inline-block;
+ font-size: 9px;
+ font-weight: 700;
+ letter-spacing: .6px;
+ text-transform: uppercase;
+ color: var(--badge-text);
+ border: 1px solid var(--badge-border);
+ border-radius: 10px;
+ padding: 2px 7px;
+ margin-top: 6px;
}
-.events-list::-webkit-scrollbar-thumb:hover {
- background: var(--accent-primary);
+/* ===========================================
+ Week strip
+ =========================================== */
+.week-strip {
+ display: flex;
+ justify-content: space-between;
+ gap: 4px;
+ padding: 8px 16px 4px;
+ border-top: var(--week-divider);
}
-/* Skeleton loading cards */
-@keyframes shimmer {
- 0% {
- transform: translateX(-100%);
- }
-
- 100% {
- transform: translateX(100%);
- }
+.week-day {
+ text-align: center;
+ flex: 1;
}
-@keyframes skeletonFadeIn {
- from {
- opacity: 0;
- transform: translateY(8px) scale(0.98);
- }
-
- to {
- opacity: 1;
- transform: translateY(0) scale(1);
- }
+.week-day-letter {
+ font-size: 9px;
+ font-weight: 700;
+ color: var(--weekday-text);
+ letter-spacing: .5px;
}
-.skeleton-card {
- background: var(--bg);
- border: 1px solid var(--border);
- border-radius: 6px;
- padding: 11px 12px;
- margin-bottom: 6px;
- position: relative;
- overflow: hidden;
- animation: skeletonFadeIn 400ms cubic-bezier(0.4, 0, 0.2, 1);
- animation-fill-mode: both;
+.week-day-num {
+ font-size: 13px;
+ font-weight: 700;
+ color: var(--ink);
+ margin-top: 2px;
}
-.skeleton-card::after {
- content: "";
- position: absolute;
- top: 0;
- left: -100%;
- width: 200%;
- height: 100%;
- background: linear-gradient(90deg,
- transparent,
- var(--accent-lighter),
- transparent);
- animation: shimmer 1.8s linear infinite;
+.week-day-num.sunday {
+ color: var(--sunday-text);
}
-.skeleton-content {
+.week-dot-container {
+ height: 6px;
+ margin-top: 3px;
display: flex;
- flex-direction: column;
- gap: 6px;
-}
-
-.skeleton-line {
- height: 12px;
- background: var(--border);
- border-radius: 4px;
- position: relative;
+ justify-content: center;
}
-/* Match event title - centered, can be 2 lines at 13px with line-height 1.4 */
-.skeleton-line.title {
- width: 40%;
- height: calc(13px * 1.4);
- margin: 0 auto;
+.week-dot {
+ width: 6px;
+ height: 6px;
+ border-radius: 50%;
}
-/* Match event-bottom layout */
-.skeleton-bottom {
+/* ===========================================
+ Footer
+ =========================================== */
+.planner-footer {
display: flex;
- flex-direction: row;
- gap: 12px;
+ align-items: center;
justify-content: space-between;
- align-items: flex-end;
-}
-
-/* Match event-times */
-.skeleton-times {
- display: flex;
- flex-direction: column;
- gap: 2px;
-}
-
-/* Match event-time - 11px with line-height 1.3 */
-.skeleton-line.time {
- width: 100px;
- height: calc(11px * 1.3);
+ padding: 11px 16px;
+ background: var(--footer-bg);
+ border-top: 1px solid var(--footer-border);
}
-/* Match event-location */
-.skeleton-line.location {
- width: 80px;
- height: calc(11px * 1.3);
- text-align: right;
+.footer-selected {
+ font-family: 'Kalam', cursive;
+ font-size: 15px;
+ color: var(--footer-text);
}
-/* Event cards */
-@keyframes slideIn {
- from {
- opacity: 0;
- transform: translateY(8px) scale(0.98);
- }
-
- to {
- opacity: 1;
- transform: translateY(0) scale(1);
- }
+.footer-actions {
+ display: flex;
+ align-items: center;
+ gap: 10px;
}
-.event-card {
+.cta-btn {
+ background: var(--brand);
+ color: #fff;
+ font-weight: 700;
+ font-size: 13px;
+ padding: 9px 15px;
+ border-radius: 20px;
+ border: none;
+ box-shadow: var(--cta-shadow);
cursor: pointer;
- background: var(--bg);
- border: 1px solid var(--border);
- border-radius: 6px;
- padding: 11px 12px;
- margin-bottom: 6px;
- transition: all var(--transition-base);
- animation: slideIn 350ms cubic-bezier(0.4, 0, 0.2, 1);
- animation-fill-mode: both;
- position: relative;
+ font-family: 'Hanken Grotesk', sans-serif;
+ transition: all var(--transition-fast);
}
-.event-card:last-child {
- margin-bottom: 0;
+.cta-btn:hover {
+ filter: brightness(1.08);
+ transform: translateY(-1px);
}
-.event-card:nth-child(1) {
- animation-delay: 0ms;
+.cta-btn:active {
+ transform: translateY(0);
}
-.event-card:nth-child(2) {
- animation-delay: 30ms;
+.cta-btn:disabled {
+ opacity: 0.5;
+ cursor: not-allowed;
+ transform: none;
}
-.event-card:nth-child(3) {
- animation-delay: 60ms;
+/* ===========================================
+ Results container — visibility states
+ =========================================== */
+.results-section {
+ display: none;
}
-.event-card:nth-child(4) {
- animation-delay: 90ms;
+.results-section.open {
+ display: block;
}
-.event-card:nth-child(5) {
- animation-delay: 120ms;
+.results-section:not(.has-results) .planner-footer {
+ display: none;
}
-.event-card:nth-child(6) {
- animation-delay: 150ms;
+.results-section:not(.has-results) .week-strip {
+ display: none;
}
-.event-card:nth-child(7) {
- animation-delay: 180ms;
+.results-section.quota-exceeded .ruled-list,
+.results-section.quota-exceeded .planner-footer,
+.results-section.quota-exceeded .week-strip,
+.results-section.quota-exceeded .segment-toggle,
+.results-section.quota-exceeded .month-line {
+ display: none;
}
-.event-card:nth-child(8) {
- animation-delay: 210ms;
+/* ===========================================
+ Skeleton loading
+ =========================================== */
+@keyframes shimmer {
+ 0% { transform: translateX(-100%); }
+ 100% { transform: translateX(100%); }
}
-.event-card:hover {
- border-color: var(--accent-primary);
- transform: translateY(-2px);
- box-shadow: var(--shadow-md);
- z-index: 1;
+@keyframes skeletonFadeIn {
+ from { opacity: 0; transform: translateY(4px); }
+ to { opacity: 1; transform: translateY(0); }
}
-.event-card.selected {
- background: var(--accent-lighter);
- border-color: var(--accent-primary);
- border-left-width: 4px;
- padding-left: 9px;
+.skeleton-row {
+ display: grid;
+ grid-template-columns: 20px 44px 1fr;
+ gap: 10px;
+ align-items: start;
+ padding: 9px 2px 7px;
+ animation: skeletonFadeIn 400ms cubic-bezier(0.4, 0, 0.2, 1) both;
}
-.event-card.selected:hover {
- transform: translateX(1px);
+.skeleton-checkbox {
+ width: 20px;
+ height: 20px;
+ border-radius: 5px;
+ background: var(--ruled-line);
}
-.event-card:focus-visible {
- outline: 2px solid var(--accent-primary);
- outline-offset: 2px;
+.skeleton-chip {
+ width: 44px;
+ height: 44px;
+ border-radius: 7px;
+ background: var(--ruled-line);
}
-.event-row {
+.skeleton-content {
display: flex;
flex-direction: column;
gap: 6px;
+ padding-top: 2px;
}
-.event-bottom {
- display: flex;
- flex-direction: row;
- gap: 12px;
- align-items: flex-end;
- justify-content: space-between;
-}
-
-.event-times {
- display: flex;
- flex-direction: column;
- gap: 2px;
-}
-
-.event-location {
- font-size: 11px;
- color: var(--text-secondary);
- text-align: right;
- line-height: 1.3;
- max-width: 50%;
- overflow: hidden;
- text-overflow: ellipsis;
- display: -webkit-box;
- -webkit-line-clamp: 2;
- line-clamp: 2;
- -webkit-box-orient: vertical;
- min-height: calc(11px * 1.3 * 2);
-}
-
-.event-title {
- font-size: 13px;
- font-weight: 500;
- margin: 0;
- color: var(--text);
- line-height: 1.4;
+.skeleton-line {
+ background: var(--ruled-line);
+ border-radius: 3px;
+ position: relative;
overflow: hidden;
- text-overflow: ellipsis;
- display: -webkit-box;
- -webkit-line-clamp: 2;
- line-clamp: 2;
- -webkit-box-orient: vertical;
- text-align: center;
}
-.event-time {
- font-size: 11px;
- color: var(--text-secondary);
- line-height: 1.3;
- font-weight: 400;
- display: flex;
- align-items: center;
- gap: 4px;
-}
-
-.recurrence-icon {
- color: var(--text-secondary);
- opacity: 0.8;
- flex-shrink: 0;
-}
-
-.event-time.start {
- color: var(--accent-date);
+.skeleton-line::after {
+ content: "";
+ position: absolute;
+ top: 0;
+ left: -100%;
+ width: 200%;
+ height: 100%;
+ background: linear-gradient(90deg, transparent, rgba(255, 255, 255, .15), transparent);
+ animation: shimmer 1.8s linear infinite;
}
-.event-time.end {
- color: var(--text-secondary);
+.skeleton-line.title {
+ width: 75%;
+ height: 14px;
}
-/* Header buttons container */
-.header-buttons {
- display: flex;
- gap: 8px;
+.skeleton-line.note {
+ width: 55%;
+ height: 12px;
}
-/* Custom Context Section */
+/* ===========================================
+ Custom Context Section
+ =========================================== */
.custom-context-section {
- padding: 12px 16px;
+ padding: 16px;
display: flex;
flex-direction: column;
gap: 12px;
- transition: all 300ms ease;
}
-
-/* Custom Input Wrapper */
.custom-input-wrapper {
position: relative;
width: 100%;
@@ -768,12 +665,11 @@ body.auto-mode #toast.toast-info .toast-icon svg {
min-height: 100px;
padding: 12px;
padding-bottom: 40px;
- /* Make room for the button */
- border: 1px solid var(--border);
+ border: 1px solid var(--ruled-line);
border-radius: 8px;
- background: var(--bg-secondary);
- color: var(--text);
- font-family: inherit;
+ background: var(--paper);
+ color: var(--ink);
+ font-family: 'Hanken Grotesk', sans-serif;
font-size: 13px;
resize: none;
transition: border-color 200ms ease;
@@ -781,12 +677,9 @@ body.auto-mode #toast.toast-info .toast-icon svg {
#customInput:focus {
outline: none;
- border-color: var(--accent-primary);
- background: var(--bg);
+ border-color: var(--brand);
}
-
-/* File Input Styles */
.file-input-container {
position: absolute;
bottom: 8px;
@@ -807,31 +700,18 @@ body.auto-mode #toast.toast-info .toast-icon svg {
width: 32px;
height: 32px;
border-radius: 6px;
- color: var(--text-secondary);
+ color: var(--ink-muted);
cursor: pointer;
- transition: all var(--transition-base);
background: transparent;
border: 1px solid transparent;
- /* Prevent layout shift on focus */
+ transition: all var(--transition-base);
}
.file-input-label-icon:hover {
- background: var(--bg-hover);
- color: var(--accent-primary);
+ background: var(--tab-track);
+ color: var(--brand);
}
-.file-input-label-icon svg {
- width: 18px;
- height: 18px;
-}
-
-.file-count {
- font-size: 11px;
- color: var(--text-secondary);
- pointer-events: none;
-}
-
-/* Image Preview Styles */
.image-preview-container {
display: flex;
flex-wrap: wrap;
@@ -845,7 +725,7 @@ body.auto-mode #toast.toast-info .toast-icon svg {
height: 60px;
border-radius: 6px;
overflow: hidden;
- border: 1px solid var(--border);
+ border: 1px solid var(--ruled-line);
}
.image-preview-img {
@@ -869,22 +749,53 @@ body.auto-mode #toast.toast-info .toast-icon svg {
border: none;
padding: 0;
color: white;
- transition: background 0.2s;
}
-.image-remove-btn:hover {
- background: rgba(0, 0, 0, 0.8);
+.scan-media-btn {
+ background: var(--brand);
+ color: #fff;
+ font-weight: 600;
+ font-size: 13px;
+ padding: 9px 15px;
+ border-radius: 20px;
+ border: none;
+ box-shadow: var(--cta-shadow);
+ cursor: pointer;
+ font-family: 'Hanken Grotesk', sans-serif;
}
-.image-remove-btn svg {
- width: 10px;
- height: 10px;
+.scan-media-btn:hover {
+ filter: brightness(1.08);
}
-/* ============================================
- Quota Exceeded Panel
- ============================================ */
+.scan-media-btn:disabled {
+ opacity: 0.5;
+ cursor: not-allowed;
+}
+
+.scan-media-btn.scanning {
+ position: relative;
+ color: transparent;
+ pointer-events: none;
+}
+
+.scan-media-btn.scanning::after {
+ content: "";
+ position: absolute;
+ top: 50%;
+ left: 50%;
+ transform: translate(-50%, -50%);
+ width: 14px;
+ height: 14px;
+ border: 2px solid rgba(255, 255, 255, .25);
+ border-top-color: white;
+ border-radius: 50%;
+ animation: spin 0.8s cubic-bezier(0.4, 0.2, 0.2, 1) infinite;
+}
+/* ===========================================
+ Quota Exceeded Panel
+ =========================================== */
.quota-exceeded-panel {
display: flex;
flex-direction: column;
@@ -900,15 +811,8 @@ body.auto-mode #toast.toast-info .toast-icon svg {
}
@keyframes quotaFadeIn {
- from {
- opacity: 0;
- transform: translateY(8px);
- }
-
- to {
- opacity: 1;
- transform: translateY(0);
- }
+ from { opacity: 0; transform: translateY(8px); }
+ to { opacity: 1; transform: translateY(0); }
}
.quota-icon {
@@ -918,39 +822,21 @@ body.auto-mode #toast.toast-info .toast-icon svg {
width: 56px;
height: 56px;
border-radius: 50%;
- background: linear-gradient(135deg, rgba(255, 107, 53, 0.15) 0%, rgba(255, 87, 34, 0.1) 100%);
+ background: linear-gradient(135deg, rgba(244, 105, 31, .15) 0%, rgba(244, 105, 31, .1) 100%);
margin-bottom: 14px;
- color: var(--accent-primary);
- position: relative;
-}
-
-.quota-icon::before {
- content: "";
- position: absolute;
- inset: -2px;
- border-radius: 50%;
- background: linear-gradient(135deg, rgba(255, 107, 53, 0.25) 0%, transparent 60%);
- z-index: -1;
- opacity: 0.6;
-}
-
-body.dark-mode .quota-icon,
-body.auto-mode .quota-icon {
- background: linear-gradient(135deg, rgba(255, 107, 53, 0.12) 0%, rgba(255, 87, 34, 0.08) 100%);
- color: var(--accent-primary);
+ color: var(--brand);
}
.quota-title {
font-size: 15px;
font-weight: 600;
- color: var(--text);
+ color: var(--ink);
margin: 0 0 6px 0;
- letter-spacing: -0.01em;
}
.quota-message {
font-size: 12px;
- color: var(--text-secondary);
+ color: var(--ink-muted);
margin: 0 0 16px 0;
line-height: 1.5;
max-width: 240px;
@@ -964,37 +850,222 @@ body.auto-mode .quota-icon {
font-size: 12px;
font-weight: 500;
background: transparent;
- color: var(--accent-primary);
- border: 1px solid var(--accent-primary);
+ color: var(--brand);
+ border: 1px solid var(--brand);
border-radius: 6px;
cursor: pointer;
+ font-family: 'Hanken Grotesk', sans-serif;
transition: all var(--transition-base);
}
.quota-action-btn:hover {
- background: var(--accent-primary);
+ background: var(--brand);
color: white;
- box-shadow: 0 2px 8px rgba(255, 107, 53, 0.25);
}
-.quota-action-btn:active {
- transform: scale(0.98);
+/* ===========================================
+ Toast notifications
+ =========================================== */
+#toast {
+ position: fixed;
+ bottom: 12px;
+ left: 50%;
+ transform: translateX(-50%) translateY(6px);
+ width: calc(100% - 48px);
+ box-sizing: border-box;
+ background: var(--paper);
+ border: 1px solid var(--ruled-line);
+ padding: 9px 11px;
+ border-radius: 8px;
+ box-shadow: 0 4px 12px rgba(0, 0, 0, .12);
+ z-index: 9999;
+ font-size: 12px;
+ font-weight: 500;
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ opacity: 0;
+ pointer-events: none;
+ transition: opacity 250ms ease, transform 250ms ease;
}
-.quota-action-btn svg {
+#toast.visible {
+ opacity: 1;
+ transform: translateX(-50%) translateY(0);
+ pointer-events: auto;
+}
+
+.toast-content {
+ display: flex;
+ align-items: flex-start;
+ gap: 8px;
+ width: 100%;
+ line-height: 1.35;
+}
+
+.toast-icon {
+ display: flex;
+ align-items: center;
+ justify-content: center;
flex-shrink: 0;
}
-/* Quota state for results section */
-.results-section.quota-exceeded {
- grid-template-rows: auto 0fr 0fr;
+.toast-icon svg {
+ width: 13px;
+ height: 13px;
}
-.results-section.quota-exceeded .events-section {
- display: none;
+#toast.toast-success {
+ background: rgba(16, 185, 129, 0.08);
+ border-color: rgba(16, 185, 129, 0.2);
+ color: #047857;
}
-.results-section.quota-exceeded .action-buttons-container {
- display: none;
+#toast.toast-success .toast-icon svg { color: #10b981; }
+
+body.dark-mode #toast.toast-success,
+body.auto-mode #toast.toast-success {
+ background: rgba(16, 185, 129, 0.12);
+ border-color: rgba(16, 185, 129, 0.25);
+ color: #6ee7b7;
+}
+
+#toast.toast-error {
+ background: rgba(239, 68, 68, 0.06);
+ border-color: rgba(239, 68, 68, 0.15);
+ color: #b91c1c;
}
+body.dark-mode #toast.toast-error,
+body.auto-mode #toast.toast-error {
+ background: rgba(239, 68, 68, 0.1);
+ border-color: rgba(239, 68, 68, 0.2);
+ color: #fca5a5;
+}
+
+#toast.toast-info {
+ background: rgba(59, 130, 246, 0.06);
+ border-color: rgba(59, 130, 246, 0.15);
+ color: #1d4ed8;
+}
+
+body.dark-mode #toast.toast-info,
+body.auto-mode #toast.toast-info {
+ background: rgba(59, 130, 246, 0.1);
+ border-color: rgba(59, 130, 246, 0.2);
+ color: #93c5fd;
+}
+
+/* ===========================================
+ Empty state — idle view before any scan
+ =========================================== */
+.empty-state {
+ position: relative;
+}
+
+.empty-body {
+ position: relative;
+ padding: 6px 22px 0;
+ background-image: repeating-linear-gradient(180deg,
+ transparent 0, transparent 33px,
+ var(--ruled-line) 33px, var(--ruled-line) 34px);
+}
+
+.empty-doodle {
+ position: absolute;
+ top: 2px;
+ right: 30px;
+ width: 104px;
+ height: 80px;
+ overflow: visible;
+ pointer-events: none;
+}
+
+.empty-hero {
+ text-align: center;
+ padding: 36px 4px 30px;
+}
+
+.empty-note {
+ margin: 0;
+ font-family: 'Kalam', cursive;
+ font-size: 17px;
+ color: #B6803F;
+ transform: rotate(-2deg);
+}
+
+.empty-title {
+ margin: 8px 0 0;
+ font-weight: 800;
+ font-size: 23px;
+ color: var(--ink);
+}
+
+.empty-sub {
+ margin: 9px auto 0;
+ max-width: 280px;
+ font-weight: 500;
+ font-size: 14.5px;
+ line-height: 1.5;
+ color: var(--ink-muted);
+}
+
+/* primary CTA — hard pop-shadow sticker */
+.empty-cta {
+ display: inline-flex;
+ align-items: center;
+ gap: 8px;
+ margin-top: 20px;
+ padding: 12px 22px;
+ border-radius: 22px;
+ background: var(--brand);
+ color: #fff;
+ font-weight: 700;
+ font-size: 15px;
+ font-family: inherit;
+ border: 3px solid #2A333A;
+ cursor: pointer;
+ box-shadow: 4px 4px 0 #2A333A;
+ transition: transform .1s ease, box-shadow .1s ease;
+}
+
+.empty-cta:hover {
+ transform: translate(-1px, -1px);
+ box-shadow: 5px 5px 0 #2A333A;
+}
+
+.empty-cta:active {
+ transform: translate(2px, 2px);
+ box-shadow: 2px 2px 0 #2A333A;
+}
+
+.empty-or {
+ margin: 14px 0 0;
+ font-size: 13px;
+ color: var(--ink-muted);
+}
+
+.empty-link {
+ background: none;
+ border: 0;
+ padding: 0;
+ cursor: pointer;
+ font: inherit;
+ font-weight: 600;
+ color: var(--ink);
+ border-bottom: 1.5px solid var(--ruled-line);
+}
+
+.empty-foot {
+ padding: 11px 18px;
+ text-align: center;
+ border-top: 1px solid var(--ruled-line);
+ font-size: 11px;
+ color: var(--ink-muted);
+}
+
+/* hide the empty state once results are shown or the paste panel opens */
+.planner-page:has(#results.open) #emptyState,
+.planner-page:has(#customContextSection:not(.hidden)) #emptyState {
+ display: none;
+}
diff --git a/tests/popupUi.test.js b/tests/popupUi.test.js
index 3a27d59..5ed62b8 100644
--- a/tests/popupUi.test.js
+++ b/tests/popupUi.test.js
@@ -2,30 +2,25 @@ import test from "node:test";
import assert from "node:assert/strict";
import fs from "node:fs";
-test("scan button starts disabled until page availability is known", () => {
+test("scan button exists in toolbar", () => {
const html = fs.readFileSync(new URL("../src/ui/index.html", import.meta.url), "utf8");
- const scanSectionTag = html.match(/
]*>/)?.[0];
const scanButtonTag = html.match(/