Files
platform/frontend-v2/src/routes/(app)/trips/trip/+page.svelte
Yusuf Suleman 8275f3a71b feat: brain service — self-contained second brain knowledge manager
Full backend service with:
- FastAPI REST API with CRUD, search, reprocess endpoints
- PostgreSQL + pgvector for items and semantic search
- Redis + RQ for background job processing
- Meilisearch for fast keyword/filter search
- Browserless/Chrome for JS rendering and screenshots
- OpenAI structured output for AI classification
- Local file storage with S3-ready abstraction
- Gateway auth via X-Gateway-User-Id header
- Own docker-compose stack (6 containers)

Classification: fixed folders (Home/Family/Work/Travel/Knowledge/Faith/Projects)
and fixed tags (28 predefined). AI assigns exactly 1 folder, 2-3 tags, title,
summary, and confidence score per item.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-01 11:48:29 -05:00

1049 lines
56 KiB
Svelte
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<script lang="ts">
import { onMount } from 'svelte';
import { page } from '$app/state';
import AtelierTripDetailPage from '$lib/pages/trips/AtelierTripDetailPage.svelte';
import ItemModal from '$lib/components/trips/ItemModal.svelte';
import TripEditModal from '$lib/components/trips/TripEditModal.svelte';
import ImageUpload from '$lib/components/trips/ImageUpload.svelte';
let { data } = $props();
// ── Types ──
interface ItineraryEvent {
time: string; name: string; description: string;
category: string; mapsLink?: boolean; stayingOvernight?: boolean;
image?: string; cost?: { points?: number; cash?: number };
flightNumber?: string; reservation?: string;
lat?: number | null; lng?: number | null;
entityId?: string;
entityType?: 'transportation' | 'lodging' | 'location';
rawData?: any;
}
interface Day { day: number; date: string; description: string; weather?: { temp: string; icon: string }; events: ItineraryEvent[]; }
interface DayStory { day: number; text: string; photos: string[]; }
// ── State ──
let trip = $state({ name: '', dates: '', away: '', duration: '' });
let coverImages = $state<string[]>(['']);
let itinerary = $state<Day[]>([]);
let unscheduled = $state<{ name: string; description: string }[]>([]);
let notes = $state<{ id: string; date: string; content: string }[]>([]);
let highlights = $state<{ label: string; value: string; icon: string }[]>([]);
let dayStories = $state<DayStory[]>([]);
let loading = $state(true);
let mapLocations = $state<{ name: string; lat: number; lng: number; type: string }[]>([]);
// ── Modals ──
let itemModalOpen = $state(false);
let itemModalType = $state<'transportation' | 'lodging' | 'location' | 'note'>('location');
let itemModalEdit = $state<any>(null);
let tripEditOpen = $state(false);
function openCreateModal(type: 'transportation' | 'lodging' | 'location' | 'note') {
itemModalType = type;
itemModalEdit = null;
itemModalOpen = true;
fabOpen = false;
}
let currentCoverIdx = $state(0);
let activeView = $state<'map' | 'ai'>('map');
let expandedDays = $state<Set<number>>(new Set([1, 2, 3]));
let aiPrompt = $state('');
let shareModalOpen = $state(false);
let fabOpen = $state(false);
let shareMode = $state(false);
async function deleteTrip() {
const tripId = page.url.searchParams.get('id');
if (!tripId) return;
if (!confirm('Delete this trip and all its data? This cannot be undone.')) return;
fabOpen = false;
try {
const res = await fetch(`/api/trips/trip/delete`, {
method: 'POST', credentials: 'include',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ id: tripId })
});
if (res.ok) {
window.location.href = '/trips';
}
} catch { /* silent */ }
}
// ── Edit/Delete items ──
let editingEvent = $state<ItineraryEvent | null>(null);
let editFields = $state<Record<string, string>>({});
let editSaving = $state(false);
let rawTripData = $state<any>(null);
function openEdit(evt: ItineraryEvent) {
if (!evt.entityId || !evt.entityType) return;
itemModalType = evt.entityType;
itemModalEdit = evt.rawData || {};
itemModalOpen = true;
}
function closeEdit() { editingEvent = null; editFields = {}; }
async function saveEdit() {
if (!editingEvent?.entityId || !editingEvent?.entityType) return;
editSaving = true;
const type = editingEvent.entityType;
const updates: Record<string, any> = { id: editingEvent.entityId, trip_id: page.url.searchParams.get('id') };
if (type === 'transportation') {
updates.name = editFields.name;
updates.description = editFields.description;
updates.date = editFields.date;
updates.flight_number = editFields.flight_number;
updates.cost_points = Number(editFields.cost_points) || 0;
updates.cost_cash = Number(editFields.cost_cash) || 0;
} else if (type === 'lodging') {
updates.name = editFields.name;
updates.description = editFields.description;
updates.check_in = editFields.date;
updates.reservation_number = editFields.reservation_number;
updates.cost_points = Number(editFields.cost_points) || 0;
updates.cost_cash = Number(editFields.cost_cash) || 0;
} else if (type === 'location') {
updates.name = editFields.name;
updates.description = editFields.description;
updates.visit_date = editFields.date;
updates.cost_points = Number(editFields.cost_points) || 0;
updates.cost_cash = Number(editFields.cost_cash) || 0;
}
try {
await fetch(`/api/trips/${type}/update`, {
method: 'POST', credentials: 'include',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(updates)
});
closeEdit();
reloadTrip();
} catch { /* silent */ }
finally { editSaving = false; }
}
async function deleteItem() {
if (!editingEvent?.entityId || !editingEvent?.entityType) return;
if (!confirm(`Delete "${editingEvent.name}"?`)) return;
editSaving = true;
try {
await fetch(`/api/trips/${editingEvent.entityType}/delete`, {
method: 'POST', credentials: 'include',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ id: editingEvent.entityId })
});
closeEdit();
reloadTrip();
} catch { /* silent */ }
finally { editSaving = false; }
}
async function deleteNote(noteId: string) {
if (!confirm('Delete this note?')) return;
try {
await fetch('/api/trips/note/delete', {
method: 'POST', credentials: 'include',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ id: noteId })
});
reloadTrip();
} catch { /* silent */ }
}
async function reloadTrip() {
const tripId = page.url.searchParams.get('id');
if (!tripId) return;
try {
const res = await fetch(`/api/trips/trip/${tripId}`, { credentials: 'include' });
if (res.ok) {
const data = await res.json();
rawTripData = data;
buildItinerary(data);
}
} catch { /* silent */ }
}
function buildItinerary(data: any) {
// Same logic as onMount — extracted for reuse
trip = {
name: data.name || '',
dates: formatDateRange(data.start_date, data.end_date),
away: daysAway(data.start_date),
duration: durationDays(data.start_date, data.end_date)
};
const allImages: string[] = [];
if (data.cover_image) allImages.push(data.cover_image);
for (const img of data.images || []) {
const path = img.file_path || '';
if (path && !allImages.includes(`/images/${path}`)) allImages.push(`/images/${path}`);
}
coverImages = allImages.length > 0 ? allImages : [''];
const startDate = new Date(data.start_date + 'T00:00:00');
const endDate = new Date(data.end_date + 'T00:00:00');
const totalDays = Math.ceil((endDate.getTime() - startDate.getTime()) / 86400000) + 1;
const dayMap: Record<string, ItineraryEvent[]> = {};
for (let i = 0; i < totalDays; i++) {
const d = new Date(startDate);
d.setDate(d.getDate() + i);
dayMap[d.toISOString().slice(0, 10)] = [];
}
// Helper to attach images/docs to each entity
const allImgs = data.images || [];
const allDocs = data.documents || [];
function enrichRaw(item: any, type: string) {
const typeMap: Record<string, string> = { transportation: 'transportation', lodging: 'lodging', location: 'location' };
const eType = typeMap[type] || type;
return {
...item,
images: item.images?.length ? item.images : allImgs.filter((i: any) => i.entity_type === eType && i.entity_id === item.id),
documents: item.documents?.length ? item.documents : allDocs.filter((d: any) => d.entity_type === eType && d.entity_id === item.id)
};
}
for (const t of data.transportations || []) {
const date = (t.date || '').slice(0, 10);
if (!dayMap[date]) dayMap[date] = [];
dayMap[date].push({
time: formatTime(t.date), name: t.name || `${t.from_location}${t.to_location}`,
description: t.description || (t.from_location && t.to_location ? `${t.from_location}${t.to_location}` : ''),
category: mapCategory(t, 'transportation'), flightNumber: t.flight_number || '',
cost: { points: t.cost_points || 0, cash: t.cost_cash || 0 },
mapsLink: !!(t.from_lat || t.to_lat), lat: t.from_lat || t.to_lat, lng: t.from_lng || t.to_lng,
image: entityImage(data.images, t.id, t.images),
entityId: t.id, entityType: 'transportation', rawData: enrichRaw(t, 'transportation')
});
}
for (const l of data.lodging || []) {
const date = (l.check_in || '').slice(0, 10);
if (!dayMap[date]) dayMap[date] = [];
dayMap[date].push({
time: formatTime(l.check_in), name: l.name,
description: l.description || `${l.type || 'Hotel'} · ${l.location || ''}`.trim(),
category: 'Hotel', stayingOvernight: true, mapsLink: !!(l.latitude), lat: l.latitude, lng: l.longitude,
cost: { points: l.cost_points || 0, cash: l.cost_cash || 0 },
reservation: l.reservation_number || '', image: entityImage(data.images, l.id, l.images),
entityId: l.id, entityType: 'lodging', rawData: enrichRaw(l, 'lodging')
});
}
for (const loc of data.locations || []) {
const date = loc.visit_date || (loc.start_time || '').slice(0, 10);
if (!dayMap[date]) dayMap[date] = [];
let desc = loc.description || '';
if (loc.hike_distance) desc = `${loc.hike_distance} · ${loc.hike_difficulty || ''} · ${loc.hike_time || ''}`.trim();
dayMap[date].push({
time: formatTime(loc.start_time), name: loc.name, description: desc,
category: mapCategory(loc, 'location'), mapsLink: !!(loc.latitude), lat: loc.latitude, lng: loc.longitude,
cost: { points: loc.cost_points || 0, cash: loc.cost_cash || 0 },
image: entityImage(data.images, loc.id, loc.images),
entityId: loc.id, entityType: 'location', rawData: enrichRaw(loc, 'location')
});
}
const days: Day[] = [];
let dayNum = 1;
for (const [date, events] of Object.entries(dayMap).sort()) {
events.sort((a, b) => (a.time || '').localeCompare(b.time || ''));
days.push({ day: dayNum, date: formatDayDate(date), description: events.length > 0 ? events[0].name : '', events });
dayNum++;
}
itinerary = days;
notes = (data.notes || []).map((n: any) => ({
id: n.id,
date: n.date ? new Date(n.date + 'T00:00:00').toLocaleDateString('en-US', { month: 'short', day: 'numeric' }) : '',
content: n.content || n.name || ''
}));
const unschedItems: typeof unscheduled = [];
for (const loc of data.locations || []) {
if (!loc.visit_date && !loc.start_time) unschedItems.push({ name: loc.name, description: loc.description || '' });
}
unscheduled = unschedItems;
// Collect map pins
const pins: typeof mapLocations = [];
for (const t of data.transportations || []) {
if (t.from_lat && t.from_lng) pins.push({ name: t.from_location || t.name, lat: t.from_lat, lng: t.from_lng, type: 'transport' });
if (t.to_lat && t.to_lng) pins.push({ name: t.to_location || t.name, lat: t.to_lat, lng: t.to_lng, type: 'transport' });
}
for (const l of data.lodging || []) {
if (l.latitude && l.longitude) pins.push({ name: l.name, lat: l.latitude, lng: l.longitude, type: 'hotel' });
}
for (const loc of data.locations || []) {
if (loc.latitude && loc.longitude) pins.push({ name: loc.name, lat: loc.latitude, lng: loc.longitude, type: loc.category || 'activity' });
}
mapLocations = pins;
}
const isShareView = page.url.searchParams.get('share') === 'true';
if (isShareView) shareMode = true;
function nextCover() { currentCoverIdx = (currentCoverIdx + 1) % coverImages.length; }
function prevCover() { currentCoverIdx = (currentCoverIdx - 1 + coverImages.length) % coverImages.length; }
function toggleDay(day: number) {
const next = new Set(expandedDays);
if (next.has(day)) next.delete(day); else next.add(day);
expandedDays = next;
}
function getDayStory(dayNum: number): DayStory | undefined {
return dayStories.find(s => s.day === dayNum);
}
const tripExpenses = $derived({
points: itinerary.flatMap(d => d.events).reduce((s, e) => s + (e.cost?.points || 0), 0),
cash: itinerary.flatMap(d => d.events).reduce((s, e) => s + (e.cost?.cash || 0), 0),
flights: itinerary.flatMap(d => d.events).filter(e => e.category === 'Flight').length,
hotels: itinerary.flatMap(d => d.events).filter(e => e.category === 'Hotel').length,
activities: itinerary.flatMap(d => d.events).filter(e => ['Activity', 'Hike'].includes(e.category)).length
});
const categoryColors: Record<string, { bg: string; color: string }> = {
Flight: { bg: 'var(--accent-bg)', color: 'var(--accent)' },
Hotel: { bg: 'rgba(168,85,247,0.1)', color: '#a855f7' },
Restaurant: { bg: 'rgba(249,115,22,0.1)', color: '#f97316' },
Activity: { bg: 'var(--success-bg)', color: 'var(--success)' },
Hike: { bg: 'rgba(34,197,94,0.15)', color: '#16a34a' },
Drive: { bg: 'var(--warning-bg)', color: 'var(--warning)' },
Logistics: { bg: 'rgba(161,161,170,0.1)', color: 'var(--text-3)' }
};
// ── Helpers ──
function formatDateRange(start: string, end: string): string {
if (!start) return '';
const s = new Date(start + 'T00:00:00');
const e = new Date(end + 'T00:00:00');
const sM = s.toLocaleDateString('en-US', { month: 'short' });
const eM = e.toLocaleDateString('en-US', { month: 'short' });
return sM === eM ? `${sM} ${s.getDate()} ${e.getDate()}, ${s.getFullYear()}`
: `${sM} ${s.getDate()} ${eM} ${e.getDate()}, ${s.getFullYear()}`;
}
function durationDays(start: string, end: string): string {
if (!start || !end) return '';
const days = Math.ceil((new Date(end).getTime() - new Date(start).getTime()) / 86400000) + 1;
return `${days} days`;
}
function daysAway(start: string): string {
const diff = Math.ceil((new Date(start).getTime() - Date.now()) / 86400000);
return diff > 0 ? `${diff} days away` : '';
}
function formatTime(dateStr: string): string {
if (!dateStr) return '';
const d = new Date(dateStr);
if (isNaN(d.getTime())) return '';
return d.toLocaleTimeString('en-US', { hour: 'numeric', minute: '2-digit', hour12: true });
}
function formatDayDate(dateStr: string): string {
const d = new Date(dateStr + 'T00:00:00');
return d.toLocaleDateString('en-US', { weekday: 'short', month: 'short', day: 'numeric' });
}
function mapCategory(item: any, type: string): string {
if (type === 'transportation') {
const t = (item.type || '').toLowerCase();
if (t === 'plane' || t === 'flight') return 'Flight';
if (t === 'car' || t === 'rental') return 'Drive';
return 'Logistics';
}
if (type === 'lodging') return 'Hotel';
if (type === 'location') {
const c = (item.category || '').toLowerCase();
if (c === 'restaurant' || c === 'cafe' || c === 'bar' || c === 'food') return 'Restaurant';
if (c === 'hiking' || c === 'hike') return 'Hike';
return 'Activity';
}
return 'Activity';
}
function entityImage(tripImages: any[], entityId: string, entityOwnImages?: any[]): string {
// Check entity's own images first, then trip-level
const img = (entityOwnImages || []).find((i: any) => i.file_path)
|| (tripImages || []).find((i: any) => i.entity_id === entityId);
if (!img) return '';
const path = img.file_path || '';
return path.startsWith('/') ? path : `/images/${path}`;
}
// ── Load ──
onMount(async () => {
const tripId = page.url.searchParams.get('id');
if (!tripId) { loading = false; return; }
try {
const res = await fetch(`/api/trips/trip/${tripId}`, { credentials: 'include' });
if (!res.ok) { loading = false; return; }
const data = await res.json();
rawTripData = data;
buildItinerary(data);
expandedDays = new Set(itinerary.slice(0, 3).map(d => d.day));
} catch { /* silent */ }
finally { loading = false; }
});
</script>
{#if data?.useAtelierShell}
<AtelierTripDetailPage />
{:else}
<div class="page trip-detail-page">
<div class="app-surface">
<!-- Back (hidden in share mode) -->
{#if !shareMode}
<a href="/trips" class="back-link">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M15 18l-6-6 6-6"/></svg>
Back to Trips
</a>
{/if}
<!-- Cover Carousel -->
<div class="cover" style="background-image:url('{coverImages[currentCoverIdx]}')">
<div class="cover-overlay">
<div class="cover-content">
<div class="cover-name">{trip.name}</div>
<div class="cover-dates">{trip.dates}{trip.duration ? ' · ' + trip.duration : ''}</div>
{#if trip.away}<div class="cover-away">{trip.away}</div>{/if}
</div>
<div class="cover-controls">
<button class="cover-nav" onclick={prevCover} aria-label="Previous photo">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M15 18l-6-6 6-6"/></svg>
</button>
<span class="cover-counter">{currentCoverIdx + 1}/{coverImages.length}</span>
<button class="cover-nav" onclick={nextCover} aria-label="Next photo">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M9 18l6-6-6-6"/></svg>
</button>
</div>
</div>
<!-- Share button -->
<button class="cover-share" onclick={() => shareModalOpen = !shareModalOpen} aria-label="Share trip">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M4 12v8a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2v-8"/><polyline points="16 6 12 2 8 6"/><line x1="12" y1="2" x2="12" y2="15"/></svg>
</button>
</div>
<!-- Trip Highlights -->
{#if highlights.length > 0}
<div class="highlights">
{#each highlights as hl}
<div class="highlight-card">
<div class="highlight-icon">
{#if hl.icon === 'star'}
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z"/></svg>
{:else if hl.icon === 'restaurant'}
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M18 8h1a4 4 0 0 1 0 8h-1"/><path d="M2 8h16v9a4 4 0 0 1-4 4H6a4 4 0 0 1-4-4V8z"/><line x1="6" y1="1" x2="6" y2="4"/><line x1="10" y1="1" x2="10" y2="4"/><line x1="14" y1="1" x2="14" y2="4"/></svg>
{:else}
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="22 12 18 12 15 21 9 3 6 12 2 12"/></svg>
{/if}
</div>
<div class="highlight-text">
<div class="highlight-label">{hl.label}</div>
<div class="highlight-value">{hl.value}</div>
</div>
</div>
{/each}
</div>
{/if}
<!-- Trip Stats -->
<div class="trip-stats">
<div class="trip-stat">
<span class="trip-stat-value">{tripExpenses.flights}</span>
<span class="trip-stat-label">Flights</span>
</div>
<div class="trip-stat-sep"></div>
<div class="trip-stat">
<span class="trip-stat-value">{tripExpenses.hotels}</span>
<span class="trip-stat-label">Hotels</span>
</div>
<div class="trip-stat-sep"></div>
<div class="trip-stat">
<span class="trip-stat-value">{tripExpenses.activities}</span>
<span class="trip-stat-label">Activities</span>
</div>
<div class="trip-stat-sep"></div>
<div class="trip-stat">
<span class="trip-stat-value accent">{(tripExpenses.points / 1000).toFixed(0)}K</span>
<span class="trip-stat-label">Points</span>
</div>
<div class="trip-stat-sep"></div>
<div class="trip-stat">
<span class="trip-stat-value">${tripExpenses.cash.toLocaleString()}</span>
<span class="trip-stat-label">Cash</span>
</div>
</div>
<!-- Toggle: Map / AI Guide (hidden in share mode) -->
{#if !shareMode}
<div class="toggle-row">
<button class="toggle-btn" class:active={activeView === 'map'} onclick={() => (activeView = 'map')}>
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polygon points="1 6 1 22 8 18 16 22 23 18 23 2 16 6 8 2 1 6"/><line x1="8" y1="2" x2="8" y2="18"/><line x1="16" y1="6" x2="16" y2="22"/></svg>
Map
</button>
<button class="toggle-btn" class:active={activeView === 'ai'} onclick={() => (activeView = 'ai')}>
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 2a10 10 0 1 0 10 10H12V2z"/><path d="M20 12a8 8 0 0 0-8-8v8h8z"/></svg>
AI Guide
</button>
</div>
<!-- Map placeholder / AI Guide -->
{#if activeView === 'map'}
<div class="map-panel">
{#if mapLocations.length > 0}
<div class="map-embed">
<iframe
title="Trip Map"
src="https://www.openstreetmap.org/export/embed.html?bbox={Math.min(...mapLocations.map(p=>p.lng))-0.05},{Math.min(...mapLocations.map(p=>p.lat))-0.03},{Math.max(...mapLocations.map(p=>p.lng))+0.05},{Math.max(...mapLocations.map(p=>p.lat))+0.03}&layer=mapnik&marker={mapLocations[0].lat},{mapLocations[0].lng}"
allowfullscreen
loading="lazy"
></iframe>
</div>
{:else}
<div class="map-empty">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M21 10c0 7-9 13-9 13s-9-6-9-13a9 9 0 0 1 18 0z"/><circle cx="12" cy="10" r="3"/></svg>
<span>No pinned locations yet</span>
</div>
{/if}
</div>
{:else}
<div class="ai-guide">
<div class="ai-header">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 2a10 10 0 1 0 10 10H12V2z"/></svg>
<span>AI Trip Guide</span>
</div>
<div class="ai-suggestions">
<div class="ai-chip">Best restaurants near Garden of the Gods</div>
<div class="ai-chip">Hiking gear checklist for 14ers</div>
<div class="ai-chip">Weather forecast for Breckenridge Apr 15</div>
</div>
<div class="ai-input-wrap">
<input class="ai-input" type="text" placeholder="Ask about your trip..." bind:value={aiPrompt} />
<button class="ai-send" disabled={!aiPrompt.trim()}>
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="22" y1="2" x2="11" y2="13"/><polygon points="22 2 15 22 11 13 2 9 22 2"/></svg>
</button>
</div>
</div>
{/if}
{/if}
<!-- Content grid: itinerary + sidebar -->
<div class="content-grid">
<div class="content-main">
<!-- Itinerary -->
<div class="itinerary">
{#each itinerary as day (day.day)}
{@const isExpanded = expandedDays.has(day.day)}
<div class="day-section">
<button class="day-header" onclick={() => toggleDay(day.day)}>
<div class="day-left">
<div class="day-number">Day {day.day}</div>
<div class="day-info">
<span class="day-date">{day.date}</span>
<span class="day-desc">{day.description}</span>
</div>
</div>
<div class="day-right">
{#if day.weather}
<span class="day-weather">{day.weather.temp}</span>
{/if}
<span class="day-count">{day.events.length}</span>
<svg class="day-chevron" class:expanded={isExpanded} viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M6 9l6 6 6-6"/></svg>
</div>
</button>
{#if isExpanded}
<!-- Day Story (journal entry) -->
{@const story = getDayStory(day.day)}
{#if story}
<div class="day-story">
<div class="day-story-label">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"/><path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"/></svg>
Journal
</div>
<div class="day-story-text">{story.text}</div>
{#if story.photos.length > 0}
<div class="day-story-photos">
{#each story.photos as photo}
<div class="story-photo" style="background-image:url('{photo}')"></div>
{/each}
</div>
{/if}
</div>
{/if}
<div class="day-events">
{#each day.events as ev}
<button class="event-row" type="button" onclick={() => openEdit(ev)}>
<div class="event-time">{ev.time}</div>
<div class="event-body">
{#if ev.image}
<div class="event-image" style="background-image:url('{ev.image}')"></div>
{/if}
<div class="event-content">
<div class="event-top">
<div class="event-name">{ev.name}</div>
{#if categoryColors[ev.category]}
<span class="category-badge" style="background:{categoryColors[ev.category].bg};color:{categoryColors[ev.category].color}">{ev.category}</span>
{/if}
</div>
{#if ev.description}
<div class="event-desc">{ev.description}</div>
{/if}
<div class="event-meta">
{#if ev.flightNumber}
<span class="meta-tag flight">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M17.8 19.2L16 11l3.5-3.5C21 6 21.5 4 21 3c-1-.5-3 0-4.5 1.5L13 8 4.8 6.2c-.5-.1-.9.1-1.1.5l-.3.5 5.3 3.5-2.8 2.8L4 13l-1 1 3 1 1 3 1-1 .5-1.9 2.8-2.8 3.5 5.3.5-.3c.4-.2.6-.6.5-1.1z"/></svg>
{ev.flightNumber}
</span>
{/if}
{#if ev.reservation}
<span class="meta-tag">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="4" width="18" height="18" rx="2"/><line x1="16" y1="2" x2="16" y2="6"/><line x1="8" y1="2" x2="8" y2="6"/><line x1="3" y1="10" x2="21" y2="10"/></svg>
{ev.reservation}
</span>
{/if}
{#if ev.cost?.points}
<span class="meta-tag points">{(ev.cost.points / 1000).toFixed(0)}K pts</span>
{/if}
{#if ev.cost?.cash}
<span class="meta-tag cash">${ev.cost.cash}</span>
{/if}
{#if ev.mapsLink && ev.lat && ev.lng}
<span class="meta-tag maps-link" onclick={(e) => { e.stopPropagation(); window.open(`https://www.google.com/maps/search/?api=1&query=${ev.lat},${ev.lng}`, '_blank'); }}>
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 10c0 7-9 13-9 13s-9-6-9-13a9 9 0 0 1 18 0z"/><circle cx="12" cy="10" r="3"/></svg>
Maps
</span>
{/if}
{#if ev.stayingOvernight}
<span class="meta-tag overnight">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"/></svg>
Overnight
</span>
{/if}
</div>
</div>
</div>
</button>
{/each}
</div>
{/if}
</div>
{/each}
</div>
</div><!-- /content-main -->
<!-- Sidebar: notes + unscheduled -->
<div class="content-sidebar">
<!-- Notes -->
<section class="section">
<div class="section-title">NOTES</div>
<div class="notes-list">
{#each notes as note}
<div class="note-item">
<div class="note-date">{note.date}</div>
<div class="note-content">{note.content}</div>
{#if !shareMode}
<button class="note-delete" onclick={() => deleteNote(note.id)} title="Delete note">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>
</button>
{/if}
</div>
{/each}
</div>
{#if !shareMode}
<textarea class="input notes-textarea" placeholder="Add a note..." rows="3"></textarea>
{/if}
</section>
<!-- Unscheduled -->
{#if unscheduled.length > 0}
<section class="section">
<div class="section-title">UNSCHEDULED</div>
{#each unscheduled as item}
<div class="unsched-item">
<div class="unsched-name">{item.name}</div>
<div class="unsched-desc">{item.description}</div>
</div>
{/each}
</section>
{/if}
</div><!-- /content-sidebar -->
</div><!-- /content-grid -->
</div>
</div>
<!-- Share modal -->
{#if shareModalOpen}
<!-- svelte-ignore a11y_no_static_element_interactions -->
<div class="modal-overlay" onclick={() => shareModalOpen = false}>
<!-- svelte-ignore a11y_no_static_element_interactions -->
<div class="share-modal" onclick={(e) => e.stopPropagation()}>
<div class="share-title">Share Trip</div>
<div class="share-desc">Anyone with the link can view your itinerary</div>
<div class="share-link-row">
<input class="share-link-input" type="text" readonly value="https://platform.quadjourney.com/trips/view/co2026" />
<button class="share-copy-btn">Copy</button>
</div>
</div>
</div>
{/if}
<!-- Contextual FAB (hidden in share mode) -->
{#if !shareMode}
<button class="fab" class:open={fabOpen} onclick={() => fabOpen = !fabOpen} aria-label="Add to trip">
<svg class="fab-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/></svg>
</button>
{#if fabOpen}
<!-- svelte-ignore a11y_no_static_element_interactions -->
<div class="fab-overlay" onclick={() => fabOpen = false}></div>
<div class="fab-sheet">
<div class="fab-sheet-handle"></div>
<button class="fab-action" onclick={() => openCreateModal('location')}>
<div class="fab-action-icon accent">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 10c0 7-9 13-9 13s-9-6-9-13a9 9 0 0 1 18 0z"/><circle cx="12" cy="10" r="3"/></svg>
</div>
<div class="fab-action-text">
<div class="fab-action-name">Add activity</div>
<div class="fab-action-desc">Restaurant, hike, attraction, or event</div>
</div>
</button>
<button class="fab-action" onclick={() => openCreateModal('lodging')}>
<div class="fab-action-icon">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/><polyline points="9 22 9 12 15 12 15 22"/></svg>
</div>
<div class="fab-action-text">
<div class="fab-action-name">Add accommodation</div>
<div class="fab-action-desc">Hotel, Airbnb, hostel, or resort</div>
</div>
</button>
<button class="fab-action" onclick={() => openCreateModal('transportation')}>
<div class="fab-action-icon">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M17.8 19.2L16 11l3.5-3.5C21 6 21.5 4 21 3c-1-.5-3 0-4.5 1.5L13 8 4.8 6.2c-.5-.1-.9.1-1.1.5l-.3.5 5.3 3.5-2.8 2.8L4 13l-1 1 3 1 1 3 1-1 .5-1.9 2.8-2.8 3.5 5.3.5-.3c.4-.2.6-.6.5-1.1z"/></svg>
</div>
<div class="fab-action-text">
<div class="fab-action-name">Add transportation</div>
<div class="fab-action-desc">Flight, train, rental car, or bus</div>
</div>
</button>
<button class="fab-action" onclick={() => openCreateModal('note')}>
<div class="fab-action-icon">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"/><path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"/></svg>
</div>
<div class="fab-action-text">
<div class="fab-action-name">Add note</div>
<div class="fab-action-desc">Journal entry, reminder, or tip</div>
</div>
</button>
<button class="fab-action" onclick={() => { fabOpen = false; tripEditOpen = true; }}>
<div class="fab-action-icon">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 1 1-2.83 2.83l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 1 1-4 0v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 1 1-2.83-2.83l.06-.06A1.65 1.65 0 0 0 4.68 15a1.65 1.65 0 0 0-1.51-1H3a2 2 0 1 1 0-4h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 1 1 2.83-2.83l.06.06A1.65 1.65 0 0 0 9 4.68a1.65 1.65 0 0 0 1-1.51V3a2 2 0 1 1 4 0v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 1 1 2.83 2.83l-.06.06A1.65 1.65 0 0 0 19.4 9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 1 1 0 4h-.09a1.65 1.65 0 0 0-1.51 1z"/></svg>
</div>
<div class="fab-action-text">
<div class="fab-action-name">Edit trip</div>
<div class="fab-action-desc">Name, dates, sharing</div>
</div>
</button>
<button class="fab-action delete-action" onclick={deleteTrip}>
<div class="fab-action-icon delete">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="3 6 5 6 21 6"/><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/></svg>
</div>
<div class="fab-action-text">
<div class="fab-action-name">Delete trip</div>
<div class="fab-action-desc">Permanently remove this trip</div>
</div>
</button>
</div>
{/if}
{/if}
<!-- Item create/edit modal -->
<ItemModal bind:open={itemModalOpen} tripId={page.url.searchParams.get('id') || ''} itemType={itemModalType} editItem={itemModalEdit} onSaved={reloadTrip} />
<!-- Trip edit modal -->
<TripEditModal bind:open={tripEditOpen} tripData={rawTripData} onSaved={reloadTrip} />
<style>
/* ── Edit sheet ── */
.edit-overlay { position: fixed; inset: 0; background: rgba(0,0,0,0.35); z-index: 60; display: flex; justify-content: flex-end; animation: editFadeIn 150ms ease; }
@keyframes editFadeIn { from { opacity: 0; } to { opacity: 1; } }
.edit-sheet { width: 420px; max-width: 100%; height: 100%; background: var(--surface); display: flex; flex-direction: column; box-shadow: -8px 0 32px rgba(0,0,0,0.1); animation: editSlideIn 200ms ease; }
@keyframes editSlideIn { from { transform: translateX(100%); } to { transform: translateX(0); } }
.edit-header { display: flex; align-items: center; justify-content: space-between; padding: var(--sp-4) var(--sp-5); border-bottom: 1px solid var(--border); }
.edit-title { font-size: var(--text-md); font-weight: 600; color: var(--text-1); }
.edit-close { background: none; border: none; cursor: pointer; color: var(--text-3); padding: var(--sp-1); border-radius: var(--radius-sm); }
.edit-close:hover { color: var(--text-1); background: var(--card-hover); }
.edit-close svg { width: 18px; height: 18px; }
.edit-body { flex: 1; overflow-y: auto; padding: var(--sp-5); display: flex; flex-direction: column; gap: 14px; }
.edit-field { display: flex; flex-direction: column; gap: var(--sp-1); }
.edit-label { font-size: var(--text-sm); font-weight: 500; color: var(--text-3); text-transform: uppercase; letter-spacing: 0.04em; }
.edit-input { padding: 10px 12px; border-radius: var(--radius-md); border: 1px solid var(--border); background: var(--surface-secondary); color: var(--text-1); font-size: var(--text-base); font-family: var(--font); }
.edit-input:focus { outline: none; border-color: var(--accent); }
.edit-textarea { resize: vertical; min-height: 60px; }
.edit-row { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; }
.edit-footer { display: flex; align-items: center; justify-content: space-between; padding: 14px var(--sp-5); border-top: 1px solid var(--border); }
.edit-footer-right { display: flex; gap: var(--sp-2); }
.edit-delete { display: flex; align-items: center; gap: var(--sp-1); padding: var(--sp-2) var(--sp-3); border-radius: var(--radius-md); background: none; border: 1px solid var(--error); color: var(--error); font-size: var(--text-sm); font-weight: 500; cursor: pointer; font-family: var(--font); }
.edit-delete:hover { background: var(--error-bg); }
.edit-delete svg { width: 14px; height: 14px; }
.edit-cancel { padding: 8px 14px; border-radius: var(--radius-md); background: var(--card-secondary); color: var(--text-2); border: 1px solid var(--border); font-size: var(--text-sm); font-weight: 500; cursor: pointer; font-family: var(--font); }
.edit-save { padding: var(--sp-2) var(--sp-4); border-radius: var(--radius-md); background: var(--accent); color: white; border: none; font-size: var(--text-sm); font-weight: 600; cursor: pointer; font-family: var(--font); }
.edit-save:disabled { opacity: 0.5; }
.note-delete { position: absolute; top: 10px; right: 10px; background: none; border: none; cursor: pointer; color: var(--text-4); padding: var(--sp-1); border-radius: var(--radius-xs); transition: all var(--transition); }
.note-item { position: relative; }
.note-delete:hover { color: var(--error); background: var(--error-bg); }
.note-delete svg { width: 14px; height: 14px; }
@media (max-width: 768px) {
.edit-sheet { width: 100%; }
}
/* ── Back ── */
.back-link { display: inline-flex; align-items: center; gap: var(--sp-1.5); font-size: var(--text-sm); color: var(--text-3); margin-bottom: var(--sp-4); transition: color var(--transition); }
.back-link:hover { color: var(--text-1); }
.back-link svg { width: 14px; height: 14px; }
/* ── Cover Carousel ── */
.cover { width: 100%; height: 280px; border-radius: var(--radius); background: var(--text-4) center/cover no-repeat; overflow: hidden; position: relative; margin-bottom: var(--sp-4); }
.cover-overlay { position: absolute; inset: 0; background: linear-gradient(to top, rgba(0,0,0,0.7) 0%, rgba(0,0,0,0.1) 45%, transparent); display: flex; align-items: flex-end; justify-content: space-between; padding: var(--sp-6); }
.cover-content { color: white; }
.cover-name { font-size: var(--text-3xl); font-weight: 600; line-height: 1.1; }
.cover-dates { font-size: var(--text-base); opacity: 0.85; margin-top: 5px; }
.cover-away { font-size: var(--text-sm); opacity: 0.65; margin-top: 2px; }
.cover-controls { display: flex; align-items: center; gap: var(--sp-1.5); align-self: flex-end; }
.cover-nav { width: 30px; height: 30px; border-radius: 50%; background: rgba(255,255,255,0.15); backdrop-filter: blur(6px); border: none; color: white; display: flex; align-items: center; justify-content: center; cursor: pointer; transition: background var(--transition); }
.cover-nav:hover { background: rgba(255,255,255,0.3); }
.cover-nav svg { width: 14px; height: 14px; }
.cover-counter { color: white; font-size: var(--text-xs); opacity: 0.6; font-family: var(--mono); }
.cover-share { position: absolute; top: 14px; right: 14px; width: 34px; height: 34px; border-radius: 50%; background: rgba(0,0,0,0.3); backdrop-filter: blur(6px); border: none; color: white; display: flex; align-items: center; justify-content: center; cursor: pointer; transition: all var(--transition); }
.cover-share:hover { background: rgba(0,0,0,0.5); }
.cover-share svg { width: 15px; height: 15px; }
/* ── Highlights ── */
.highlights { display: flex; flex-direction: column; gap: var(--sp-1.5); margin-bottom: 14px; }
.highlight-card {
display: flex; align-items: flex-start; gap: var(--sp-3);
padding: var(--sp-3) 14px; background: var(--card); border-radius: 10px;
border: 1px solid var(--border); box-shadow: 0 2px 6px rgba(0,0,0,0.05);
}
.highlight-icon {
width: 30px; height: 30px; border-radius: var(--radius-md);
background: var(--accent-bg); color: var(--accent);
display: flex; align-items: center; justify-content: center; flex-shrink: 0;
}
.highlight-icon svg { width: 14px; height: 14px; }
.highlight-text { flex: 1; min-width: 0; }
.highlight-label { font-size: var(--text-xs); font-weight: 600; text-transform: uppercase; letter-spacing: 0.05em; color: var(--text-3); margin-bottom: 2px; }
.highlight-value { font-size: var(--text-sm); color: var(--text-1); line-height: 1.45; }
/* ── Trip Stats ── */
.trip-stats { display: flex; align-items: center; justify-content: space-between; padding: 12px 22px; background: var(--card); border: 1px solid var(--border); border-radius: var(--radius); margin-bottom: var(--sp-2); }
.trip-stat { text-align: center; flex: 1; min-width: 0; }
.trip-stat-value { font-size: var(--text-md); font-weight: 600; font-family: var(--mono); color: var(--text-1); display: block; }
.trip-stat-value.accent { color: var(--accent); }
.trip-stat-label { font-size: var(--text-xs); color: var(--text-3); margin-top: 2px; display: block; text-transform: uppercase; letter-spacing: 0.03em; }
.trip-stat-sep { width: 1px; height: 24px; background: var(--border); flex-shrink: 0; margin: 0 var(--sp-1); }
/* ── Toggle ── */
.toggle-row { display: flex; gap: 0; margin-bottom: var(--sp-1.5); border-bottom: 1px solid var(--border); }
.toggle-btn { display: flex; align-items: center; gap: var(--sp-1.5); padding: 10px 16px 11px; border-radius: 0; background: none; color: var(--text-3); border: none; border-bottom: 2px solid transparent; font-size: var(--text-sm); font-weight: 500; cursor: pointer; transition: all var(--transition); font-family: var(--font); margin-bottom: -1px; }
.toggle-btn:hover { color: var(--text-2); }
.toggle-btn.active { color: var(--text-1); border-bottom-color: var(--accent); font-weight: 600; }
.toggle-btn svg { width: 14px; height: 14px; }
/* ── Map placeholder ── */
.map-panel { border-radius: 0 0 var(--radius) var(--radius); background: var(--card); border: 1px solid var(--border); border-top: none; margin-bottom: var(--sp-5); overflow: hidden; }
.map-embed { width: 100%; height: 250px; }
.map-embed iframe { width: 100%; height: 100%; border: none; display: block; }
.map-empty { display: flex; flex-direction: column; align-items: center; gap: var(--sp-1.5); padding: var(--sp-8); color: var(--text-4); }
.map-empty svg { width: 24px; height: 24px; opacity: 0.3; }
.map-empty span { font-size: var(--text-sm); }
/* ── AI Guide ── */
.ai-guide { background: var(--surface-secondary); border: 1px solid var(--border); border-radius: 0 0 var(--radius) var(--radius); border-top: none; padding: 18px 16px 16px; margin-bottom: var(--sp-5); }
.ai-header { display: flex; align-items: center; gap: var(--sp-2); font-size: var(--text-base); font-weight: 600; color: var(--text-1); margin-bottom: var(--sp-3); }
.ai-header svg { width: 16px; height: 16px; color: var(--accent); }
.ai-suggestions { display: flex; flex-wrap: wrap; gap: var(--sp-1.5); margin-bottom: var(--sp-3); }
.ai-chip { padding: 5px 11px; border-radius: var(--radius-lg); background: var(--surface-secondary); border: 1px solid var(--border); font-size: var(--text-sm); color: var(--text-2); cursor: pointer; transition: all var(--transition); }
.ai-chip:hover { background: var(--accent-bg); color: var(--accent); border-color: var(--accent); }
.ai-input-wrap { display: flex; gap: var(--sp-2); }
.ai-input { flex: 1; padding: 9px 12px; border-radius: var(--radius-md); border: 1px solid var(--border); background: var(--surface-secondary); color: var(--text-1); font-size: var(--text-md); font-family: var(--font); }
.ai-input:focus { outline: none; border-color: var(--accent); }
.ai-input::placeholder { color: var(--text-4); }
.ai-send { width: 38px; height: 38px; border-radius: var(--radius-md); background: var(--accent); color: white; border: none; display: flex; align-items: center; justify-content: center; cursor: pointer; transition: opacity var(--transition); }
.ai-send:disabled { opacity: 0.4; cursor: default; }
.ai-send svg { width: 15px; height: 15px; }
/* ── Itinerary ── */
/* ── Wide page override ── */
.trip-detail-page :global(.app-surface) { max-width: 1400px; }
/* ── Content grid ── */
.content-grid { display: grid; grid-template-columns: 1fr; gap: var(--sp-6); }
.content-main { min-width: 0; }
.content-sidebar { display: flex; flex-direction: column; gap: var(--sp-5); }
@media (min-width: 1024px) {
.content-grid { grid-template-columns: 1fr 320px; gap: var(--sp-8); }
.content-sidebar { position: sticky; top: 80px; align-self: start; }
}
.itinerary { margin-bottom: var(--sp-5); padding-bottom: 60px; }
/* ── Day sections ── */
.day-section { margin-bottom: 0; }
.day-section + .day-section { margin-top: var(--sp-4); }
.day-header {
display: flex; align-items: center; justify-content: space-between; width: 100%;
padding: 14px 0 12px; background: none; border: none;
border-bottom: 2px solid var(--border); cursor: pointer;
font-family: var(--font); color: inherit; text-align: left;
}
.day-left { display: flex; align-items: baseline; gap: var(--sp-2); }
.day-number { font-size: var(--text-sm); font-weight: 700; color: var(--accent); white-space: nowrap; letter-spacing: 0.01em; }
.day-info { display: flex; gap: var(--sp-1.5); align-items: baseline; }
.day-date { font-size: var(--text-xs); color: var(--text-4); font-family: var(--mono); }
.day-desc { font-size: var(--text-sm); color: var(--text-1); font-weight: 600; }
.day-right { display: flex; align-items: center; gap: var(--sp-2); }
.day-weather { font-size: var(--text-xs); color: var(--text-3); font-family: var(--mono); }
.day-count { font-size: var(--text-xs); color: var(--text-4); background: var(--card-hover); padding: 2px 7px; border-radius: var(--radius-md); font-weight: 500; }
.day-chevron { width: 14px; height: 14px; color: var(--text-4); transition: transform var(--transition); }
.day-chevron.expanded { transform: rotate(180deg); }
/* ── Day Story (journal) — visually distinct from events ── */
.day-story {
padding: 12px 16px; margin: 8px 0 6px;
background: var(--surface-secondary);
border-radius: 10px;
border-left: 3px solid color-mix(in srgb, var(--accent) 50%, transparent);
box-shadow: 0 1px 2px rgba(0,0,0,0.04);
}
.day-story-label {
display: flex; align-items: center; gap: 5px;
font-size: var(--text-xs); font-weight: 600; text-transform: uppercase; letter-spacing: 0.05em;
color: var(--accent); margin-bottom: 6px; opacity: 0.7;
}
.day-story-label svg { width: 12px; height: 12px; }
.day-story-text { font-size: var(--text-sm); color: var(--text-2); line-height: 1.75; }
.day-story-photos { display: flex; gap: 6px; margin-top: 10px; overflow-x: auto; padding-bottom: 4px; }
.day-story-photos::-webkit-scrollbar { height: 3px; }
.day-story-photos::-webkit-scrollbar-thumb { background: var(--border); border-radius: 2px; }
.story-photo { width: 160px; height: 108px; border-radius: var(--radius-md); background: var(--card-hover) center/cover no-repeat; flex-shrink: 0; }
/* ── Event cards (unified system) ── */
.day-events { display: grid; grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); gap: var(--sp-3); padding: var(--sp-3) 0 var(--sp-2); }
.event-row {
display: flex; flex-direction: column; gap: 0; padding: 0; width: 100%; text-align: left;
background: var(--card); border-radius: 10px; border: 1px solid var(--border);
transition: all var(--transition); cursor: pointer; overflow: hidden;
font-family: var(--font); color: inherit; -webkit-appearance: none;
}
.event-row:hover { background: var(--card-hover); }
.event-time { font-size: var(--text-xs); font-family: var(--mono); color: var(--text-3); font-weight: 500; padding: 12px 14px 0; line-height: 1.35; }
.event-body { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 0; }
.event-image { width: 100%; height: 160px; border-radius: 0; background: var(--card-hover) center/cover no-repeat; flex-shrink: 0; }
.event-content { flex: 1; min-width: 0; padding: 10px 14px 12px; }
.event-top { display: flex; align-items: flex-start; justify-content: space-between; gap: var(--sp-2); }
.event-name { font-size: var(--text-sm); font-weight: 500; color: var(--text-1); line-height: 1.35; }
.event-desc { font-size: var(--text-sm); color: var(--text-3); margin-top: 3px; line-height: 1.4; }
.category-badge { font-size: var(--text-xs); font-weight: 600; padding: 2px 7px; border-radius: var(--radius-sm); line-height: 1.35; flex-shrink: 0; white-space: nowrap; text-transform: uppercase; letter-spacing: 0.02em; margin-top: 1px; }
/* ── Event meta tags ── */
.event-meta { display: flex; align-items: center; gap: var(--sp-2); margin-top: var(--sp-1); flex-wrap: wrap; }
.meta-tag { display: inline-flex; align-items: center; gap: 3px; font-size: var(--text-xs); color: var(--text-3); }
.meta-tag svg { width: 10px; height: 10px; }
.meta-tag.flight { color: var(--accent); font-weight: 500; }
.meta-tag.points { color: var(--accent); font-family: var(--mono); font-weight: 500; }
.meta-tag.cash { color: var(--text-2); font-family: var(--mono); }
.meta-tag.overnight { color: var(--text-3); }
.maps-link { color: var(--accent); font-weight: 500; text-decoration: none; }
/* ── Sections ── */
.section { margin-bottom: var(--sp-6); }
.section-title { font-size: var(--text-xs); font-weight: 600; color: var(--text-3); text-transform: uppercase; letter-spacing: 0.06em; margin-bottom: 10px; }
.unsched-item { padding: 12px 14px; background: var(--card); border-radius: 10px; border: 1px solid var(--border); margin-bottom: var(--sp-1); }
.unsched-name { font-size: var(--text-sm); font-weight: 500; color: var(--text-1); }
.unsched-desc { font-size: var(--text-sm); color: var(--text-3); margin-top: 2px; }
/* ── Notes ── */
.notes-list { display: flex; flex-direction: column; gap: var(--sp-1); margin-bottom: 10px; }
.note-item { padding: 12px 14px; background: var(--card); border-radius: 10px; border: 1px solid var(--border); }
.note-date { font-size: var(--text-xs); color: var(--text-4); margin-bottom: 3px; text-transform: uppercase; letter-spacing: 0.03em; }
.note-content { font-size: var(--text-sm); color: var(--text-2); line-height: 1.55; }
.notes-textarea { resize: vertical; min-height: 70px; font-size: var(--text-md); }
/* ── Share modal ── */
.modal-overlay { position: fixed; inset: 0; background: var(--overlay); z-index: 60; display: flex; align-items: center; justify-content: center; animation: fadeIn 150ms ease; }
@keyframes fadeIn { from { opacity: 0; } to { opacity: 1; } }
.share-modal { background: var(--surface); border-radius: var(--radius); padding: var(--sp-6); width: 400px; max-width: 90vw; box-shadow: 0 16px 48px rgba(0,0,0,0.15); }
.share-title { font-size: var(--text-lg); font-weight: 600; color: var(--text-1); margin-bottom: var(--sp-1); }
.share-desc { font-size: var(--text-sm); color: var(--text-3); margin-bottom: 14px; }
.share-link-row { display: flex; gap: var(--sp-2); }
.share-link-input { flex: 1; padding: 9px 12px; border-radius: var(--radius-md); border: 1px solid var(--border); background: var(--surface-secondary); color: var(--text-1); font-size: var(--text-sm); font-family: var(--mono); }
.share-copy-btn { padding: 9px 14px; border-radius: var(--radius-md); background: var(--accent); color: white; border: none; font-size: var(--text-sm); font-weight: 600; cursor: pointer; font-family: var(--font); }
/* ── FAB ── */
.fab { position: fixed; bottom: 12px; right: 20px; width: 52px; height: 52px; border-radius: 50%; background: var(--accent); color: white; border: none; display: flex; align-items: center; justify-content: center; cursor: pointer; box-shadow: 0 6px 20px rgba(79,70,229,0.3); transition: all var(--transition); z-index: 50; }
.fab:hover { transform: scale(1.08); box-shadow: 0 8px 28px rgba(79,70,229,0.4); }
.fab-icon { width: 22px; height: 22px; transition: transform 200ms ease; }
.fab.open .fab-icon { transform: rotate(45deg); }
/* ── FAB Sheet ── */
.fab-overlay { position: fixed; inset: 0; background: var(--overlay); z-index: 45; animation: fabFadeIn 150ms ease; }
@keyframes fabFadeIn { from { opacity: 0; } to { opacity: 1; } }
.fab-sheet {
position: fixed; bottom: 0; left: 0; right: 0; z-index: 48;
background: var(--surface); border-top-left-radius: 20px; border-top-right-radius: 20px;
padding: 12px 16px 28px; box-shadow: 0 -8px 32px rgba(0,0,0,0.12);
animation: fabSlideUp 200ms ease;
}
@keyframes fabSlideUp { from { transform: translateY(100%); } to { transform: translateY(0); } }
.fab-sheet-handle { width: 36px; height: 4px; border-radius: 2px; background: var(--border-strong); margin: 0 auto var(--sp-4); }
.fab-action {
display: flex; align-items: center; gap: 14px; width: 100%;
padding: 13px 12px; border-radius: var(--radius); background: none; border: none;
cursor: pointer; font-family: var(--font); text-align: left; transition: background var(--transition);
}
.fab-action:hover { background: var(--card-hover); }
.fab-action-icon {
width: 38px; height: 38px; border-radius: 10px;
background: var(--card); border: 1px solid var(--border);
display: flex; align-items: center; justify-content: center; flex-shrink: 0; color: var(--text-3);
}
.fab-action-icon.accent { background: var(--accent); border-color: var(--accent); color: white; }
.fab-action-icon svg { width: 17px; height: 17px; }
.fab-action-text { flex: 1; min-width: 0; }
.fab-action-name { font-size: var(--text-base); font-weight: 500; color: var(--text-1); }
.fab-action-desc { font-size: var(--text-sm); color: var(--text-3); margin-top: 1px; }
.fab-action.delete-action { border-top: 1px solid var(--border); margin-top: var(--sp-1); padding-top: var(--sp-4); }
.fab-action-icon.delete { background: var(--error-bg); border-color: transparent; color: var(--error); }
.delete-action .fab-action-name { color: var(--error); }
@media (max-width: 768px) {
.cover { height: 220px; }
.cover-name { font-size: var(--text-2xl); }
.trip-stats { padding: var(--sp-3) var(--sp-4); }
.trip-stat-sep { height: 18px; margin: 0 var(--sp-0.5); }
.event-image { height: 120px; }
.day-events { grid-template-columns: 1fr; gap: var(--sp-2); }
.fab { bottom: 62px; }
.itinerary { padding-bottom: 80px; }
.day-left { flex-direction: column; gap: var(--sp-0.5); }
.day-section + .day-section { margin-top: var(--sp-3); }
.story-photo { width: 140px; height: 95px; }
.highlight-card { padding: 11px 12px; }
.day-story { margin: 6px 0 4px; padding: 10px 14px; }
}
</style>
{/if}