/* === 4. Layout shell: body & #app ==================================== */ body { font-family: var(--font-sans); background-color: var(--surface-page); color: var(--content); line-height: 1.5; overflow: hidden; } #app { display: flex; flex-direction: column; height: 100vh; } /* === 4. Layout shell: main =========================================== */ .main { display: flex; flex: 1; overflow: hidden; } /* === 5. Header ========================================================== */ .header { display: flex; justify-content: space-between; align-items: center; padding: var(--gap-peer) var(--gap-section); background-color: var(--surface-overlay); border-bottom: var(--border-width) solid var(--border); -webkit-user-select: none; user-select: none; position: relative; } .header-actions { display: flex; gap: var(--gap-peer); align-items: center; } /* Carries `.field` in the markup: shape is below, fill and inset edge come from the generated layout.css. Width is the only thing that made this a different rule from a form field, and it is all that is left here. */ .search-input { width: 180px; } /* `.search-spinner` and `@keyframes spin` are gone (2026-08-26). They were the only rotation left in this file, and a spinner turns at a rate it invented: it reads as progress on a wait that has none to report. Search here is a local query behind a 300ms debounce, so under rule 4 of wiki `loading-and-progress-standard` there was nothing for it to say. */ .search-input::placeholder { color: var(--content-muted); } /* `.sort-select` is gone: every property it set is `.field` plus the `select.field` cursor, and its `:focus { outline: none }` was killing the generated `.field:focus-visible` ring on the one control that had no ring of its own to replace it with. The call sites carry `.field`. */ /* === 6. Sidebar ======================================================== */ .sidebar { width: 220px; min-width: 220px; background-color: var(--surface-overlay); border-right: var(--border-width) solid var(--border); display: flex; flex-direction: column; overflow-y: auto; scrollbar-gutter: stable; } .sidebar h2 { padding: var(--gap-peer) var(--gap-group); font-size: var(--font-size-sm); font-weight: 700; color: var(--content-muted); text-transform: uppercase; letter-spacing: 0.5px; border-bottom: var(--border-width) solid var(--border); } /* === 7. Items panel ==================================================== */ .items-panel { flex: 1; display: flex; flex-direction: column; overflow-y: auto; background-color: var(--surface-page); } /* `.items-list`, `.plugin-list` and `.sources-list` each wrote `list-style: none` by hand. All three carry `.list` now, which is the generated reset; `.sources-list` keeps its padding and the other two had nothing else to keep. */ /* Article rows: feed items + reading-list bookmarks. Uses the renderRow .row layout (icon / content / actions, flex + gap) plus article appearance + read/unread/selected/pinned state. Replaces the legacy .item-* layout. */ .row--article { padding: var(--gap-peer) var(--gap-section); border-bottom: var(--border-width) solid var(--border); /* 3px, wider than a frame on purpose: this edge is the state marker. */ border-left: 3px solid transparent; cursor: pointer; } .row--article:hover { background-color: var(--surface-overlay); } .row--article.is-unread { border-left-color: var(--danger); background-color: color-mix(in srgb, var(--danger) 6%, var(--surface-page)); } .row--article.is-read { color: var(--content-secondary); } .row--article.is-selected { background-color: var(--surface-sunken); border-left-color: var(--category-four); } .row--article.is-unread.is-selected { border-left-color: var(--danger); background-color: color-mix(in srgb, var(--danger) 12%, var(--surface-page)); } .row--article.is-pinned { border-left-color: var(--action); } .empty-icon { display: flex; justify-content: center; margin-bottom: var(--gap-section); color: var(--content-muted); } .empty-icon svg { width: 2.5rem; height: 2.5rem; } /* Standalone empty-state block. The items/bookmarks empty rows use `
  • `; new surfaces use BB.ui.renderEmptyState().
    ...

    No items

    */ .empty-state { display: flex; flex-direction: column; align-items: center; justify-content: center; text-align: center; padding: var(--gap-page) var(--gap-pane); color: var(--content-muted); line-height: 1.6; gap: var(--gap-section); } .empty-state-icon { font-size: 2.5rem; opacity: 0.6; } .empty-state-text { font-size: var(--font-size-base); color: var(--content-secondary); margin: 0; } .empty-state--compact { padding: var(--gap-pane) var(--gap-group); gap: var(--gap-peer); } .empty-state--compact .empty-state-icon { font-size: var(--font-size-3xl); } /* Control stack (star / pin + read dot) passed into renderRow's icon slot. The .row layout provides the gap to the content; no margin needed here. */ .article-controls { display: flex; flex-direction: column; align-items: center; min-width: 24px; } .star-btn { background: none; border: none; cursor: pointer; font-size: var(--font-size-xl); color: var(--content-muted); padding: var(--step-tight) var(--step-snug); margin: calc(-1 * var(--step-tight)) calc(-1 * var(--step-snug)); line-height: 1; } .star-btn:hover { color: var(--category-four); transform: scale(1.1); } .star-btn.starred { color: var(--category-four); } .read-indicator { width: 7px; height: 7px; border-radius: var(--radius-full); margin-top: var(--gap-bound); } .read-indicator.unread { background-color: var(--danger); } /* Content column is renderRow's .row-content (flex:1; min-width:0). */ /* Header slot holds the author/time meta line (+ mobile source badge). */ .article-meta { display: flex; justify-content: space-between; align-items: flex-start; margin-bottom: var(--step-hair); } .article-author { font-weight: 700; color: var(--danger); font-size: var(--font-size-md); } /* Title: two-line clamp (overrides .row-primary's single flex line). */ .row--article .row-primary { display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; font-weight: 400; line-height: 1.4; font-size: var(--font-size-base); } .row--article .row-secondary { font-size: var(--font-size-md); margin-top: var(--step-hair); } /* .article-source-badge (mobile-only) lives in section 38. */ .load-more { padding: var(--gap-group); text-align: center; } /* === 8. Detail panel =================================================== */ .detail-panel { width: 400px; min-width: 400px; background-color: var(--surface-overlay); border-left: var(--border-width) solid var(--border); display: none; flex-direction: column; overflow-y: auto; } .detail-header { display: flex; justify-content: flex-end; gap: var(--gap-bound); padding: var(--gap-peer); border-bottom: var(--border-width) solid var(--border); background-color: var(--surface-sunken); } .item-detail { padding: var(--gap-group); } .detail-title { font-size: var(--font-size-xl); margin-bottom: var(--gap-section); color: var(--content); font-weight: 700; line-height: 1.4; } .detail-meta { display: flex; flex-wrap: wrap; gap: var(--gap-peer); margin-bottom: var(--gap-peer); color: var(--content-secondary); font-size: var(--font-size-md); padding-bottom: var(--gap-section); border-bottom: var(--border-width) solid var(--border); } .detail-tags { display: flex; flex-wrap: wrap; gap: var(--gap-peer); margin-bottom: var(--gap-section); }