feat: brain masonry card grid — Karakeep-style layout with Atelier aesthetics
- 3-column CSS masonry grid (2 on tablet, 1 on mobile) - Link cards show screenshot thumbnails - Note cards show content body inline - Tags as pills, folder/date in meta footer - Screenshot serving endpoint added to brain API - Auto-polling for pending items (3s interval) - Detail sheet shows raw_content for notes - Warm frosted glass card styling matching Atelier design Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,11 +1,15 @@
|
||||
<script lang="ts">
|
||||
import { onMount } from 'svelte';
|
||||
|
||||
import { onDestroy } from 'svelte';
|
||||
|
||||
interface BrainItem {
|
||||
id: string;
|
||||
type: string;
|
||||
title: string | null;
|
||||
url: string | null;
|
||||
raw_content: string | null;
|
||||
extracted_text: string | null;
|
||||
folder: string | null;
|
||||
tags: string[] | null;
|
||||
summary: string | null;
|
||||
@@ -159,10 +163,42 @@
|
||||
if (e.key === 'Enter') search();
|
||||
}
|
||||
|
||||
// Poll for pending items
|
||||
let pollTimer: ReturnType<typeof setInterval> | null = null;
|
||||
|
||||
function startPolling() {
|
||||
stopPolling();
|
||||
pollTimer = setInterval(async () => {
|
||||
const hasPending = items.some(i => i.processing_status === 'pending' || i.processing_status === 'processing');
|
||||
if (hasPending) {
|
||||
await loadItems();
|
||||
// Update selected item if it was pending
|
||||
if (selectedItem) {
|
||||
const updated = items.find(i => i.id === selectedItem!.id);
|
||||
if (updated) selectedItem = updated;
|
||||
}
|
||||
} else {
|
||||
stopPolling();
|
||||
}
|
||||
}, 3000);
|
||||
}
|
||||
|
||||
function stopPolling() {
|
||||
if (pollTimer) { clearInterval(pollTimer); pollTimer = null; }
|
||||
}
|
||||
|
||||
// Start polling after capture
|
||||
$effect(() => {
|
||||
const hasPending = items.some(i => i.processing_status === 'pending' || i.processing_status === 'processing');
|
||||
if (hasPending && !pollTimer) startPolling();
|
||||
});
|
||||
|
||||
onMount(async () => {
|
||||
await loadConfig();
|
||||
await loadItems();
|
||||
});
|
||||
|
||||
onDestroy(stopPolling);
|
||||
</script>
|
||||
|
||||
<div class="page">
|
||||
@@ -220,94 +256,95 @@
|
||||
{/each}
|
||||
</section>
|
||||
|
||||
<!-- ═══ Search + Item feed ═══ -->
|
||||
<section class="brain-layout">
|
||||
<div class="brain-main">
|
||||
|
||||
<div class="toolbar-card">
|
||||
<div class="toolbar-head">
|
||||
<div>
|
||||
<div class="toolbar-label">Search</div>
|
||||
<div class="toolbar-title">Find saved items</div>
|
||||
</div>
|
||||
<div class="toolbar-meta">{items.length} item{items.length !== 1 ? 's' : ''}</div>
|
||||
</div>
|
||||
<div class="search-wrap">
|
||||
<svg class="search-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="11" cy="11" r="8"/><path d="M21 21l-4.35-4.35"/></svg>
|
||||
<input
|
||||
type="text"
|
||||
class="search-input"
|
||||
placeholder="Search by title, content, tags..."
|
||||
bind:value={searchQuery}
|
||||
onkeydown={handleSearchKey}
|
||||
/>
|
||||
{#if searchQuery}
|
||||
<button class="search-clear" onclick={() => { searchQuery = ''; loadItems(); }}>
|
||||
<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>
|
||||
</div>
|
||||
|
||||
<div class="queue-header">
|
||||
<div>
|
||||
<div class="queue-label">Feed</div>
|
||||
<div class="queue-title">{activeFolder || 'All items'}</div>
|
||||
<div class="queue-summary">
|
||||
{#if activeFolder}
|
||||
Items the AI classified under {activeFolder.toLowerCase()}.
|
||||
{:else}
|
||||
Your complete saved collection, newest first.
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#if loading}
|
||||
<div class="items-card">
|
||||
{#each [1, 2, 3, 4] as _}
|
||||
<div class="item-row skeleton-row" style="height: 80px"></div>
|
||||
{/each}
|
||||
</div>
|
||||
{:else if items.length === 0}
|
||||
<div class="items-card">
|
||||
<div class="empty">No items yet. Paste a URL or note above to get started.</div>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="items-card">
|
||||
{#each items as item (item.id)}
|
||||
<button class="item-row" onclick={() => selectedItem = item}>
|
||||
<div class="row-accent" class:processing={item.processing_status === 'processing'} class:failed={item.processing_status === 'failed'}></div>
|
||||
<div class="item-info">
|
||||
<div class="item-name">{item.title || 'Processing...'}</div>
|
||||
<div class="item-meta">
|
||||
<span>{formatDate(item.created_at)}</span>
|
||||
{#if item.folder}<span class="meta-folder">{item.folder}</span>{/if}
|
||||
{#if item.url}<span class="meta-url">{new URL(item.url).hostname}</span>{/if}
|
||||
{#each (item.tags || []).slice(0, 2) as tag}
|
||||
<span class="meta-tag">{tag}</span>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
<div class="item-tail">
|
||||
{#if item.processing_status === 'pending' || item.processing_status === 'processing'}
|
||||
<span class="status-badge processing-badge">Processing</span>
|
||||
{:else if item.processing_status === 'failed'}
|
||||
<span class="status-badge error">Failed</span>
|
||||
{:else if item.confidence && item.confidence > 0.8}
|
||||
<span class="conf-dot high"></span>
|
||||
{:else if item.confidence}
|
||||
<span class="conf-dot med"></span>
|
||||
{/if}
|
||||
<svg class="row-chevron" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M9 18l6-6-6-6"/></svg>
|
||||
</div>
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
<!-- ═══ Search bar ═══ -->
|
||||
<section class="search-section">
|
||||
<div class="search-wrap">
|
||||
<svg class="search-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="11" cy="11" r="8"/><path d="M21 21l-4.35-4.35"/></svg>
|
||||
<input
|
||||
type="text"
|
||||
class="search-input"
|
||||
placeholder="Search your brain..."
|
||||
bind:value={searchQuery}
|
||||
onkeydown={handleSearchKey}
|
||||
/>
|
||||
{#if searchQuery}
|
||||
<button class="search-clear" onclick={() => { searchQuery = ''; loadItems(); }}>
|
||||
<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>
|
||||
</section>
|
||||
|
||||
<!-- ═══ Masonry card grid ═══ -->
|
||||
{#if loading}
|
||||
<div class="masonry">
|
||||
{#each [1, 2, 3, 4, 5, 6] as _}
|
||||
<div class="card skeleton-card"></div>
|
||||
{/each}
|
||||
</div>
|
||||
{:else if items.length === 0}
|
||||
<div class="empty-state">
|
||||
<div class="empty-icon">
|
||||
<svg width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M12 2L2 7l10 5 10-5-10-5z"/><path d="M2 17l10 5 10-5"/><path d="M2 12l10 5 10-5"/></svg>
|
||||
</div>
|
||||
<div class="empty-title">Nothing saved yet</div>
|
||||
<div class="empty-desc">Paste a URL or type a note in the capture bar above.</div>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="masonry">
|
||||
{#each items as item (item.id)}
|
||||
<button class="card" class:is-note={item.type === 'note'} class:is-processing={item.processing_status !== 'ready'} onclick={() => selectedItem = item}>
|
||||
<!-- Screenshot for links -->
|
||||
{#if item.type === 'link' && item.assets?.some(a => a.asset_type === 'screenshot')}
|
||||
<div class="card-thumb">
|
||||
<img src="/api/brain/storage/{item.id}/screenshot/screenshot.png" alt="" loading="lazy" />
|
||||
{#if item.processing_status !== 'ready'}
|
||||
<div class="card-processing-overlay">
|
||||
<span class="processing-dot"></span>
|
||||
Processing...
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{:else if item.type === 'note'}
|
||||
<!-- Note shows content directly -->
|
||||
<div class="card-note-body">
|
||||
{(item.raw_content || '').slice(0, 200)}{(item.raw_content || '').length > 200 ? '...' : ''}
|
||||
</div>
|
||||
{:else if item.processing_status !== 'ready'}
|
||||
<div class="card-placeholder">
|
||||
<span class="processing-dot"></span>
|
||||
<span>Processing...</span>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- Card content -->
|
||||
<div class="card-content">
|
||||
<div class="card-title">{item.title || 'Untitled'}</div>
|
||||
{#if item.url}
|
||||
<div class="card-domain">{(() => { try { return new URL(item.url).hostname; } catch { return ''; } })()}</div>
|
||||
{/if}
|
||||
{#if item.summary && item.type !== 'note'}
|
||||
<div class="card-summary">{item.summary.slice(0, 100)}{item.summary.length > 100 ? '...' : ''}</div>
|
||||
{/if}
|
||||
<div class="card-footer">
|
||||
{#if item.tags && item.tags.length > 0}
|
||||
<div class="card-tags">
|
||||
{#each item.tags.slice(0, 3) as tag}
|
||||
<span class="card-tag">{tag}</span>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
<div class="card-meta">
|
||||
{#if item.folder}<span class="card-folder">{item.folder}</span>{/if}
|
||||
<span class="card-date">{formatDate(item.created_at)}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -330,8 +367,18 @@
|
||||
<a class="detail-url" href={selectedItem.url} target="_blank" rel="noopener">{selectedItem.url}</a>
|
||||
{/if}
|
||||
|
||||
{#if selectedItem.raw_content}
|
||||
<div class="detail-content">
|
||||
<div class="content-label">Note</div>
|
||||
<div class="content-body">{selectedItem.raw_content}</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if selectedItem.summary}
|
||||
<div class="detail-summary">{selectedItem.summary}</div>
|
||||
<div class="detail-summary">
|
||||
<div class="content-label">AI Summary</div>
|
||||
{selectedItem.summary}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div class="detail-meta-grid">
|
||||
@@ -440,80 +487,170 @@
|
||||
.signal-value { font-size: clamp(1.6rem, 3vw, 2.2rem); line-height: 0.95; letter-spacing: -0.05em; color: #1e1812; }
|
||||
.signal-note { color: #4f463d; line-height: 1.6; font-size: 0.85rem; }
|
||||
|
||||
/* ═══ Layout ═══ */
|
||||
.brain-layout { display: grid; gap: 18px; }
|
||||
.brain-main {
|
||||
border-radius: 28px; border: 1px solid rgba(35,26,17,0.08);
|
||||
background: linear-gradient(180deg, rgba(255,252,248,0.84), rgba(244,237,229,0.74));
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
/* ═══ Toolbar ═══ */
|
||||
.toolbar-card {
|
||||
border-radius: 28px; border: 1px solid rgba(35,26,17,0.08);
|
||||
background: rgba(255,255,255,0.42); padding: 16px 16px 12px; margin-bottom: 8px;
|
||||
}
|
||||
.toolbar-head { display: flex; align-items: end; justify-content: space-between; gap: 12px; margin-bottom: 14px; }
|
||||
.toolbar-label { font-size: 11px; text-transform: uppercase; letter-spacing: 0.12em; color: #7d6f61; }
|
||||
.toolbar-title { margin-top: 4px; font-size: 1.1rem; letter-spacing: -0.035em; color: #1e1812; }
|
||||
.toolbar-meta { color: #4f463d; font-size: 0.88rem; }
|
||||
|
||||
/* ═══ Search ═══ */
|
||||
.search-section { margin-bottom: 18px; }
|
||||
.search-wrap { position: relative; }
|
||||
.search-icon { position: absolute; left: 14px; top: 50%; transform: translateY(-50%); width: 18px; height: 18px; color: #7f7365; pointer-events: none; }
|
||||
.search-icon { position: absolute; left: 16px; top: 50%; transform: translateY(-50%); width: 18px; height: 18px; color: #7f7365; pointer-events: none; }
|
||||
.search-input {
|
||||
width: 100%; padding: 12px 40px 12px 42px;
|
||||
border-radius: var(--radius); border: 1.5px solid rgba(35,26,17,0.12);
|
||||
background: rgba(255,255,255,0.92); color: #1e1812;
|
||||
font-size: var(--text-md); font-family: var(--font);
|
||||
box-shadow: inset 0 1px 0 rgba(255,255,255,0.5);
|
||||
width: 100%; padding: 14px 40px 14px 46px;
|
||||
border-radius: 28px; border: 1px solid rgba(35,26,17,0.08);
|
||||
background: rgba(255,252,248,0.68); backdrop-filter: blur(14px);
|
||||
color: #1e1812; font-size: 1rem; font-family: var(--font);
|
||||
}
|
||||
.search-input::placeholder { color: #8b7b6a; }
|
||||
.search-input:focus { outline: none; border-color: rgba(179,92,50,0.5); box-shadow: 0 0 0 4px rgba(179,92,50,0.08); }
|
||||
.search-clear { position: absolute; right: 12px; top: 50%; transform: translateY(-50%); background: none; border: none; color: #7f7365; }
|
||||
.search-input:focus { outline: none; border-color: rgba(179,92,50,0.4); box-shadow: 0 0 0 4px rgba(179,92,50,0.06); background: rgba(255,255,255,0.9); }
|
||||
.search-clear { position: absolute; right: 14px; top: 50%; transform: translateY(-50%); background: none; border: none; color: #7f7365; }
|
||||
.search-clear svg { width: 16px; height: 16px; }
|
||||
|
||||
/* ═══ Queue ═══ */
|
||||
.queue-header { display: flex; align-items: end; justify-content: space-between; gap: 14px; padding: 8px 4px 14px; }
|
||||
.queue-label { font-size: 11px; text-transform: uppercase; letter-spacing: 0.12em; color: #7d6f61; }
|
||||
.queue-title { margin-top: 4px; font-size: 1.35rem; letter-spacing: -0.04em; color: #1e1812; }
|
||||
.queue-summary { margin-top: 6px; color: #6a5d50; font-size: 0.94rem; line-height: 1.45; }
|
||||
|
||||
/* ═══ Items ═══ */
|
||||
.items-card {
|
||||
background: rgba(255,255,255,0.82); border-radius: 24px;
|
||||
border: 1px solid rgba(35,26,17,0.06); overflow: hidden;
|
||||
box-shadow: 0 16px 44px rgba(42,30,19,0.05);
|
||||
/* ═══ Masonry grid ═══ */
|
||||
.masonry {
|
||||
columns: 3;
|
||||
column-gap: 14px;
|
||||
}
|
||||
.item-row {
|
||||
display: grid; grid-template-columns: 4px minmax(0, 1fr) auto;
|
||||
align-items: center; gap: 16px; padding: 18px 16px;
|
||||
width: 100%; background: none; border: none; text-align: left;
|
||||
transition: background 160ms ease, transform 160ms ease;
|
||||
|
||||
.card {
|
||||
break-inside: avoid;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
border-radius: 20px;
|
||||
border: 1px solid rgba(35,26,17,0.08);
|
||||
background: rgba(255,252,248,0.82);
|
||||
backdrop-filter: blur(10px);
|
||||
overflow: hidden;
|
||||
margin-bottom: 14px;
|
||||
width: 100%;
|
||||
text-align: left;
|
||||
transition: transform 180ms ease, box-shadow 180ms ease, border-color 180ms ease;
|
||||
}
|
||||
.item-row:hover { background: rgba(255,248,242,0.88); transform: translateX(2px); }
|
||||
.item-row + .item-row { border-top: 1px solid rgba(35,26,17,0.07); }
|
||||
.card:hover {
|
||||
transform: translateY(-3px);
|
||||
box-shadow: 0 12px 32px rgba(42,30,19,0.08);
|
||||
border-color: rgba(35,26,17,0.14);
|
||||
}
|
||||
.card:active { transform: scale(0.985); }
|
||||
|
||||
.row-accent { align-self: stretch; border-radius: 999px; background: rgba(35,26,17,0.08); }
|
||||
.row-accent.processing { background: linear-gradient(180deg, #f6a33a, #e28615); }
|
||||
.row-accent.failed { background: linear-gradient(180deg, #ff5d45, #ef3d2f); }
|
||||
.card.is-processing { opacity: 0.7; }
|
||||
|
||||
.item-info { flex: 1; min-width: 0; }
|
||||
.item-name { font-size: 1rem; font-weight: 700; color: #1e1812; line-height: 1.25; }
|
||||
.item-meta { display: flex; flex-wrap: wrap; gap: 8px 12px; margin-top: 6px; color: #5d5248; font-size: 0.88rem; }
|
||||
.meta-folder { font-weight: 600; color: #8c7b69; }
|
||||
.meta-url { color: #7d6f61; }
|
||||
.meta-tag { background: rgba(35,26,17,0.06); padding: 1px 8px; border-radius: 999px; font-size: 0.8rem; color: #5d5248; }
|
||||
/* Card thumbnail */
|
||||
.card-thumb {
|
||||
width: 100%;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
background: rgba(244,237,229,0.6);
|
||||
}
|
||||
.card-thumb img {
|
||||
width: 100%;
|
||||
height: auto;
|
||||
display: block;
|
||||
object-fit: cover;
|
||||
max-height: 240px;
|
||||
}
|
||||
.card-processing-overlay {
|
||||
position: absolute; inset: 0;
|
||||
background: rgba(30,24,18,0.6);
|
||||
display: flex; align-items: center; justify-content: center; gap: 6px;
|
||||
color: white; font-size: 0.85rem; font-weight: 600;
|
||||
}
|
||||
.processing-dot {
|
||||
width: 6px; height: 6px; border-radius: 50%; background: #f6a33a;
|
||||
animation: pulse 1.5s ease-in-out infinite;
|
||||
}
|
||||
@keyframes pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.3; } }
|
||||
|
||||
.item-tail { display: flex; align-items: center; gap: 12px; margin-left: auto; }
|
||||
.conf-dot { width: 8px; height: 8px; border-radius: 50%; }
|
||||
.conf-dot.high { background: #059669; }
|
||||
.conf-dot.med { background: #D97706; }
|
||||
.status-badge { font-size: 11px; font-weight: 700; padding: 5px 10px; border-radius: 999px; flex-shrink: 0; }
|
||||
.processing-badge { background: rgba(217,119,6,0.12); color: #9a5d09; }
|
||||
.status-badge.error { background: var(--error-dim); color: var(--error); }
|
||||
.row-chevron { width: 14px; height: 14px; color: #7d6f61; flex-shrink: 0; opacity: 0.7; }
|
||||
/* Note card body */
|
||||
.card-note-body {
|
||||
padding: 18px 18px 0;
|
||||
font-size: 0.92rem;
|
||||
color: #3d342c;
|
||||
line-height: 1.65;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.empty { padding: 48px; text-align: center; color: #5f564b; font-size: 1rem; }
|
||||
.card-placeholder {
|
||||
padding: 32px;
|
||||
display: flex; align-items: center; justify-content: center; gap: 6px;
|
||||
color: #8c7b69; font-size: 0.85rem;
|
||||
background: rgba(244,237,229,0.4);
|
||||
}
|
||||
|
||||
/* Card content */
|
||||
.card-content {
|
||||
padding: 14px 18px 16px;
|
||||
}
|
||||
|
||||
.card-title {
|
||||
font-size: 0.95rem;
|
||||
font-weight: 700;
|
||||
color: #1e1812;
|
||||
line-height: 1.3;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.card-domain {
|
||||
font-size: 0.8rem;
|
||||
color: #8c7b69;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.card-summary {
|
||||
font-size: 0.82rem;
|
||||
color: #5c5046;
|
||||
line-height: 1.5;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.card-footer {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.card-tags {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.card-tag {
|
||||
background: rgba(35,26,17,0.06);
|
||||
padding: 2px 8px;
|
||||
border-radius: 999px;
|
||||
font-size: 0.72rem;
|
||||
color: #5d5248;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.card-meta {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-size: 0.75rem;
|
||||
color: #8c7b69;
|
||||
}
|
||||
|
||||
.card-folder {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
/* Skeleton cards */
|
||||
.skeleton-card {
|
||||
height: 200px;
|
||||
background: linear-gradient(90deg, rgba(244,237,229,0.5) 25%, rgba(255,252,248,0.8) 50%, rgba(244,237,229,0.5) 75%);
|
||||
background-size: 200% 100%;
|
||||
animation: shimmer 1.5s infinite;
|
||||
margin-bottom: 14px;
|
||||
break-inside: avoid;
|
||||
}
|
||||
|
||||
/* Empty state */
|
||||
.empty-state {
|
||||
padding: 64px 32px;
|
||||
text-align: center;
|
||||
}
|
||||
.empty-icon { color: #8c7b69; margin-bottom: 16px; }
|
||||
.empty-title { font-size: 1.2rem; font-weight: 700; color: #1e1812; margin-bottom: 6px; }
|
||||
.empty-desc { font-size: 0.95rem; color: #6a5d50; }
|
||||
|
||||
/* ═══ Detail sheet ═══ */
|
||||
.detail-overlay {
|
||||
@@ -545,8 +682,20 @@
|
||||
}
|
||||
.detail-url:hover { color: #1e1812; text-decoration: underline; }
|
||||
|
||||
.detail-content {
|
||||
margin-bottom: 20px; padding-bottom: 20px;
|
||||
border-bottom: 1px solid rgba(35,26,17,0.08);
|
||||
}
|
||||
.content-label {
|
||||
font-size: 11px; text-transform: uppercase;
|
||||
letter-spacing: 0.12em; color: #7d6f61; margin-bottom: 8px;
|
||||
}
|
||||
.content-body {
|
||||
font-size: 1rem; color: #1e1812; line-height: 1.7;
|
||||
white-space: pre-wrap; word-break: break-word;
|
||||
}
|
||||
.detail-summary {
|
||||
font-size: 1rem; color: #3d342c; line-height: 1.6;
|
||||
font-size: 0.95rem; color: #3d342c; line-height: 1.6;
|
||||
margin-bottom: 20px; padding-bottom: 20px;
|
||||
border-bottom: 1px solid rgba(35,26,17,0.08);
|
||||
}
|
||||
@@ -591,10 +740,14 @@
|
||||
@keyframes sheetIn { from { opacity: 0; transform: translateX(20px); } to { opacity: 1; transform: translateX(0); } }
|
||||
|
||||
/* ═══ Mobile ═══ */
|
||||
@media (max-width: 1100px) {
|
||||
.masonry { columns: 2; }
|
||||
}
|
||||
@media (max-width: 768px) {
|
||||
.brain-command { display: grid; gap: 14px; }
|
||||
.command-actions { justify-items: start; }
|
||||
.signal-strip { grid-template-columns: 1fr 1fr; }
|
||||
.masonry { columns: 1; }
|
||||
.detail-sheet { width: 100%; padding: 20px; }
|
||||
.detail-meta-grid { grid-template-columns: 1fr; }
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user