. */
.source-item--empty {
cursor: default;
flex-direction: column;
align-items: center;
text-align: center;
padding: var(--gap-pane) var(--gap-group);
color: var(--content-muted);
font-size: var(--font-size-md);
line-height: 1.5;
}
.source-item--empty:hover { background-color: transparent; }
.source-item-empty-icon {
display: flex;
justify-content: center;
margin-bottom: var(--gap-peer);
color: var(--content-muted);
}
.source-item-empty-icon svg { width: 1.5rem; height: 1.5rem; }
.source-item-empty-strong { color: var(--category-four); font-weight: 700; }
.source-item-empty-link { color: var(--category-four); }
/* sources.js "+ Saved Filter" button label. Was inline color. */
/* settings-sync.js renderConnect block. Was three inline styles. */
.sync-connect {
text-align: center;
padding: var(--gap-pane) 0;
}
.sync-connect p { margin-bottom: var(--gap-section); }
.sync-connect p + p { margin-bottom: var(--gap-pane); color: var(--content-secondary); }
/* settings-sync.js showCodeEntry spinner. Was three inline styles. */
.sync-auth-spinner {
text-align: center;
padding: var(--gap-group) 0;
color: var(--content-secondary);
}
/* settings-sync.js pricing fine-print. Class already existed but carried
inline styles; rules moved here. */
.sync-sub-fine-print {
font-size: var(--font-size-base);
opacity: 0.75;
margin-top: var(--gap-bound);
}
/* settings-sync.js subscribe button row. Layout-only; class-ifying for
consistency. */
.sync-sub-actions {
display: flex;
gap: var(--gap-peer);
margin-top: var(--gap-peer);
}
/* mobile.js action sheets: stacked full-width buttons. Replaces inline
`style.display='block'; style.width='100%'; style.marginBottom='0.5rem'`. */
.button--stacked {
display: block;
width: 100%;
margin-bottom: var(--gap-peer);
}
.button--stacked:last-child { margin-bottom: 0; }
/* app.js settings theme-import/export row. Layout-only; class-ifying. */
.theme-actions {
display: flex;
gap: var(--gap-peer);
margin-top: var(--gap-peer);
}
/* Default body paragraph rhythm: retires the per-`` inline
`margin-bottom: 0.75rem` pattern in feeds.js plugin-warning. */
.modal-body p + p { margin-top: var(--gap-section); }
/* === 12. Toast =========================================================
Canonical: .toast (+ .toast.success / .toast.error today;
rename to .toast-success / .toast-error in F5, add .toast-info,
.toast-undo). Helper: BB.ui.showToast; must inject zero style.cssText.
========================================================================== */
.toast-container {
position: fixed;
bottom: 1rem;
right: 1rem;
z-index: 1001;
}
.toast {
background-color: var(--surface-raised);
border: var(--border-width) solid var(--border);
border-radius: var(--radius-sm);
padding: var(--gap-peer) var(--gap-group);
margin-top: var(--gap-peer);
box-shadow: var(--bevel-raised), var(--shadow-brutal);
animation: slideIn 0.3s ease-out;
font-weight: 500;
font-size: var(--font-size-base);
}
.toast.success { border-left: 4px solid var(--success); background-color: color-mix(in srgb, var(--success) 8%, var(--surface-page)); }
.toast.error { border-left: 4px solid var(--danger); background-color: color-mix(in srgb, var(--danger) 8%, var(--surface-page)); }
.toast-undo {
border-left: 4px solid var(--category-four);
background-color: color-mix(in srgb, var(--category-four) 10%, var(--surface-page));
display: flex;
align-items: center;
gap: var(--gap-bound);
}
.undo-message { flex: 1; }
.undo-btn {
background: none;
border: var(--border-width) solid currentColor;
border-radius: var(--radius-sm);
color: var(--content);
cursor: pointer;
padding: var(--step-tight) var(--step-roomy);
font-size: var(--font-size-md);
font-weight: 600;
}
.undo-btn:hover { background-color: color-mix(in srgb, var(--category-four) 20%, transparent); }
.undo-countdown {
font-size: var(--font-size-xs);
color: var(--content-muted);
min-width: 1.5rem;
text-align: right;
font-variant-numeric: tabular-nums;
}
@keyframes slideIn {
from { transform: translateX(100%); opacity: 0; }
to { transform: translateX(0); opacity: 1; }
}
/* === 13. Tag ===========================================================
Canonical: .tag[data-color], display chip (detail panel, source/bookmark
tags). Size modifier .tag--xs. .tag-chip is the separate interactive
filter-chip (tag bars): it toggles (cursor + .active), unlike a display tag.
Source tags use .tag.tag--xs[data-color="muted"]; .bookmark-tag composes on
.tag with a click-to-filter modifier.
========================================================================== */
/* Carries `.raised`, which is where the fill and the bevel come from now.
Left standing on purpose: a tag is a label and not pressable, which is the
argument that flattened `.badge` on 2026-08-05, so `.tag` being an object at
all is worth a second look. That is a decision about how the app should
look, not a place the design system was being restated, so it is recorded
rather than taken here. */
.tag {
border: var(--border-width) solid var(--border);
border-radius: var(--radius-sm);
padding: var(--step-hair) var(--step-base);
font-size: var(--font-size-sm);
color: var(--content-secondary);
}
/* F2 additions (2026-06-02): color variants via [data-color] attribute.
Consumes --category-five and --category-six.
.badge dropped off these selectors when badges moved to [data-tone]:
nothing sets [data-color] on a badge any more, so the badge halves were
matching nothing. .tag keeps the axis, since a tag names a category
rather than a state and green-because-green is the right contract for
it. That is also why the two stopped sharing: they were never saying
the same thing, only spelling it the same way. */
.tag[data-color="green"] { border-color: var(--category-two); color: var(--category-two); background-color: color-mix(in srgb, var(--category-two) 10%, transparent); }
.tag[data-color="yellow"] { border-color: var(--category-four); color: var(--content); background-color: color-mix(in srgb, var(--category-four) 12%, transparent); }
.tag[data-color="red"] { border-color: var(--category-one); color: var(--category-one); background-color: color-mix(in srgb, var(--category-one) 10%, transparent); }
.tag[data-color="blue"] { border-color: var(--category-three); color: var(--category-three); background-color: color-mix(in srgb, var(--category-three) 10%, transparent); }
.tag[data-color="cyan"] { border-color: var(--category-six); color: var(--category-six); background-color: color-mix(in srgb, var(--category-six) 10%, transparent); }
.tag[data-color="purple"] { border-color: var(--category-five); color: var(--category-five); background-color: color-mix(in srgb, var(--category-five) 10%, transparent); }
.tag[data-color="muted"] { border-color: var(--border); color: var(--content-muted); background-color: var(--surface-sunken); }
.tag--xs { font-size: var(--font-size-xxs); padding: 0 var(--step-snug); } /* mirrors .badge--xs */
/* .badge: smaller and more compact than .tag; used for counts and inline
status indicators.
The colour axis is [data-tone], which makeover-webview emits, replacing
the [data-color] axis this file used to carry. The old axis named the
paint (green, yellow, red, blue) and the new one names the meaning, so
a badge now says what it is rather than what colour it came out. The
swap is close to free here because three of the four category tokens
were already the same value under a second name: category-one is
danger, category-two is success, category-four is warning.
The base text colour is the generated one. What stays below is the box:
the border and the type.
The raised fill and bevel this rule used to add are gone (2026-08-05).
Token::Badge is Depth::Flat because a label is not pressable, GoingsOn's
badges are flat, and BB was the only app dressing one as an object. A
divergence nothing else shares is drift, whatever the comment above it
said. */
.badge {
display: inline-flex;
align-items: center;
gap: var(--gap-bound);
border: var(--border-width) solid var(--border);
border-radius: var(--radius-sm);
padding: 0 var(--step-snug);
font-size: var(--font-size-xs);
font-weight: 600;
line-height: 1.3;
white-space: nowrap;
}
/* Filled is this app's own variant and has no counterpart in the
description, which tones the text and leaves the fill alone. It is kept
because a filled badge is how a recommendation reads at a glance, and it
is driven off the same tone attribute so there is still one axis. */
.badge--filled { border: none; }
.badge--filled[data-tone="info"] { background-color: var(--info); color: var(--content-on-action); }
.badge--filled[data-tone="success"] { background-color: var(--success); color: var(--content-on-action); }
.badge--filled[data-tone="warning"] { background-color: var(--warning); color: var(--content); }
.badge--filled[data-tone="danger"] { background-color: var(--danger); color: var(--content-on-action); }
.detail-body {
color: var(--content);
line-height: 1.7;
font-size: var(--font-size-base);
overflow-wrap: break-word;
word-break: break-word;
}
.detail-body p { margin-bottom: 0.5em; }
.detail-body pre {
white-space: pre-wrap;
background-color: var(--surface-sunken);
padding: var(--gap-peer);
border-radius: var(--radius-sm);
overflow-x: auto;
}
.detail-actions {
display: flex;
gap: var(--gap-peer);
margin-top: var(--gap-section);
padding-top: var(--gap-section);
border-top: var(--border-width) solid var(--border);
}
/* === 14. Context menu ==================================================
Canonical: .context-menu (state: .visible). Items: .context-menu-item
(+ .context-menu-item.danger; rename to --danger in F5). Charter
target: add BB.ui.showContextMenu(x, y, items) helper.
========================================================================== */
.context-menu {
position: fixed;
z-index: 1100;
background: var(--surface-raised);
border: var(--border-width) solid var(--border);
border-radius: var(--radius-sm);
box-shadow: var(--bevel-raised), var(--shadow-brutal);
padding: var(--gap-bound);
min-width: 140px;
}
.context-menu-item {
display: block;
width: 100%;
padding: var(--gap-peer) var(--gap-group);
border: none;
border-radius: var(--radius-sm);
background: none;
color: var(--content);
font-size: var(--font-size-md);
text-align: left;
cursor: pointer;
}
.context-menu-item:hover {
background-color: var(--surface-sunken);
}
/* === 15. Health indicator ==============================================
STATE-BY-COLOR-ONLY VIOLATION (F3): pair color with shape (--broken
= hollow ring, --rate_limited = dashed) + aria-label. F5 fix.
========================================================================== */
.health-dot {
display: inline-block;
width: 8px;
height: 8px;
border-radius: var(--radius-full);
margin-right: var(--gap-bound);
flex-shrink: 0;
}
/* F5 (2026-06-02): dropped legacy color-only .health-* classes; render
site (sources.js) and the sources render tests updated to use [data-health]. */
/* F3 fix (2026-06-02): state-by-color violation retired. Each state now
pairs color with a SHAPE modifier via [data-health], so the indicator
is distinguishable in monochrome / for color-blind users. Render
site must also pass an `aria-label` for screen readers. */
.health-dot[data-health="green"] { background-color: var(--success); }
.health-dot[data-health="yellow"],
.health-dot[data-health="rate_limited"] {
background-color: var(--category-four);
border: 1.5px dashed var(--shadow-color);
box-sizing: border-box;
}
.health-dot[data-health="red"] { background-color: var(--danger); position: relative; }
.health-dot[data-health="red"]::after,
.health-dot[data-health="auth_error"]::after,
.health-dot[data-health="config_error"]::after {
content: '!';
position: absolute;
top: 50%; left: 50%;
transform: translate(-50%, -55%);
color: var(--content-on-action);
font-size: 7px;
font-weight: 900;
line-height: 1;
pointer-events: none;
}
.health-dot[data-health="auth_error"],
.health-dot[data-health="config_error"] {
background-color: var(--danger);
position: relative;
}
.health-dot[data-health="circuit_broken"] {
background-color: transparent;
border: 2px solid var(--content-muted);
box-sizing: border-box;
}
/* === 16. Skeleton & loading ============================================
Canonical: .skeleton-item, .skeleton-line.{short,medium,long}. Charter
target (F5): add BB.ui.renderSkeleton helper; widen first-paint usage
beyond items panel.
========================================================================== */
.skeleton-item {
display: flex;
padding: var(--gap-peer) var(--gap-section);
border-bottom: var(--border-width) solid var(--border);
animation: shimmer 1.5s infinite;
}
.skeleton-item .skeleton-indicators { width: 24px; margin-right: var(--gap-bound); }
.skeleton-item .skeleton-content { flex: 1; }
.skeleton-line {
height: 0.75rem;
border-radius: var(--radius-sm);
background: linear-gradient(90deg, var(--surface-sunken) 25%, var(--border) 50%, var(--surface-sunken) 75%);
background-size: 200% 100%;
animation: shimmer 1.5s infinite;
}
.skeleton-line.short { width: 40%; }
.skeleton-line.long { width: 90%; margin-top: var(--gap-peer); }
@keyframes shimmer {
0% { background-position: 200% 0; }
100% { background-position: -200% 0; }
}
/* === 17. Progress bar ================================================== */
.progress-bar-container {
position: absolute;
bottom: 0;
left: 0;
right: 0;
height: 3px;
background-color: var(--border);
box-shadow: var(--bevel-inset);
overflow: hidden;
}
/* Kept motion: the fill moves through space, which is period-correct. State
feedback that only recolors is not, and was dropped across the file. */
.progress-bar {
height: 100%;
background-color: var(--category-four);
transition: width var(--transition-slow) ease;
}
/* The two numbers the bar is drawn from, said out loud. A 3px bar at the foot
of the header reports roughly-a-quarter and nothing else; this says which
quarter of what. Empty until something has counted, so a refresh that has not
heard from a feed yet shows a trough and no claim. */
.progress-reading {
position: absolute;
right: var(--gap-section);
bottom: var(--gap-bound);
color: var(--content-muted);
font-size: var(--font-size-xs);
}
/* Toast with action */
.toast-action {
display: inline-block;
margin-left: var(--gap-bound);
background: none;
border: var(--border-width) solid currentColor;
border-radius: var(--radius-sm);
color: inherit;
cursor: pointer;
padding: var(--step-hair) var(--step-base);
font-size: var(--font-size-md);
font-weight: 600;
}
.toast-action:hover { opacity: 0.8; }
/* === 18. Update banner ================================================
z-index 1200: above mobile nav (1100/1101) and context menus, below
any future fullscreen overlay. F5: lowered from 9999 (was unnecessarily
high; sat above modals which made it impossible to dismiss when a
modal opened post-banner-show).
========================================================================== */
.update-banner {
position: fixed;
bottom: 1rem;
right: 1rem;
background: var(--surface-overlay);
border: var(--border-width) solid var(--border);
border-radius: var(--radius-sm);
padding: var(--gap-peer) var(--gap-group);
z-index: 1200;
max-width: 320px;
box-shadow: var(--bevel-raised), var(--shadow-brutal);
font-size: var(--font-size-base);
}
/* === 19. Health popover ================================================
Floating panel from gear icon. Actions use the .button family (.hp-actions .button
fills the row). .hp-row is a one-off label/value line (kept).
========================================================================== */
.health-popover {
position: fixed;
z-index: 100;
background: var(--surface-overlay);
border: var(--border-width) solid var(--border);
border-radius: var(--radius-sm);
box-shadow: var(--bevel-raised), var(--shadow-brutal);
padding: var(--gap-group);
min-width: 220px;
max-width: 320px;
font-size: var(--font-size-md);
}
.hp-title {
font-weight: 700;
margin-bottom: var(--gap-peer);
padding-bottom: var(--gap-peer);
border-bottom: var(--border-width) solid var(--border);
}
.hp-row {
display: flex;
justify-content: space-between;
gap: var(--gap-bound);
padding: var(--gap-bound) 0;
}
.hp-row-error {
flex-direction: column;
gap: var(--step-hair);
}
.hp-label {
color: var(--content-muted);
flex-shrink: 0;
}
.hp-value {
text-align: right;
word-break: break-word;
}
.hp-row-error .hp-value {
text-align: left;
font-size: var(--font-size-sm);
color: var(--content-secondary);
}
.hp-green { color: var(--success); }
.hp-yellow { color: var(--category-four); }
.hp-red { color: var(--danger); }
.hp-grey { color: var(--content-muted); }
.hp-actions {
display: flex;
gap: var(--gap-peer);
margin-top: var(--gap-section);
padding-top: var(--gap-peer);
border-top: var(--border-width) solid var(--border);
}
/* Popover actions are the .button family (Mark Read / Edit / Tags / Delete);
they fill the row equally. */
.hp-actions .button { flex: 1; }