max / balanced_breakfast
- Co-Authored-By
- Claude Opus 5 (1M context) <noreply@anthropic.com>
- Claude-Session
- https://claude.ai/code/session_01EEmeiSJnmyL98QzA5Dwsvz
9 files changed,
+1485 insertions,
-438 deletions
| @@ -98,7 +98,7 @@ | |||
| 98 | 98 | | Database layer | `crates/bb-db/src/` | | |
| 99 | 99 | | Tauri commands | `src-tauri/src/commands/` | | |
| 100 | 100 | | Frontend JS | `src-tauri/frontend/js/` | | |
| 101 | - | | Styles | `src-tauri/frontend/css/styles.css` | | |
| 101 | + | | Styles | `src-tauri/frontend/css/` (styles, shell, components, features, mobile, responsive) | | |
| 102 | 102 | | Bundled plugins | `plugins/` | | |
| 103 | 103 | | Plugin authoring guide | `docs/plugin_authoring.md` | | |
| 104 | 104 | | Architecture | `docs/architecture.md` | |
| @@ -255,7 +255,7 @@ | |||
| 255 | 255 | ||
| 256 | 256 | ## CSS | |
| 257 | 257 | ||
| 258 | - | Single stylesheet at `frontend/css/styles.css`. Uses CSS variables set by themes.js at runtime. Theme colors come from the TOML themes `makeover` ships, materialized into `src-tauri/themes/` by `build.rs` and loaded via the `get_theme` Tauri command. | |
| 258 | + | Six hand-written stylesheets in `frontend/css/`, one per band of the structure map in `styles.css`: styles (foundations), shell, components, features, mobile, responsive. They are linked in that order by `index.html` and the cascade depends on it. Uses CSS variables set by themes.js at runtime. Theme colors come from the TOML themes `makeover` ships, materialized into `src-tauri/themes/` by `build.rs` and loaded via the `get_theme` Tauri command. | |
| 259 | 259 | ||
| 260 | 260 | Theme-derived CSS variables: | |
| 261 | 261 | ||
| @@ -294,6 +294,6 @@ | |||
| 294 | 294 | | Sync settings | `src-tauri/frontend/js/settings-sync.js` | | |
| 295 | 295 | | OTA updater | `src-tauri/frontend/js/updater.js` | | |
| 296 | 296 | | App bootstrap | `src-tauri/frontend/js/app.js` | | |
| 297 | - | | Styles | `src-tauri/frontend/css/styles.css` | | |
| 297 | + | | Styles | `src-tauri/frontend/css/` (six files, one per band) | | |
| 298 | 298 | | Entry point | `src-tauri/frontend/index.html` | | |
| 299 | 299 | | JS tests | `src-tauri/frontend/js/tests/` | |
| @@ -29,6 +29,11 @@ | |||
| 29 | 29 | <link rel="stylesheet" href="css/timing.css"> | |
| 30 | 30 | <link rel="stylesheet" href="css/layout.css"> | |
| 31 | 31 | <link rel="stylesheet" href="css/styles.css"> | |
| 32 | + | <link rel="stylesheet" href="css/shell.css"> | |
| 33 | + | <link rel="stylesheet" href="css/components.css"> | |
| 34 | + | <link rel="stylesheet" href="css/features.css"> | |
| 35 | + | <link rel="stylesheet" href="css/mobile.css"> | |
| 36 | + | <link rel="stylesheet" href="css/responsive.css"> | |
| 32 | 37 | </head> | |
| 33 | 38 | <body> | |
| 34 | 39 | <div id="app"> |
| @@ -38,6 +38,10 @@ | |||
| 38 | 38 | ||
| 39 | 39 | ---------------------------------------------------------------------------- | |
| 40 | 40 | FILE STRUCTURE: search by BAND name (uppercase anchors) to navigate. | |
| 41 | + | Each band names the file it lives in. The bands were declared here long | |
| 42 | + | before they were separate files; splitting them was following this map, | |
| 43 | + | not imposing a new one. index.html links them in this order and the | |
| 44 | + | cascade depends on it. | |
| 41 | 45 | Per-section titles use `=== N. Title ===` headers; numbered for stability, | |
| 42 | 46 | not because order matters. Bands group related sections; cascade order is | |
| 43 | 47 | load-bearing only inside the RESPONSIVE LAYERS and TOUCH OVERRIDES bands. | |
| @@ -46,21 +50,21 @@ | |||
| 46 | 50 | Gaps in the numbering (3, 21, 25) are sections not yet needed; add them in | |
| 47 | 51 | their numbered slot. Keep new sections in order. | |
| 48 | 52 | ||
| 49 | - | BAND: FOUNDATIONS | |
| 53 | + | BAND: FOUNDATIONS styles.css (this file) | |
| 50 | 54 | 1. Design System Variables: tokens (:root) | |
| 51 | 55 | 2. CSS Reset & Base | |
| 52 | 56 | 2b. Utility Classes: layout primitives, | |
| 53 | 57 | margin, text | |
| 54 | 58 | 3. Keyframes & Animation | |
| 55 | 59 | ||
| 56 | - | BAND: APP SHELL & CHROME | |
| 60 | + | BAND: APP SHELL & CHROME shell.css | |
| 57 | 61 | 4. Layout shell: #app, .main | |
| 58 | 62 | 5. Header | |
| 59 | 63 | 6. Sidebar | |
| 60 | 64 | 7. Items panel | |
| 61 | 65 | 8. Detail panel | |
| 62 | 66 | ||
| 63 | - | BAND: COMPONENT PRIMITIVES | |
| 67 | + | BAND: COMPONENT PRIMITIVES components.css | |
| 64 | 68 | 9. Buttons: .button family | |
| 65 | 69 | 10. Forms: .form-group, .field | |
| 66 | 70 | 11. Modal: .modal-overlay/.modal-content | |
| @@ -73,7 +77,7 @@ | |||
| 73 | 77 | 18. Update banner: .update-banner | |
| 74 | 78 | 19. Health popover: .health-popover | |
| 75 | 79 | ||
| 76 | - | BAND: FEATURE SURFACES | |
| 80 | + | BAND: FEATURE SURFACES features.css | |
| 77 | 81 | 20. Sources list / sidebar entries | |
| 78 | 82 | 21. Items list / read indicator / star button | |
| 79 | 83 | 22. Reader-expanded mode | |
| @@ -86,11 +90,11 @@ | |||
| 86 | 90 | 29. Sync settings (utility classes) | |
| 87 | 91 | 30. Reading list / Bookmarks | |
| 88 | 92 | ||
| 89 | - | BAND: UTILITIES | |
| 93 | + | BAND: UTILITIES features.css | |
| 90 | 94 | 31. Screen-reader only (.sr-only) | |
| 91 | 95 | 32. Focus indicators | |
| 92 | 96 | ||
| 93 | - | BAND: MOBILE PRIMITIVES | |
| 97 | + | BAND: MOBILE PRIMITIVES mobile.css | |
| 94 | 98 | 33. Mobile tab bar | |
| 95 | 99 | 34. Mobile more-popover | |
| 96 | 100 | 35. Mobile search bar | |
| @@ -98,12 +102,13 @@ | |||
| 98 | 102 | 37. Pull-to-refresh indicator | |
| 99 | 103 | 38. Source-badge on items (mobile-only display) | |
| 100 | 104 | ||
| 101 | - | BAND: RESPONSIVE LAYERS: cascade-ordered | |
| 105 | + | BAND: RESPONSIVE LAYERS: responsive.css, | |
| 106 | + | cascade-ordered | |
| 102 | 107 | 39. @media (max-width: 599px): mobile layout | |
| 103 | 108 | 40. @media (hover: none): touch overrides | |
| 104 | 109 | 41. @media (prefers-reduced-motion: reduce) | |
| 105 | 110 | ||
| 106 | - | BAND: SCROLLBAR | |
| 111 | + | BAND: SCROLLBAR responsive.css | |
| 107 | 112 | 42. Webkit scrollbar | |
| 108 | 113 | ============================================================================ */ | |
| 109 | 114 | ||
| @@ -275,1752 +280,3 @@ | |||
| 275 | 280 | .text-center { text-align: center; } | |
| 276 | 281 | .text-left { text-align: left; } | |
| 277 | 282 | ||
| 278 | - | /* === 4. Layout shell: body & #app ==================================== */ | |
| 279 | - | body { | |
| 280 | - | font-family: var(--font-sans); | |
| 281 | - | background-color: var(--surface-page); | |
| 282 | - | color: var(--content); | |
| 283 | - | line-height: 1.5; | |
| 284 | - | overflow: hidden; | |
| 285 | - | } | |
| 286 | - | ||
| 287 | - | #app { | |
| 288 | - | display: flex; | |
| 289 | - | flex-direction: column; | |
| 290 | - | height: 100vh; | |
| 291 | - | } | |
| 292 | - | ||
| 293 | - | /* === 4. Layout shell: main =========================================== */ | |
| 294 | - | .main { | |
| 295 | - | display: flex; | |
| 296 | - | flex: 1; | |
| 297 | - | overflow: hidden; | |
| 298 | - | } | |
| 299 | - | ||
| 300 | - | /* === 5. Header ========================================================== */ | |
| 301 | - | .header { | |
| 302 | - | display: flex; | |
| 303 | - | justify-content: space-between; | |
| 304 | - | align-items: center; | |
| 305 | - | padding: var(--gap-peer) var(--gap-section); | |
| 306 | - | background-color: var(--surface-overlay); | |
| 307 | - | border-bottom: var(--border-width) solid var(--border); | |
| 308 | - | -webkit-user-select: none; | |
| 309 | - | user-select: none; | |
| 310 | - | position: relative; | |
| 311 | - | } | |
| 312 | - | ||
| 313 | - | .header-actions { | |
| 314 | - | display: flex; | |
| 315 | - | gap: var(--gap-peer); | |
| 316 | - | align-items: center; | |
| 317 | - | } | |
| 318 | - | ||
| 319 | - | /* Carries `.field` in the markup: shape is below, fill and inset edge come | |
| 320 | - | from the generated layout.css. Width is the only thing that made this a | |
| 321 | - | different rule from a form field, and it is all that is left here. */ | |
| 322 | - | .search-input { | |
| 323 | - | width: 180px; | |
| 324 | - | } | |
| 325 | - | ||
| 326 | - | /* `.search-spinner` and `@keyframes spin` are gone (2026-08-26). They were the | |
| 327 | - | only rotation left in this file, and a spinner turns at a rate it invented: | |
| 328 | - | it reads as progress on a wait that has none to report. Search here is a | |
| 329 | - | local query behind a 300ms debounce, so under rule 4 of wiki | |
| 330 | - | `loading-and-progress-standard` there was nothing for it to say. */ | |
| 331 | - | ||
| 332 | - | .search-input::placeholder { color: var(--content-muted); } | |
| 333 | - | ||
| 334 | - | /* `.sort-select` is gone: every property it set is `.field` plus the | |
| 335 | - | `select.field` cursor, and its `:focus { outline: none }` was killing the | |
| 336 | - | generated `.field:focus-visible` ring on the one control that had no ring of | |
| 337 | - | its own to replace it with. The call sites carry `.field`. */ | |
| 338 | - | ||
| 339 | - | /* === 6. Sidebar ======================================================== */ | |
| 340 | - | .sidebar { | |
| 341 | - | width: 220px; | |
| 342 | - | min-width: 220px; | |
| 343 | - | background-color: var(--surface-overlay); | |
| 344 | - | border-right: var(--border-width) solid var(--border); | |
| 345 | - | display: flex; | |
| 346 | - | flex-direction: column; | |
| 347 | - | overflow-y: auto; | |
| 348 | - | scrollbar-gutter: stable; | |
| 349 | - | } | |
| 350 | - | ||
| 351 | - | .sidebar h2 { | |
| 352 | - | padding: var(--gap-peer) var(--gap-group); | |
| 353 | - | font-size: var(--font-size-sm); | |
| 354 | - | font-weight: 700; | |
| 355 | - | color: var(--content-muted); | |
| 356 | - | text-transform: uppercase; | |
| 357 | - | letter-spacing: 0.5px; | |
| 358 | - | border-bottom: var(--border-width) solid var(--border); | |
| 359 | - | } | |
| 360 | - | ||
| 361 | - | /* === 7. Items panel ==================================================== */ | |
| 362 | - | .items-panel { | |
| 363 | - | flex: 1; | |
| 364 | - | display: flex; | |
| 365 | - | flex-direction: column; | |
| 366 | - | overflow-y: auto; | |
| 367 | - | background-color: var(--surface-page); | |
| 368 | - | } | |
| 369 | - | ||
| 370 | - | /* `.items-list`, `.plugin-list` and `.sources-list` each wrote | |
| 371 | - | `list-style: none` by hand. All three carry `.list` now, which is the | |
| 372 | - | generated reset; `.sources-list` keeps its padding and the other two had | |
| 373 | - | nothing else to keep. */ | |
| 374 | - | ||
| 375 | - | /* Article rows: feed items + reading-list bookmarks. Uses the renderRow .row | |
| 376 | - | layout (icon / content / actions, flex + gap) plus article appearance + | |
| 377 | - | read/unread/selected/pinned state. Replaces the legacy .item-* layout. */ | |
| 378 | - | .row--article { | |
| 379 | - | padding: var(--gap-peer) var(--gap-section); | |
| 380 | - | border-bottom: var(--border-width) solid var(--border); | |
| 381 | - | /* 3px, wider than a frame on purpose: this edge is the state marker. */ | |
| 382 | - | border-left: 3px solid transparent; | |
| 383 | - | cursor: pointer; | |
| 384 | - | } | |
| 385 | - | .row--article:hover { background-color: var(--surface-overlay); } | |
| 386 | - | .row--article.is-unread { | |
| 387 | - | border-left-color: var(--danger); | |
| 388 | - | background-color: color-mix(in srgb, var(--danger) 6%, var(--surface-page)); | |
| 389 | - | } | |
| 390 | - | .row--article.is-read { color: var(--content-secondary); } | |
| 391 | - | .row--article.is-selected { | |
| 392 | - | background-color: var(--surface-sunken); | |
| 393 | - | border-left-color: var(--category-four); | |
| 394 | - | } | |
| 395 | - | .row--article.is-unread.is-selected { | |
| 396 | - | border-left-color: var(--danger); | |
| 397 | - | background-color: color-mix(in srgb, var(--danger) 12%, var(--surface-page)); | |
| 398 | - | } | |
| 399 | - | .row--article.is-pinned { border-left-color: var(--action); } | |
| 400 | - | .empty-icon { | |
| 401 | - | display: flex; | |
| 402 | - | justify-content: center; | |
| 403 | - | margin-bottom: var(--gap-section); | |
| 404 | - | color: var(--content-muted); | |
| 405 | - | } | |
| 406 | - | .empty-icon svg { width: 2.5rem; height: 2.5rem; } | |
| 407 | - | ||
| 408 | - | /* Standalone empty-state block. The items/bookmarks empty rows use | |
| 409 | - | `<li class="empty-state">`; new surfaces use BB.ui.renderEmptyState(). | |
| 410 | - | <div class="empty-state"> | |
| 411 | - | <div class="empty-state-icon">...</div> | |
| 412 | - | <p class="empty-state-text">No items</p> | |
| 413 | - | <button class="button button--primary">Add one</button> | |
| 414 | - | </div> | |
| 415 | - | */ | |
| 416 | - | .empty-state { | |
| 417 | - | display: flex; | |
| 418 | - | flex-direction: column; | |
| 419 | - | align-items: center; | |
| 420 | - | justify-content: center; | |
| 421 | - | text-align: center; | |
| 422 | - | padding: var(--gap-page) var(--gap-pane); | |
| 423 | - | color: var(--content-muted); | |
| 424 | - | line-height: 1.6; | |
| 425 | - | gap: var(--gap-section); | |
| 426 | - | } | |
| 427 | - | .empty-state-icon { | |
| 428 | - | font-size: 2.5rem; | |
| 429 | - | opacity: 0.6; | |
| 430 | - | } | |
| 431 | - | .empty-state-text { | |
| 432 | - | font-size: var(--font-size-base); | |
| 433 | - | color: var(--content-secondary); | |
| 434 | - | margin: 0; | |
| 435 | - | } | |
| 436 | - | .empty-state--compact { | |
| 437 | - | padding: var(--gap-pane) var(--gap-group); | |
| 438 | - | gap: var(--gap-peer); | |
| 439 | - | } | |
| 440 | - | .empty-state--compact .empty-state-icon { font-size: var(--font-size-3xl); } | |
| 441 | - | ||
| 442 | - | /* Control stack (star / pin + read dot) passed into renderRow's icon slot. | |
| 443 | - | The .row layout provides the gap to the content; no margin needed here. */ | |
| 444 | - | .article-controls { | |
| 445 | - | display: flex; | |
| 446 | - | flex-direction: column; | |
| 447 | - | align-items: center; | |
| 448 | - | min-width: 24px; | |
| 449 | - | } | |
| 450 | - | ||
| 451 | - | .star-btn { | |
| 452 | - | background: none; | |
| 453 | - | border: none; | |
| 454 | - | cursor: pointer; | |
| 455 | - | font-size: var(--font-size-xl); | |
| 456 | - | color: var(--content-muted); | |
| 457 | - | padding: var(--step-tight) var(--step-snug); | |
| 458 | - | margin: calc(-1 * var(--step-tight)) calc(-1 * var(--step-snug)); | |
| 459 | - | line-height: 1; | |
| 460 | - | } | |
| 461 | - | .star-btn:hover { color: var(--category-four); transform: scale(1.1); } | |
| 462 | - | .star-btn.starred { color: var(--category-four); } | |
| 463 | - | ||
| 464 | - | .read-indicator { | |
| 465 | - | width: 7px; | |
| 466 | - | height: 7px; | |
| 467 | - | border-radius: var(--radius-full); | |
| 468 | - | margin-top: var(--gap-bound); | |
| 469 | - | } | |
| 470 | - | .read-indicator.unread { background-color: var(--danger); } | |
| 471 | - | ||
| 472 | - | /* Content column is renderRow's .row-content (flex:1; min-width:0). */ | |
| 473 | - | ||
| 474 | - | /* Header slot holds the author/time meta line (+ mobile source badge). */ | |
| 475 | - | .article-meta { | |
| 476 | - | display: flex; | |
| 477 | - | justify-content: space-between; | |
| 478 | - | align-items: flex-start; | |
| 479 | - | margin-bottom: var(--step-hair); | |
| 480 | - | } | |
| 481 | - | .article-author { | |
| 482 | - | font-weight: 700; | |
| 483 | - | color: var(--danger); | |
| 484 | - | font-size: var(--font-size-md); | |
| 485 | - | } | |
| 486 | - | ||
| 487 | - | /* Title: two-line clamp (overrides .row-primary's single flex line). */ | |
| 488 | - | .row--article .row-primary { | |
| 489 | - | display: -webkit-box; | |
| 490 | - | -webkit-line-clamp: 2; | |
| 491 | - | -webkit-box-orient: vertical; | |
| 492 | - | overflow: hidden; | |
| 493 | - | font-weight: 400; | |
| 494 | - | line-height: 1.4; | |
| 495 | - | font-size: var(--font-size-base); | |
| 496 | - | } | |
| 497 | - | .row--article .row-secondary { | |
| 498 | - | font-size: var(--font-size-md); | |
| 499 | - | margin-top: var(--step-hair); | |
| 500 | - | } | |
| 501 | - | /* .article-source-badge (mobile-only) lives in section 38. */ | |
| 502 | - | ||
| 503 | - | .load-more { padding: var(--gap-group); text-align: center; } | |
| 504 | - | ||
| 505 | - | /* === 8. Detail panel =================================================== */ | |
| 506 | - | .detail-panel { | |
| 507 | - | width: 400px; | |
| 508 | - | min-width: 400px; | |
| 509 | - | background-color: var(--surface-overlay); | |
| 510 | - | border-left: var(--border-width) solid var(--border); | |
| 511 | - | display: none; | |
| 512 | - | flex-direction: column; | |
| 513 | - | overflow-y: auto; | |
| 514 | - | } | |
| 515 | - | ||
| 516 | - | .detail-header { | |
| 517 | - | display: flex; | |
| 518 | - | justify-content: flex-end; | |
| 519 | - | gap: var(--gap-bound); | |
| 520 | - | padding: var(--gap-peer); | |
| 521 | - | border-bottom: var(--border-width) solid var(--border); | |
| 522 | - | background-color: var(--surface-sunken); | |
| 523 | - | } | |
| 524 | - | ||
| 525 | - | .item-detail { padding: var(--gap-group); } | |
| 526 | - | ||
| 527 | - | .detail-title { | |
| 528 | - | font-size: var(--font-size-xl); | |
| 529 | - | margin-bottom: var(--gap-section); | |
| 530 | - | color: var(--content); | |
| 531 | - | font-weight: 700; | |
| 532 | - | line-height: 1.4; | |
| 533 | - | } | |
| 534 | - | ||
| 535 | - | .detail-meta { | |
| 536 | - | display: flex; | |
| 537 | - | flex-wrap: wrap; | |
| 538 | - | gap: var(--gap-peer); | |
| 539 | - | margin-bottom: var(--gap-peer); | |
| 540 | - | color: var(--content-secondary); | |
| 541 | - | font-size: var(--font-size-md); | |
| 542 | - | padding-bottom: var(--gap-section); | |
| 543 | - | border-bottom: var(--border-width) solid var(--border); | |
| 544 | - | } | |
| 545 | - | ||
| 546 | - | .detail-tags { | |
| 547 | - | display: flex; | |
| 548 | - | flex-wrap: wrap; | |
| 549 | - | gap: var(--gap-peer); | |
| 550 | - | margin-bottom: var(--gap-section); | |
| 551 | - | } | |
| 552 | - | ||
| 553 | - | /* === 9. Buttons ======================================================== | |
| 554 | - | Canonical: .button (+ .button--primary, .button--secondary, .button--success, .button--danger, | |
| 555 | - | .button--icon, .button--text, .button--link; size .button--sm; state .button--loading). | |
| 556 | - | Charter target: collapse remaining one-off *-btn classes onto this family. | |
| 557 | - | ========================================================================== */ | |
| 558 | - | /* The fill and the bevel are gone from here, along with the whole | |
| 559 | - | :active rule: .button, .button:hover and .button:active come from | |
| 560 | - | makeover-webview, which emits the fill and the edge together so they | |
| 561 | - | cannot disagree. What stays is everything that is not depth -- box | |
| 562 | - | model, type, cursor -- plus the disabled overrides below. | |
| 563 | - | ||
| 564 | - | Two visible consequences, both the description's answer rather than | |
| 565 | - | this file's: hover reads as --hover-surface instead of | |
| 566 | - | --surface-sunken, and pressing reads as a well. */ | |
| 567 | - | .button { | |
| 568 | - | padding: var(--gap-peer) var(--gap-group); | |
| 569 | - | border: var(--border-width) solid var(--border); | |
| 570 | - | border-radius: var(--radius-sm); | |
| 571 | - | color: var(--content); | |
| 572 | - | cursor: pointer; | |
| 573 | - | font-weight: 600; | |
| 574 | - | font-size: var(--font-size-base); | |
| 575 | - | } | |
| 576 | - | .button:hover { | |
| 577 | - | border-color: var(--border-strong); | |
| 578 | - | } | |
| 579 | - | /* Disabled keeps its bevel. The object is still there; greying the content | |
| 580 | - | is what says the action is unavailable -- and the greying is the generated | |
| 581 | - | sheet's, along with the cursor. Only the edge is this app's. */ | |
| 582 | - | .button:disabled { | |
| 583 | - | border-color: var(--content-muted); | |
| 584 | - | } | |
| 585 | - | /* The `.button:disabled:hover, .button:disabled:active` pair that used to | |
| 586 | - | sit here is gone. It existed to out-specify the generated hover and | |
| 587 | - | pressed rules, in those words, and makeover-webview 0.10.0 emits | |
| 588 | - | disabled after both and restates the rest depth, so source order settles | |
| 589 | - | it at equal specificity. Nothing has to reach (0,3,0) to win any more, | |
| 590 | - | which matters because raising a selector is what the cascade layer would | |
| 591 | - | have had to unpick. | |
| 592 | - | ||
| 593 | - | The colour, border colour and cursor above stay, and are not a candidate | |
| 594 | - | for the same deletion. Layers resolve before specificity, so the | |
| 595 | - | generated `.button:disabled` in `makeover` loses to `.button` in this | |
| 596 | - | file whatever its specificity. An app has to state the disabled variant | |
| 597 | - | of every property it sets at rest; makeover can only supply what the app | |
| 598 | - | is silent about. */ | |
| 599 | - | .button--primary { | |
| 600 | - | background-color: var(--action); | |
| 601 | - | border-color: var(--action); | |
| 602 | - | color: var(--content-on-action); | |
| 603 | - | } | |
| 604 | - | .button--primary:hover { background-color: var(--action-hover); border-color: var(--action-hover); } | |
| 605 | - | .button--success { | |
| 606 | - | background-color: var(--category-four); | |
| 607 | - | border-color: var(--category-four); | |
| 608 | - | color: var(--content-on-action); | |
| 609 | - | } | |
| 610 | - | .button--success:hover { background-color: var(--accent-yellow-light); border-color: var(--accent-yellow-light); } | |
| 611 | - | /* .button--sm: canonical small-button size (see wiki note `bb-design-system`). */ | |
| 612 | - | .button--sm { padding: var(--gap-bound) var(--gap-peer); font-size: var(--font-size-md); } | |
| 613 | - | .button--sm.active { background: var(--category-four); color: var(--content-on-action); border-color: var(--category-four); } | |
| 614 | - | ||
| 615 | - | /* .button--danger: destructive action. Consumes the previously-unused | |
| 616 | - | --accent-red-hover token. Replaces .hp-action-danger in F5. */ | |
| 617 | - | .button--danger { | |
| 618 | - | background-color: var(--danger); | |
| 619 | - | border-color: var(--danger); | |
| 620 | - | color: var(--content-on-action); | |
| 621 | - | } | |
| 622 | - | .button--danger:hover { background-color: var(--accent-red-hover); border-color: var(--accent-red-hover); } | |
| 623 | - | ||
| 624 | - | /* === N. Row primitive (F2-sub addition, 2026-06-02) =================== | |
| 625 | - | Canonical slot layout: [icon] [primary · badges] [meta] [actions]. | |
| 626 | - | Used via BB.ui.renderRow(model). Per-surface classes (.plugin-item, | |
| 627 | - | .source-item, .item, .bookmark-item, etc.) sit alongside .row on the | |
| 628 | - | outer element and add their own padding / border / hover treatment. | |
| 629 | - | F5 retires per-surface name/desc classes (.plugin-name, .plugin-desc) | |
| 630 | - | that duplicate .row-primary / .row-secondary. | |
| 631 | - | ========================================================================== */ | |
| 632 | - | .row { | |
| 633 | - | display: flex; | |
| 634 | - | align-items: flex-start; | |
| 635 | - | gap: var(--gap-bound); | |
| 636 | - | min-width: 0; | |
| 637 | - | } | |
| 638 | - | .row-icon { | |
| 639 | - | flex-shrink: 0; | |
| 640 | - | display: flex; | |
| 641 | - | align-items: center; | |
| 642 | - | justify-content: center; | |
| 643 | - | } | |
| 644 | - | .row-content { | |
| 645 | - | flex: 1; | |
| 646 | - | min-width: 0; | |
| 647 | - | } | |
| 648 | - | /* The three row text colours come from makeover-webview, which derives | |
| 649 | - | them from the same content ramp this file used to spell out by hand. | |
| 650 | - | Identical values, so nothing moves; what changes is that there is one | |
| 651 | - | place to change them. */ | |
| 652 | - | .row-primary { | |
| 653 | - | font-weight: 600; | |
| 654 | - | font-size: var(--font-size-base); | |
| 655 | - | line-height: 1.3; | |
| 656 | - | display: flex; | |
| 657 | - | align-items: center; | |
| 658 | - | gap: var(--gap-bound); | |
| 659 | - | flex-wrap: wrap; | |
| 660 | - | } | |
| 661 | - | .row-secondary { | |
| 662 | - | font-size: var(--font-size-md); | |
| 663 | - | line-height: 1.4; | |
| 664 | - | margin-top: var(--step-hair); | |
| 665 | - | } | |
| 666 | - | .row-meta { | |
| 667 | - | flex-shrink: 0; | |
| 668 | - | font-size: var(--font-size-sm); | |
| 669 | - | align-self: center; | |
| 670 | - | } | |
| 671 | - | .row-actions { | |
| 672 | - | flex-shrink: 0; | |
| 673 | - | display: flex; | |
| 674 | - | align-items: center; | |
| 675 | - | gap: var(--gap-peer); | |
| 676 | - | } | |
| 677 | - | /* The hover-reveal comes from makeover-webview now. It also toggles | |
| 678 | - | pointer-events, which the hand-written pair did not: a hidden action | |
| 679 | - | used to be invisible and still clickable, so the row carried dead | |
| 680 | - | targets a cursor could find. The `.always` escape hatch went with the | |
| 681 | - | rules it was written against; nothing ever set it. */ | |
| 682 | - | ||
| 683 | - | /* F2-sub addition: form-hint error variant (used by renderFormField when | |
| 684 | - | passed a `field.error`). */ | |
| 685 | - | .form-hint--error { | |
| 686 | - | color: var(--danger); | |
| 687 | - | font-weight: 600; | |
| 688 | - | } | |
| 689 | - | ||
| 690 | - | /* === 10. Forms ========================================================= | |
| 691 | - | Canonical: .form-group + .field. | |
| 692 | - | ||
| 693 | - | One class for every field kind, with the kind riding on the element | |
| 694 | - | (`select.field`, `textarea.field`) rather than on a second class. The F2 | |
| 695 | - | split into .form-input / .form-select / .form-textarea is retired: all | |
| 696 | - | twenty-odd call sites had .form-input on an <input>, .form-select on a | |
| 697 | - | <select> and .form-textarea on a <textarea>, with no exceptions, so the | |
| 698 | - | class was saying what the tag already said, and an element selector cannot | |
| 699 | - | be forgotten at a call site the way a modifier class can. | |
| 700 | - | ||
| 701 | - | Fill and inset edge now come from the generated layout.css, where a field | |
| 702 | - | is a Depth::Well. Every rule here used to write `background-color: | |
| 703 | - | var(--surface-page)` under `box-shadow: var(--bevel-inset)` -- a page fill | |
| 704 | - | beneath a recessed edge -- and so did .search-input, .sort-select and (with |
Lines truncated
| @@ -1,0 +1,785 @@ | |||
| 1 | + | /* === 9. Buttons ======================================================== | |
| 2 | + | Canonical: .button (+ .button--primary, .button--secondary, .button--success, .button--danger, | |
| 3 | + | .button--icon, .button--text, .button--link; size .button--sm; state .button--loading). | |
| 4 | + | Charter target: collapse remaining one-off *-btn classes onto this family. | |
| 5 | + | ========================================================================== */ | |
| 6 | + | /* The fill and the bevel are gone from here, along with the whole | |
| 7 | + | :active rule: .button, .button:hover and .button:active come from | |
| 8 | + | makeover-webview, which emits the fill and the edge together so they | |
| 9 | + | cannot disagree. What stays is everything that is not depth -- box | |
| 10 | + | model, type, cursor -- plus the disabled overrides below. | |
| 11 | + | ||
| 12 | + | Two visible consequences, both the description's answer rather than | |
| 13 | + | this file's: hover reads as --hover-surface instead of | |
| 14 | + | --surface-sunken, and pressing reads as a well. */ | |
| 15 | + | .button { | |
| 16 | + | padding: var(--gap-peer) var(--gap-group); | |
| 17 | + | border: var(--border-width) solid var(--border); | |
| 18 | + | border-radius: var(--radius-sm); | |
| 19 | + | color: var(--content); | |
| 20 | + | cursor: pointer; | |
| 21 | + | font-weight: 600; | |
| 22 | + | font-size: var(--font-size-base); | |
| 23 | + | } | |
| 24 | + | .button:hover { | |
| 25 | + | border-color: var(--border-strong); | |
| 26 | + | } | |
| 27 | + | /* Disabled keeps its bevel. The object is still there; greying the content | |
| 28 | + | is what says the action is unavailable -- and the greying is the generated | |
| 29 | + | sheet's, along with the cursor. Only the edge is this app's. */ | |
| 30 | + | .button:disabled { | |
| 31 | + | border-color: var(--content-muted); | |
| 32 | + | } | |
| 33 | + | /* The `.button:disabled:hover, .button:disabled:active` pair that used to | |
| 34 | + | sit here is gone. It existed to out-specify the generated hover and | |
| 35 | + | pressed rules, in those words, and makeover-webview 0.10.0 emits | |
| 36 | + | disabled after both and restates the rest depth, so source order settles | |
| 37 | + | it at equal specificity. Nothing has to reach (0,3,0) to win any more, | |
| 38 | + | which matters because raising a selector is what the cascade layer would | |
| 39 | + | have had to unpick. | |
| 40 | + | ||
| 41 | + | The colour, border colour and cursor above stay, and are not a candidate | |
| 42 | + | for the same deletion. Layers resolve before specificity, so the | |
| 43 | + | generated `.button:disabled` in `makeover` loses to `.button` in this | |
| 44 | + | file whatever its specificity. An app has to state the disabled variant | |
| 45 | + | of every property it sets at rest; makeover can only supply what the app | |
| 46 | + | is silent about. */ | |
| 47 | + | .button--primary { | |
| 48 | + | background-color: var(--action); | |
| 49 | + | border-color: var(--action); | |
| 50 | + | color: var(--content-on-action); | |
| 51 | + | } | |
| 52 | + | .button--primary:hover { background-color: var(--action-hover); border-color: var(--action-hover); } | |
| 53 | + | .button--success { | |
| 54 | + | background-color: var(--category-four); | |
| 55 | + | border-color: var(--category-four); | |
| 56 | + | color: var(--content-on-action); | |
| 57 | + | } | |
| 58 | + | .button--success:hover { background-color: var(--accent-yellow-light); border-color: var(--accent-yellow-light); } | |
| 59 | + | /* .button--sm: canonical small-button size (see wiki note `bb-design-system`). */ | |
| 60 | + | .button--sm { padding: var(--gap-bound) var(--gap-peer); font-size: var(--font-size-md); } | |
| 61 | + | .button--sm.active { background: var(--category-four); color: var(--content-on-action); border-color: var(--category-four); } | |
| 62 | + | ||
| 63 | + | /* .button--danger: destructive action. Consumes the previously-unused | |
| 64 | + | --accent-red-hover token. Replaces .hp-action-danger in F5. */ | |
| 65 | + | .button--danger { | |
| 66 | + | background-color: var(--danger); | |
| 67 | + | border-color: var(--danger); | |
| 68 | + | color: var(--content-on-action); | |
| 69 | + | } | |
| 70 | + | .button--danger:hover { background-color: var(--accent-red-hover); border-color: var(--accent-red-hover); } | |
| 71 | + | ||
| 72 | + | /* === N. Row primitive (F2-sub addition, 2026-06-02) =================== | |
| 73 | + | Canonical slot layout: [icon] [primary · badges] [meta] [actions]. | |
| 74 | + | Used via BB.ui.renderRow(model). Per-surface classes (.plugin-item, | |
| 75 | + | .source-item, .item, .bookmark-item, etc.) sit alongside .row on the | |
| 76 | + | outer element and add their own padding / border / hover treatment. | |
| 77 | + | F5 retires per-surface name/desc classes (.plugin-name, .plugin-desc) | |
| 78 | + | that duplicate .row-primary / .row-secondary. | |
| 79 | + | ========================================================================== */ | |
| 80 | + | .row { | |
| 81 | + | display: flex; | |
| 82 | + | align-items: flex-start; | |
| 83 | + | gap: var(--gap-bound); | |
| 84 | + | min-width: 0; | |
| 85 | + | } | |
| 86 | + | .row-icon { | |
| 87 | + | flex-shrink: 0; | |
| 88 | + | display: flex; | |
| 89 | + | align-items: center; | |
| 90 | + | justify-content: center; | |
| 91 | + | } | |
| 92 | + | .row-content { | |
| 93 | + | flex: 1; | |
| 94 | + | min-width: 0; | |
| 95 | + | } | |
| 96 | + | /* The three row text colours come from makeover-webview, which derives | |
| 97 | + | them from the same content ramp this file used to spell out by hand. | |
| 98 | + | Identical values, so nothing moves; what changes is that there is one | |
| 99 | + | place to change them. */ | |
| 100 | + | .row-primary { | |
| 101 | + | font-weight: 600; | |
| 102 | + | font-size: var(--font-size-base); | |
| 103 | + | line-height: 1.3; | |
| 104 | + | display: flex; | |
| 105 | + | align-items: center; | |
| 106 | + | gap: var(--gap-bound); | |
| 107 | + | flex-wrap: wrap; | |
| 108 | + | } | |
| 109 | + | .row-secondary { | |
| 110 | + | font-size: var(--font-size-md); | |
| 111 | + | line-height: 1.4; | |
| 112 | + | margin-top: var(--step-hair); | |
| 113 | + | } | |
| 114 | + | .row-meta { | |
| 115 | + | flex-shrink: 0; | |
| 116 | + | font-size: var(--font-size-sm); | |
| 117 | + | align-self: center; | |
| 118 | + | } | |
| 119 | + | .row-actions { | |
| 120 | + | flex-shrink: 0; | |
| 121 | + | display: flex; | |
| 122 | + | align-items: center; | |
| 123 | + | gap: var(--gap-peer); | |
| 124 | + | } | |
| 125 | + | /* The hover-reveal comes from makeover-webview now. It also toggles | |
| 126 | + | pointer-events, which the hand-written pair did not: a hidden action | |
| 127 | + | used to be invisible and still clickable, so the row carried dead | |
| 128 | + | targets a cursor could find. The `.always` escape hatch went with the | |
| 129 | + | rules it was written against; nothing ever set it. */ | |
| 130 | + | ||
| 131 | + | /* F2-sub addition: form-hint error variant (used by renderFormField when | |
| 132 | + | passed a `field.error`). */ | |
| 133 | + | .form-hint--error { | |
| 134 | + | color: var(--danger); | |
| 135 | + | font-weight: 600; | |
| 136 | + | } | |
| 137 | + | ||
| 138 | + | /* === 10. Forms ========================================================= | |
| 139 | + | Canonical: .form-group + .field. | |
| 140 | + | ||
| 141 | + | One class for every field kind, with the kind riding on the element | |
| 142 | + | (`select.field`, `textarea.field`) rather than on a second class. The F2 | |
| 143 | + | split into .form-input / .form-select / .form-textarea is retired: all | |
| 144 | + | twenty-odd call sites had .form-input on an <input>, .form-select on a | |
| 145 | + | <select> and .form-textarea on a <textarea>, with no exceptions, so the | |
| 146 | + | class was saying what the tag already said, and an element selector cannot | |
| 147 | + | be forgotten at a call site the way a modifier class can. | |
| 148 | + | ||
| 149 | + | Fill and inset edge now come from the generated layout.css, where a field | |
| 150 | + | is a Depth::Well. Every rule here used to write `background-color: | |
| 151 | + | var(--surface-page)` under `box-shadow: var(--bevel-inset)` -- a page fill | |
| 152 | + | beneath a recessed edge -- and so did .search-input, .sort-select and (with | |
| 153 | + | no bevel at all) the two mobile fields. Five hand-written answers to one | |
| 154 | + | question. goingson had the same pair and retired it the same way; its | |
| 155 | + | .field block carries the long form of this note. | |
| 156 | + | ========================================================================== */ | |
| 157 | + | .form-group { margin-bottom: var(--gap-section); } | |
| 158 | + | ||
| 159 | + | /* Shape only. The wider padding is what .search-input and .sort-select | |
| 160 | + | carried; a field inside a form group keeps the tighter one, below. */ | |
| 161 | + | .field { | |
| 162 | + | padding: var(--gap-peer) var(--gap-group); | |
| 163 | + | border: var(--border-width) solid var(--border); | |
| 164 | + | border-radius: var(--radius-sm); | |
| 165 | + | color: var(--content); | |
| 166 | + | font-size: var(--font-size-base); | |
| 167 | + | } | |
| 168 | + | ||
| 169 | + | /* A field in a form group fills it. The three fields outside one carry their | |
| 170 | + | own width, which is why this is not on .field itself. */ | |
| 171 | + | .form-group .field { | |
| 172 | + | width: 100%; | |
| 173 | + | padding: var(--gap-peer); | |
| 174 | + | } | |
| 175 | + | ||
| 176 | + | select.field { cursor: pointer; } | |
| 177 | + | ||
| 178 | + | /* layout.css draws the disabled field -- muted content, not-allowed cursor -- | |
| 179 | + | but the two rules above set colour and cursor at rest in an unlayered sheet, | |
| 180 | + | so the resting rules win and the generated disabled arm never applies. An | |
| 181 | + | app must state the disabled variant of every property it sets at rest. | |
| 182 | + | Found by adopting the generated states, not reported. The fill and the edge | |
| 183 | + | come from layout.css and are not restated here. */ | |
| 184 | + | .field:disabled, | |
| 185 | + | .field[aria-disabled="true"] { | |
| 186 | + | color: var(--content-muted); | |
| 187 | + | cursor: not-allowed; | |
| 188 | + | } | |
| 189 | + | ||
| 190 | + | textarea.field { | |
| 191 | + | font-family: inherit; | |
| 192 | + | resize: vertical; | |
| 193 | + | min-height: 4rem; | |
| 194 | + | } | |
| 195 | + | ||
| 196 | + | /* .form-label replaces the unclassed `<label>` under `.form-group`. */ | |
| 197 | + | .form-group label, | |
| 198 | + | .settings-content .form-group label, | |
| 199 | + | .form-label { | |
| 200 | + | display: block; | |
| 201 | + | margin-bottom: var(--gap-bound); | |
| 202 | + | color: var(--content-secondary); | |
| 203 | + | font-size: var(--font-size-md); | |
| 204 | + | font-weight: 700; | |
| 205 | + | } | |
| 206 | + | /* .form-row: horizontal grouping of two related fields (e.g. label + button) */ | |
| 207 | + | ||
| 208 | + | .form-actions { | |
| 209 | + | display: flex; | |
| 210 | + | justify-content: flex-end; | |
| 211 | + | gap: var(--gap-peer); | |
| 212 | + | margin-top: var(--gap-section); | |
| 213 | + | } | |
| 214 | + | ||
| 215 | + | .form-hint { | |
| 216 | + | font-size: var(--font-size-xs); | |
| 217 | + | color: var(--content-muted); | |
| 218 | + | margin-top: var(--gap-bound); | |
| 219 | + | } | |
| 220 | + | ||
| 221 | + | /* === 11. Modal ========================================================= | |
| 222 | + | Canonical: .modal-overlay (single global at #modal-overlay) + | |
| 223 | + | .modal-content (will rename to .modal-container for parity in F5). | |
| 224 | + | .tag block (13. Tag) lives below this block in file order. | |
| 225 | + | ========================================================================== */ | |
| 226 | + | .modal-overlay { | |
| 227 | + | position: fixed; | |
| 228 | + | top: 0; left: 0; right: 0; bottom: 0; | |
| 229 | + | background-color: color-mix(in srgb, var(--content) 40%, transparent); | |
| 230 | + | display: none; | |
| 231 | + | align-items: center; | |
| 232 | + | justify-content: center; | |
| 233 | + | z-index: 1000; | |
| 234 | + | } | |
| 235 | + | ||
| 236 | + | .modal-content { | |
| 237 | + | background-color: var(--surface-raised); | |
| 238 | + | border-radius: var(--radius-lg); | |
| 239 | + | width: 90%; | |
| 240 | + | max-width: 500px; | |
| 241 | + | max-height: 80vh; | |
| 242 | + | overflow-y: auto; | |
| 243 | + | /* Floating surfaces carry both: the bevel says the panel is a lit | |
| 244 | + | object, the drop shadow says it is above the page. */ | |
| 245 | + | box-shadow: var(--bevel-raised), var(--shadow-brutal-lg); | |
| 246 | + | } | |
| 247 | + | ||
| 248 | + | .modal-header { | |
| 249 | + | display: flex; | |
| 250 | + | justify-content: space-between; | |
| 251 | + | align-items: center; | |
| 252 | + | padding: var(--gap-group) var(--gap-pane); | |
| 253 | + | border-bottom: var(--border-width) solid var(--border); | |
| 254 | + | background-color: var(--surface-overlay); | |
| 255 | + | border-radius: var(--radius-lg) var(--radius-lg) 0 0; | |
| 256 | + | } | |
| 257 | + | .modal-header h2 { font-size: var(--font-size-xl); font-weight: 700; color: var(--content); } | |
| 258 | + | ||
| 259 | + | /* F3 (2026-06-02): step indicator for multi-step modal flows (OAuth, | |
| 260 | + | plugin import, encryption setup). Set via BB.ui.setModalStep(n, of). */ | |
| 261 | + | .modal-step-indicator { | |
| 262 | + | margin-left: var(--gap-bound); | |
| 263 | + | padding: var(--step-hair) var(--step-base); | |
| 264 | + | background-color: var(--surface-sunken); | |
| 265 | + | border: var(--border-width) solid var(--border); | |
| 266 | + | border-radius: var(--radius-xl); | |
| 267 | + | font-size: var(--font-size-xs); | |
| 268 | + | font-weight: 600; | |
| 269 | + | color: var(--content-muted); | |
| 270 | + | white-space: nowrap; | |
| 271 | + | } | |
| 272 | + | ||
| 273 | + | .modal-body { padding: var(--gap-pane); } | |
| 274 | + | ||
| 275 | + | ||
| 276 | + | ||
| 277 | + | /* Carries `.card`: a pressable raised object with a hover fill and an inset | |
| 278 | + | pressed state, which is what these three rules wrote by hand. The fill, the | |
| 279 | + | bevel, the hover and the press all come from layout.css now, wrapped in | |
| 280 | + | (hover: hover) and (pointer: fine) there so a fingertip gets no phantom | |
| 281 | + | hover. What is left here is the box and the accent border. */ | |
| 282 | + | .plugin-item { | |
| 283 | + | padding: var(--gap-group); | |
| 284 | + | border: var(--border-width) solid var(--border); | |
| 285 | + | border-radius: var(--radius-sm); | |
| 286 | + | margin-bottom: var(--gap-peer); | |
| 287 | + | cursor: pointer; | |
| 288 | + | } | |
| 289 | + | .plugin-item:hover { | |
| 290 | + | border-color: var(--category-four); | |
| 291 | + | } | |
| 292 | + | ||
| 293 | + | /* F5 (2026-06-02): dropped .plugin-name and .plugin-desc; plugin-list | |
| 294 | + | migrated to BB.ui.renderRow in F2-sub; .row-primary / .row-secondary | |
| 295 | + | carry the typography now. */ | |
| 296 | + | ||
| 297 | + | /* F2-sub (2026-06-02): intro paragraph at the top of multi-step modals | |
| 298 | + | (replaces inline `style="margin-bottom:1rem;color:var(--content-secondary)"` | |
| 299 | + | in feeds.js plugin picker). */ | |
| 300 | + | .modal-intro { | |
| 301 | + | margin-bottom: var(--gap-section); | |
| 302 | + | color: var(--content-secondary); | |
| 303 | + | font-size: var(--font-size-base); | |
| 304 | + | } | |
| 305 | + | ||
| 306 | + | /* F4 additions (2026-06-02): replace inline styles previously hard-coded | |
| 307 | + | in JS render paths. Each rule below retires a specific style="..." or | |
| 308 | + | style.cssText violation flagged in the F4 catalogue. */ | |
| 309 | + | ||
| 310 | + | /* sources.js empty-state (no feeds yet). Was inline cssText + emoji | |
| 311 | + | wrapper. F5 may further consolidate onto the .empty-state block once | |
| 312 | + | the row primitive migration lands; this fix lives in the sidebar list | |
| 313 | + | so it stays a styled <li>. */ | |
| 314 | + | .source-item--empty { | |
| 315 | + | cursor: default; | |
| 316 | + | flex-direction: column; | |
| 317 | + | align-items: center; | |
| 318 | + | text-align: center; | |
| 319 | + | padding: var(--gap-pane) var(--gap-group); | |
| 320 | + | color: var(--content-muted); | |
| 321 | + | font-size: var(--font-size-md); | |
| 322 | + | line-height: 1.5; | |
| 323 | + | } | |
| 324 | + | .source-item--empty:hover { background-color: transparent; } | |
| 325 | + | .source-item-empty-icon { | |
| 326 | + | display: flex; | |
| 327 | + | justify-content: center; | |
| 328 | + | margin-bottom: var(--gap-peer); | |
| 329 | + | color: var(--content-muted); | |
| 330 | + | } | |
| 331 | + | .source-item-empty-icon svg { width: 1.5rem; height: 1.5rem; } | |
| 332 | + | .source-item-empty-strong { color: var(--category-four); font-weight: 700; } | |
| 333 | + | .source-item-empty-link { color: var(--category-four); } | |
| 334 | + | ||
| 335 | + | /* sources.js "+ Saved Filter" button label. Was inline color. */ | |
| 336 | + | ||
| 337 | + | /* settings-sync.js renderConnect block. Was three inline styles. */ | |
| 338 | + | .sync-connect { | |
| 339 | + | text-align: center; | |
| 340 | + | padding: var(--gap-pane) 0; | |
| 341 | + | } | |
| 342 | + | .sync-connect p { margin-bottom: var(--gap-section); } | |
| 343 | + | .sync-connect p + p { margin-bottom: var(--gap-pane); color: var(--content-secondary); } | |
| 344 | + | ||
| 345 | + | /* settings-sync.js showCodeEntry spinner. Was three inline styles. */ | |
| 346 | + | .sync-auth-spinner { | |
| 347 | + | text-align: center; | |
| 348 | + | padding: var(--gap-group) 0; | |
| 349 | + | color: var(--content-secondary); | |
| 350 | + | } | |
| 351 | + | ||
| 352 | + | /* settings-sync.js pricing fine-print. Class already existed but carried | |
| 353 | + | inline styles; rules moved here. */ | |
| 354 | + | .sync-sub-fine-print { | |
| 355 | + | font-size: var(--font-size-base); | |
| 356 | + | opacity: 0.75; | |
| 357 | + | margin-top: var(--gap-bound); | |
| 358 | + | } | |
| 359 | + | ||
| 360 | + | /* settings-sync.js subscribe button row. Layout-only; class-ifying for | |
| 361 | + | consistency. */ | |
| 362 | + | .sync-sub-actions { | |
| 363 | + | display: flex; | |
| 364 | + | gap: var(--gap-peer); | |
| 365 | + | margin-top: var(--gap-peer); | |
| 366 | + | } | |
| 367 | + | ||
| 368 | + | /* mobile.js action sheets: stacked full-width buttons. Replaces inline | |
| 369 | + | `style.display='block'; style.width='100%'; style.marginBottom='0.5rem'`. */ | |
| 370 | + | .button--stacked { | |
| 371 | + | display: block; | |
| 372 | + | width: 100%; | |
| 373 | + | margin-bottom: var(--gap-peer); | |
| 374 | + | } | |
| 375 | + | .button--stacked:last-child { margin-bottom: 0; } | |
| 376 | + | ||
| 377 | + | /* app.js settings theme-import/export row. Layout-only; class-ifying. */ | |
| 378 | + | .theme-actions { | |
| 379 | + | display: flex; | |
| 380 | + | gap: var(--gap-peer); | |
| 381 | + | margin-top: var(--gap-peer); | |
| 382 | + | } | |
| 383 | + | ||
| 384 | + | /* Default body paragraph rhythm: retires the per-`<p>` inline | |
| 385 | + | `margin-bottom: 0.75rem` pattern in feeds.js plugin-warning. */ | |
| 386 | + | .modal-body p + p { margin-top: var(--gap-section); } | |
| 387 | + | ||
| 388 | + | /* === 12. Toast ========================================================= | |
| 389 | + | Canonical: .toast (+ .toast.success / .toast.error today; | |
| 390 | + | rename to .toast-success / .toast-error in F5, add .toast-info, | |
| 391 | + | .toast-undo). Helper: BB.ui.showToast; must inject zero style.cssText. | |
| 392 | + | ========================================================================== */ | |
| 393 | + | .toast-container { | |
| 394 | + | position: fixed; | |
| 395 | + | bottom: 1rem; | |
| 396 | + | right: 1rem; | |
| 397 | + | z-index: 1001; | |
| 398 | + | } | |
| 399 | + | ||
| 400 | + | .toast { | |
| 401 | + | background-color: var(--surface-raised); | |
| 402 | + | border: var(--border-width) solid var(--border); | |
| 403 | + | border-radius: var(--radius-sm); | |
| 404 | + | padding: var(--gap-peer) var(--gap-group); | |
| 405 | + | margin-top: var(--gap-peer); | |
| 406 | + | box-shadow: var(--bevel-raised), var(--shadow-brutal); | |
| 407 | + | animation: slideIn 0.3s ease-out; | |
| 408 | + | font-weight: 500; | |
| 409 | + | font-size: var(--font-size-base); | |
| 410 | + | } | |
| 411 | + | .toast.success { border-left: 4px solid var(--success); background-color: color-mix(in srgb, var(--success) 8%, var(--surface-page)); } | |
| 412 | + | .toast.error { border-left: 4px solid var(--danger); background-color: color-mix(in srgb, var(--danger) 8%, var(--surface-page)); } | |
| 413 | + | .toast-undo { | |
| 414 | + | border-left: 4px solid var(--category-four); | |
| 415 | + | background-color: color-mix(in srgb, var(--category-four) 10%, var(--surface-page)); | |
| 416 | + | display: flex; | |
| 417 | + | align-items: center; | |
| 418 | + | gap: var(--gap-bound); | |
| 419 | + | } | |
| 420 | + | .undo-message { flex: 1; } | |
| 421 | + | .undo-btn { | |
| 422 | + | background: none; | |
| 423 | + | border: var(--border-width) solid currentColor; | |
| 424 | + | border-radius: var(--radius-sm); | |
| 425 | + | color: var(--content); | |
| 426 | + | cursor: pointer; | |
| 427 | + | padding: var(--step-tight) var(--step-roomy); | |
| 428 | + | font-size: var(--font-size-md); | |
| 429 | + | font-weight: 600; | |
| 430 | + | } | |
| 431 | + | .undo-btn:hover { background-color: color-mix(in srgb, var(--category-four) 20%, transparent); } | |
| 432 | + | .undo-countdown { | |
| 433 | + | font-size: var(--font-size-xs); | |
| 434 | + | color: var(--content-muted); | |
| 435 | + | min-width: 1.5rem; | |
| 436 | + | text-align: right; | |
| 437 | + | font-variant-numeric: tabular-nums; | |
| 438 | + | } | |
| 439 | + | ||
| 440 | + | @keyframes slideIn { | |
| 441 | + | from { transform: translateX(100%); opacity: 0; } | |
| 442 | + | to { transform: translateX(0); opacity: 1; } | |
| 443 | + | } | |
| 444 | + | ||
| 445 | + | /* === 13. Tag =========================================================== | |
| 446 | + | Canonical: .tag[data-color], display chip (detail panel, source/bookmark | |
| 447 | + | tags). Size modifier .tag--xs. .tag-chip is the separate interactive | |
| 448 | + | filter-chip (tag bars): it toggles (cursor + .active), unlike a display tag. | |
| 449 | + | Source tags use .tag.tag--xs[data-color="muted"]; .bookmark-tag composes on | |
| 450 | + | .tag with a click-to-filter modifier. | |
| 451 | + | ========================================================================== */ | |
| 452 | + | /* Carries `.raised`, which is where the fill and the bevel come from now. | |
| 453 | + | Left standing on purpose: a tag is a label and not pressable, which is the | |
| 454 | + | argument that flattened `.badge` on 2026-08-05, so `.tag` being an object at | |
| 455 | + | all is worth a second look. That is a decision about how the app should | |
| 456 | + | look, not a place the design system was being restated, so it is recorded | |
| 457 | + | rather than taken here. */ | |
| 458 | + | .tag { | |
| 459 | + | border: var(--border-width) solid var(--border); | |
| 460 | + | border-radius: var(--radius-sm); | |
| 461 | + | padding: var(--step-hair) var(--step-base); | |
| 462 | + | font-size: var(--font-size-sm); | |
| 463 | + | color: var(--content-secondary); | |
| 464 | + | } | |
| 465 | + | ||
| 466 | + | /* F2 additions (2026-06-02): color variants via [data-color] attribute. | |
| 467 | + | Consumes --category-five and --category-six. | |
| 468 | + | ||
| 469 | + | .badge dropped off these selectors when badges moved to [data-tone]: | |
| 470 | + | nothing sets [data-color] on a badge any more, so the badge halves were | |
| 471 | + | matching nothing. .tag keeps the axis, since a tag names a category | |
| 472 | + | rather than a state and green-because-green is the right contract for | |
| 473 | + | it. That is also why the two stopped sharing: they were never saying | |
| 474 | + | the same thing, only spelling it the same way. */ | |
| 475 | + | .tag[data-color="green"] { border-color: var(--category-two); color: var(--category-two); background-color: color-mix(in srgb, var(--category-two) 10%, transparent); } | |
| 476 | + | .tag[data-color="yellow"] { border-color: var(--category-four); color: var(--content); background-color: color-mix(in srgb, var(--category-four) 12%, transparent); } | |
| 477 | + | .tag[data-color="red"] { border-color: var(--category-one); color: var(--category-one); background-color: color-mix(in srgb, var(--category-one) 10%, transparent); } | |
| 478 | + | .tag[data-color="blue"] { border-color: var(--category-three); color: var(--category-three); background-color: color-mix(in srgb, var(--category-three) 10%, transparent); } | |
| 479 | + | .tag[data-color="cyan"] { border-color: var(--category-six); color: var(--category-six); background-color: color-mix(in srgb, var(--category-six) 10%, transparent); } | |
| 480 | + | .tag[data-color="purple"] { border-color: var(--category-five); color: var(--category-five); background-color: color-mix(in srgb, var(--category-five) 10%, transparent); } | |
| 481 | + | .tag[data-color="muted"] { border-color: var(--border); color: var(--content-muted); background-color: var(--surface-sunken); } | |
| 482 | + | .tag--xs { font-size: var(--font-size-xxs); padding: 0 var(--step-snug); } /* mirrors .badge--xs */ | |
| 483 | + | ||
| 484 | + | /* .badge: smaller and more compact than .tag; used for counts and inline | |
| 485 | + | status indicators. | |
| 486 | + | ||
| 487 | + | The colour axis is [data-tone], which makeover-webview emits, replacing | |
| 488 | + | the [data-color] axis this file used to carry. The old axis named the | |
| 489 | + | paint (green, yellow, red, blue) and the new one names the meaning, so | |
| 490 | + | a badge now says what it is rather than what colour it came out. The | |
| 491 | + | swap is close to free here because three of the four category tokens | |
| 492 | + | were already the same value under a second name: category-one is | |
| 493 | + | danger, category-two is success, category-four is warning. | |
| 494 | + | ||
| 495 | + | The base text colour is the generated one. What stays below is the box: | |
| 496 | + | the border and the type. | |
| 497 | + | ||
| 498 | + | The raised fill and bevel this rule used to add are gone (2026-08-05). | |
| 499 | + | Token::Badge is Depth::Flat because a label is not pressable, GoingsOn's | |
| 500 | + | badges are flat, and BB was the only app dressing one as an object. A |
Lines truncated
| @@ -1,0 +1,396 @@ | |||
| 1 | + | /* === 20. Sources list: sidebar entries ================================ | |
| 2 | + | Charter target (F5): .source-item collapses onto .card --row. | |
| 3 | + | ========================================================================== */ | |
| 4 | + | .sources-list { padding: var(--gap-bound) 0; } | |
| 5 | + | ||
| 6 | + | .source-item { | |
| 7 | + | display: flex; | |
| 8 | + | justify-content: space-between; | |
| 9 | + | align-items: center; | |
| 10 | + | padding: var(--gap-peer) var(--gap-group); | |
| 11 | + | cursor: pointer; | |
| 12 | + | min-height: 2.5rem; | |
| 13 | + | /* 3px, wider than a frame on purpose: this edge is the state marker. */ | |
| 14 | + | border-left: 3px solid transparent; | |
| 15 | + | } | |
| 16 | + | .source-item:hover { background-color: var(--surface-sunken); } | |
| 17 | + | .source-item.active { | |
| 18 | + | background-color: var(--surface-sunken); | |
| 19 | + | border-left-color: var(--category-four); | |
| 20 | + | } | |
| 21 | + | ||
| 22 | + | .source-name { | |
| 23 | + | flex: 1; | |
| 24 | + | white-space: nowrap; | |
| 25 | + | overflow: hidden; | |
| 26 | + | text-overflow: ellipsis; | |
| 27 | + | font-weight: 600; | |
| 28 | + | font-size: var(--font-size-base); | |
| 29 | + | } | |
| 30 | + | ||
| 31 | + | ||
| 32 | + | /* Carries `.raised`; same as `.tag`, the fill and the bevel are the design | |
| 33 | + | system's now. */ | |
| 34 | + | .source-count { | |
| 35 | + | padding: var(--step-hair) var(--step-snug); | |
| 36 | + | border-radius: var(--radius-xl); | |
| 37 | + | font-size: var(--font-size-xs); | |
| 38 | + | color: var(--content-muted); | |
| 39 | + | border: var(--border-width) solid var(--border); | |
| 40 | + | min-width: 1.8rem; | |
| 41 | + | text-align: center; | |
| 42 | + | } | |
| 43 | + | .source-count.all-read { | |
| 44 | + | color: var(--success); | |
| 45 | + | border-color: var(--success); | |
| 46 | + | } | |
| 47 | + | ||
| 48 | + | .source-delete { | |
| 49 | + | display: none; | |
| 50 | + | background: none; | |
| 51 | + | border: none; | |
| 52 | + | color: var(--content-muted); | |
| 53 | + | cursor: pointer; | |
| 54 | + | font-size: var(--font-size-base); | |
| 55 | + | padding: 0 var(--step-tight); | |
| 56 | + | line-height: 1; | |
| 57 | + | } | |
| 58 | + | .source-delete:hover { color: var(--danger); } | |
| 59 | + | .source-item:hover .source-delete { display: inline; } | |
| 60 | + | ||
| 61 | + | /* === 20. Sources: tag filter bar ====================================== */ | |
| 62 | + | .tag-filter-bar { | |
| 63 | + | display: flex; | |
| 64 | + | flex-wrap: wrap; | |
| 65 | + | gap: var(--gap-peer); | |
| 66 | + | padding: var(--gap-peer) var(--gap-group); | |
| 67 | + | border-bottom: var(--border-width) solid var(--border); | |
| 68 | + | background-color: var(--surface-overlay); | |
| 69 | + | } | |
| 70 | + | /* Carries `.chip`, which is what a filter chip is: raised at rest, hover, | |
| 71 | + | press, and a `.latched` state for the one that is on. The fill and both | |
| 72 | + | bevels come from layout.css; the pill shape, the type and the accent are | |
| 73 | + | this app's. */ | |
| 74 | + | .tag-chip { | |
| 75 | + | border: var(--border-width) solid var(--border); | |
| 76 | + | border-radius: var(--radius-xl); | |
| 77 | + | padding: var(--step-hair) var(--step-base); | |
| 78 | + | font-size: var(--font-size-xs); | |
| 79 | + | color: var(--content-secondary); | |
| 80 | + | cursor: pointer; | |
| 81 | + | white-space: nowrap; | |
| 82 | + | } | |
| 83 | + | .tag-chip:hover { border-color: var(--category-four); color: var(--content); } | |
| 84 | + | /* An active filter chip is latched down, same inversion as a pressed button. | |
| 85 | + | The inset bevel comes from `.latched`, which the markup sets alongside | |
| 86 | + | `.active`; the accent fill stays here, because which colour a latched chip | |
| 87 | + | takes is this app's call and the depth is not. */ | |
| 88 | + | .tag-chip.active { | |
| 89 | + | background-color: var(--category-four); | |
| 90 | + | border-color: var(--category-four); | |
| 91 | + | color: var(--content); | |
| 92 | + | font-weight: 600; | |
| 93 | + | } | |
| 94 | + | .source-tags { | |
| 95 | + | display: flex; | |
| 96 | + | flex-wrap: wrap; | |
| 97 | + | gap: var(--gap-peer); | |
| 98 | + | margin-top: var(--step-hair); | |
| 99 | + | } | |
| 100 | + | /* Source-row tags use .tag.tag--xs[data-color="muted"] (see § Tag). */ | |
| 101 | + | ||
| 102 | + | /* === 20. Sources: edit button ========================================= */ | |
| 103 | + | .source-edit { | |
| 104 | + | display: inline; | |
| 105 | + | background: none; | |
| 106 | + | border: none; | |
| 107 | + | color: var(--content-muted); | |
| 108 | + | cursor: pointer; | |
| 109 | + | font-size: var(--font-size-md); | |
| 110 | + | padding: 0 var(--step-tight); | |
| 111 | + | line-height: 1; | |
| 112 | + | opacity: 0.4; | |
| 113 | + | } | |
| 114 | + | .source-edit:hover { color: var(--category-four); opacity: 1; } | |
| 115 | + | ||
| 116 | + | /* === 22. Reader-expanded mode ========================================= | |
| 117 | + | Hides items panel and expands detail to full width. */ | |
| 118 | + | .main.reader-expanded .items-panel { | |
| 119 | + | display: none; | |
| 120 | + | } | |
| 121 | + | ||
| 122 | + | .main.reader-expanded .detail-panel { | |
| 123 | + | flex: 1; | |
| 124 | + | width: auto; | |
| 125 | + | min-width: 0; | |
| 126 | + | border-left: none; | |
| 127 | + | } | |
| 128 | + | ||
| 129 | + | .main.reader-expanded .item-detail { | |
| 130 | + | max-width: 800px; | |
| 131 | + | margin: 0 auto; | |
| 132 | + | } | |
| 133 | + | ||
| 134 | + | .reader-back-btn { | |
| 135 | + | margin-right: auto; | |
| 136 | + | } | |
| 137 | + | ||
| 138 | + | /* === 23. Query feeds: sidebar divider ================================ */ | |
| 139 | + | .source-divider { | |
| 140 | + | padding: var(--gap-peer) var(--gap-group) var(--gap-bound); | |
| 141 | + | font-size: var(--font-size-xxs); | |
| 142 | + | font-weight: 600; | |
| 143 | + | color: var(--content-muted); | |
| 144 | + | text-transform: uppercase; | |
| 145 | + | letter-spacing: 0.5px; | |
| 146 | + | border-top: var(--border-width) solid var(--border); | |
| 147 | + | margin-top: var(--gap-bound); | |
| 148 | + | cursor: default; | |
| 149 | + | } | |
| 150 | + | ||
| 151 | + | /* Query feed items: subtle visual distinction */ | |
| 152 | + | .source-item.query-feed .source-name { | |
| 153 | + | font-style: italic; | |
| 154 | + | } | |
| 155 | + | ||
| 156 | + | /* Add query feed button */ | |
| 157 | + | .source-item.add-query-feed-btn { | |
| 158 | + | padding: var(--gap-peer) var(--gap-group); | |
| 159 | + | border-top: var(--border-width) solid var(--border); | |
| 160 | + | } | |
| 161 | + | .source-item.add-query-feed-btn:hover { | |
| 162 | + | background-color: var(--surface-sunken); | |
| 163 | + | } | |
| 164 | + | ||
| 165 | + | /* === 23. Query feeds: builder ========================================= | |
| 166 | + | `.condition-row` is the canonical NAMED LAYOUT for mixed-intent rows | |
| 167 | + | (see compose-first rule, named-layout exception). | |
| 168 | + | ========================================================================== */ | |
| 169 | + | .qf-builder .form-group { margin-bottom: var(--gap-section); } | |
| 170 | + | ||
| 171 | + | .condition-row { | |
| 172 | + | display: flex; | |
| 173 | + | align-items: center; | |
| 174 | + | gap: var(--gap-bound); | |
| 175 | + | margin-bottom: var(--gap-peer); | |
| 176 | + | } | |
| 177 | + | .condition-row .field { | |
| 178 | + | flex: 1; | |
| 179 | + | min-width: 0; | |
| 180 | + | font-size: var(--font-size-md); | |
| 181 | + | padding: var(--gap-bound) var(--gap-peer); | |
| 182 | + | } | |
| 183 | + | .condition-row .cond-field { flex: 0 0 auto; width: 90px; } | |
| 184 | + | .condition-row .cond-op { flex: 0 0 auto; width: 120px; } | |
| 185 | + | .condition-row .cond-val { flex: 1; } | |
| 186 | + | .condition-row .cond-remove { | |
| 187 | + | flex-shrink: 0; | |
| 188 | + | padding: var(--step-tight) var(--step-snug); | |
| 189 | + | font-size: var(--font-size-lg); | |
| 190 | + | line-height: 1; | |
| 191 | + | } | |
| 192 | + | ||
| 193 | + | .add-condition-btn { | |
| 194 | + | margin-top: var(--gap-peer); | |
| 195 | + | } | |
| 196 | + | ||
| 197 | + | .match-preview { | |
| 198 | + | padding: var(--gap-peer) 0; | |
| 199 | + | font-size: var(--font-size-md); | |
| 200 | + | color: var(--content-muted); | |
| 201 | + | } | |
| 202 | + | ||
| 203 | + | .modal-actions { | |
| 204 | + | display: flex; | |
| 205 | + | justify-content: flex-end; | |
| 206 | + | gap: var(--gap-peer); | |
| 207 | + | margin-top: var(--gap-section); | |
| 208 | + | padding-top: var(--gap-section); | |
| 209 | + | border-top: var(--border-width) solid var(--border); | |
| 210 | + | } | |
| 211 | + | ||
| 212 | + | /* === 24. Sidebar saved articles ======================================== */ | |
| 213 | + | .sidebar-saved { | |
| 214 | + | display: flex; | |
| 215 | + | justify-content: space-between; | |
| 216 | + | align-items: center; | |
| 217 | + | padding: var(--gap-peer) var(--gap-group); | |
| 218 | + | cursor: pointer; | |
| 219 | + | border-top: var(--border-width) solid var(--border); | |
| 220 | + | /* 3px, wider than a frame on purpose: this edge is the state marker. */ | |
| 221 | + | border-left: 3px solid transparent; | |
| 222 | + | min-height: 2.5rem; | |
| 223 | + | font-size: var(--font-size-base); | |
| 224 | + | font-weight: 600; | |
| 225 | + | } | |
| 226 | + | .sidebar-saved:hover { background-color: var(--surface-sunken); } | |
| 227 | + | .sidebar-saved.active { | |
| 228 | + | background-color: var(--surface-sunken); | |
| 229 | + | border-left-color: var(--category-four); | |
| 230 | + | } | |
| 231 | + | ||
| 232 | + | /* Sidebar footer */ | |
| 233 | + | .sidebar-footer { | |
| 234 | + | margin-top: auto; | |
| 235 | + | padding: var(--gap-peer) var(--gap-group); | |
| 236 | + | border-top: var(--border-width) solid var(--border); | |
| 237 | + | } | |
| 238 | + | ||
| 239 | + | /* F2 additions (2026-06-02): utility primitives. | |
| 240 | + | .hidden: visibility toggle utility (replaces ad-hoc style.display = 'none'). | |
| 241 | + | .confirm-message: retires the inline-style violation in | |
| 242 | + | components.js confirmAction (was setting marginBottom, fontSize, color | |
| 243 | + | via style.cssText). */ | |
| 244 | + | .hidden { display: none !important; } | |
| 245 | + | .confirm-message { | |
| 246 | + | margin-bottom: var(--gap-section); | |
| 247 | + | font-size: var(--font-size-base); | |
| 248 | + | color: var(--content); | |
| 249 | + | } | |
| 250 | + | ||
| 251 | + | /* === 26. Help shortcuts ================================================ */ | |
| 252 | + | .help-shortcuts { | |
| 253 | + | display: grid; | |
| 254 | + | grid-template-columns: 1fr 1fr; | |
| 255 | + | gap: var(--gap-pane); | |
| 256 | + | } | |
| 257 | + | .help-shortcuts .help-section:last-child { | |
| 258 | + | grid-column: 1 / -1; | |
| 259 | + | } | |
| 260 | + | .help-section h3 { | |
| 261 | + | font-size: var(--font-size-sm); | |
| 262 | + | font-weight: 600; | |
| 263 | + | color: var(--content-muted); | |
| 264 | + | text-transform: uppercase; | |
| 265 | + | letter-spacing: 0.5px; | |
| 266 | + | margin-bottom: var(--gap-peer); | |
| 267 | + | } | |
| 268 | + | .help-row { | |
| 269 | + | display: flex; | |
| 270 | + | align-items: center; | |
| 271 | + | gap: var(--gap-bound); | |
| 272 | + | margin-bottom: var(--gap-peer); | |
| 273 | + | font-size: var(--font-size-base); | |
| 274 | + | } | |
| 275 | + | .help-row kbd { | |
| 276 | + | background: var(--surface-raised); | |
| 277 | + | border: var(--border-width) solid var(--border); | |
| 278 | + | border-radius: var(--radius-sm); | |
| 279 | + | box-shadow: var(--bevel-raised); | |
| 280 | + | padding: var(--step-hair) var(--step-snug); | |
| 281 | + | font-family: inherit; | |
| 282 | + | font-size: var(--font-size-md); | |
| 283 | + | min-width: 1.4rem; | |
| 284 | + | text-align: center; | |
| 285 | + | color: var(--content); | |
| 286 | + | } | |
| 287 | + | .help-row span { color: var(--content-secondary); margin-left: auto; } | |
| 288 | + | ||
| 289 | + | /* === 27. Welcome modal ================================================= */ | |
| 290 | + | .welcome-content h3 { | |
| 291 | + | font-size: var(--font-size-base); | |
| 292 | + | font-weight: 600; | |
| 293 | + | color: var(--content); | |
| 294 | + | margin-top: var(--gap-section); | |
| 295 | + | margin-bottom: var(--gap-bound); | |
| 296 | + | } | |
| 297 | + | .welcome-content h3:first-child { margin-top: 0; } | |
| 298 | + | .welcome-content p { | |
| 299 | + | font-size: var(--font-size-base); | |
| 300 | + | color: var(--content-secondary); | |
| 301 | + | line-height: 1.5; | |
| 302 | + | } | |
| 303 | + | .welcome-cta { | |
| 304 | + | display: flex; | |
| 305 | + | justify-content: center; | |
| 306 | + | gap: var(--gap-peer); | |
| 307 | + | margin-top: var(--gap-section); | |
| 308 | + | padding-top: var(--gap-section); | |
| 309 | + | border-top: var(--border-width) solid var(--border); | |
| 310 | + | } | |
| 311 | + | ||
| 312 | + | /* === 28. Settings modal ================================================ */ | |
| 313 | + | .settings-content .form-group { margin-bottom: var(--gap-section); } | |
| 314 | + | .sync-manual-entry { margin-top: var(--gap-section); } | |
| 315 | + | .sync-manual-form { margin-top: var(--gap-peer); } | |
| 316 | + | .sync-action-spaced { margin-bottom: var(--gap-section); } | |
| 317 | + | .sync-disconnect { margin-top: var(--gap-section); } | |
| 318 | + | ||
| 319 | + | /* === 30. Reading list / Bookmarks ====================================== */ | |
| 320 | + | ||
| 321 | + | .bookmark-tag-bar { | |
| 322 | + | display: flex; | |
| 323 | + | gap: var(--gap-peer); | |
| 324 | + | padding: var(--gap-peer) var(--gap-group); | |
| 325 | + | border-bottom: var(--border-width) solid var(--border); | |
| 326 | + | overflow-x: auto; | |
| 327 | + | flex-shrink: 0; | |
| 328 | + | } | |
| 329 | + | /* Bookmark Open/more actions use renderRow's .row-actions (hover-revealed); | |
| 330 | + | pinned state uses .row--article.is-pinned. */ | |
| 331 | + | .bookmark-tags { | |
| 332 | + | display: flex; | |
| 333 | + | gap: var(--gap-peer); | |
| 334 | + | flex-wrap: wrap; | |
| 335 | + | margin-top: var(--gap-bound); | |
| 336 | + | } | |
| 337 | + | .bookmark-tag { | |
| 338 | + | cursor: pointer; | |
| 339 | + | font-size: var(--font-size-xs); | |
| 340 | + | } | |
| 341 | + | .bookmark-tag:hover { | |
| 342 | + | background: var(--action); | |
| 343 | + | color: white; | |
| 344 | + | } | |
| 345 | + | .context-menu-item.danger, | |
| 346 | + | .context-menu-item--danger { | |
| 347 | + | /* F2 (2026-06-02): added --danger parity name; .danger kept as legacy | |
| 348 | + | alias until F5 retires call sites. Inline #dc3545 fallback removed; | |
| 349 | + | --accent-red is themed and always defined. */ | |
| 350 | + | color: var(--danger); | |
| 351 | + | } | |
| 352 | + | ||
| 353 | + | /* === 31. Screen-reader only (.sr-only) ================================ */ | |
| 354 | + | .sr-only { | |
| 355 | + | position: absolute; | |
| 356 | + | width: 1px; | |
| 357 | + | height: 1px; | |
| 358 | + | padding: 0; | |
| 359 | + | margin: -1px; | |
| 360 | + | overflow: hidden; | |
| 361 | + | clip: rect(0, 0, 0, 0); | |
| 362 | + | white-space: nowrap; | |
| 363 | + | border: 0; | |
| 364 | + | } | |
| 365 | + | ||
| 366 | + | /* === 32. Focus indicators ============================================== | |
| 367 | + | The design system's ring for BB's own clickable elements; the same colour | |
| 368 | + | as a box-shadow ring for inputs, which avoids the layout shift an outline | |
| 369 | + | causes on a bevelled field. Rule: every new interactive primitive must | |
| 370 | + | appear in one of these selector lists. | |
| 371 | + | ||
| 372 | + | makeover's own primitives are absent on purpose. `.button` and `.tab` take | |
| 373 | + | an identical ring from layout.css, and restating it here unlayered would | |
| 374 | + | override the design system with a copy of itself. | |
| 375 | + | ========================================================================== */ | |
| 376 | + | .source-item:focus-visible, | |
| 377 | + | .star-btn:focus-visible, | |
| 378 | + | .source-delete:focus-visible, | |
| 379 | + | .row--article:focus-visible, | |
| 380 | + | .plugin-item:focus-visible, | |
| 381 | + | .toast-action:focus-visible { | |
| 382 | + | outline: 2px solid var(--focus-ring); | |
| 383 | + | outline-offset: 2px; | |
| 384 | + | } | |
| 385 | + | ||
| 386 | + | /* A field's focus ring comes from layout.css now. What stood here was this | |
| 387 | + | app's own third answer to the same question: `outline: none` plus a composed | |
| 388 | + | box-shadow that had to restate the bevel beside it. `outline: none` also | |
| 389 | + | killed the generated `.field:focus-visible`, so keeping this rule would have | |
| 390 | + | left fields with no ring rather than with two. Same 2px, and --focus-ring | |
| 391 | + | resolves to the same colour; the generated ring sits inside the well | |
| 392 | + | (`outline-offset: calc(-1 * 2px)`), which is where a well's ring belongs. | |
| 393 | + | ||
| 394 | + | `:focus-visible` rather than `:focus` is not a behaviour change for a text | |
| 395 | + | field: a focused text input always matches `:focus-visible`. */ | |
| 396 | + |
| @@ -1,0 +1,126 @@ | |||
| 1 | + | /* === 33. Mobile tab bar ================================================ */ | |
| 2 | + | .mobile-tab-bar { | |
| 3 | + | display: none; | |
| 4 | + | position: fixed; | |
| 5 | + | bottom: 0; | |
| 6 | + | left: 0; | |
| 7 | + | right: 0; | |
| 8 | + | z-index: 1100; | |
| 9 | + | height: calc(52px + env(safe-area-inset-bottom)); | |
| 10 | + | padding-bottom: env(safe-area-inset-bottom); | |
| 11 | + | background-color: var(--surface-overlay); | |
| 12 | + | border-top: var(--border-width) solid var(--border); | |
| 13 | + | align-items: center; | |
| 14 | + | justify-content: space-around; | |
| 15 | + | } | |
| 16 | + | ||
| 17 | + | .mobile-tab { | |
| 18 | + | flex: 1; | |
| 19 | + | display: flex; | |
| 20 | + | align-items: center; | |
| 21 | + | justify-content: center; | |
| 22 | + | height: 52px; | |
| 23 | + | background: none; | |
| 24 | + | border: none; | |
| 25 | + | color: var(--content-muted); | |
| 26 | + | font-family: inherit; | |
| 27 | + | font-size: var(--font-size-xs); | |
| 28 | + | font-weight: 600; | |
| 29 | + | text-transform: uppercase; | |
| 30 | + | letter-spacing: 0.5px; | |
| 31 | + | cursor: pointer; | |
| 32 | + | } | |
| 33 | + | .mobile-tab.active { color: var(--action); } | |
| 34 | + | ||
| 35 | + | .mobile-tab-create { | |
| 36 | + | font-size: 1.4rem; | |
| 37 | + | font-weight: 400; | |
| 38 | + | color: var(--success); | |
| 39 | + | text-transform: none; | |
| 40 | + | letter-spacing: 0; | |
| 41 | + | } | |
| 42 | + | ||
| 43 | + | /* === 34. Mobile more-popover =========================================== */ | |
| 44 | + | .mobile-more-popover { | |
| 45 | + | display: none; | |
| 46 | + | position: fixed; | |
| 47 | + | bottom: calc(52px + env(safe-area-inset-bottom)); | |
| 48 | + | right: 0.5rem; | |
| 49 | + | z-index: 1101; | |
| 50 | + | background-color: var(--surface-raised); | |
| 51 | + | border: var(--border-width) solid var(--border); | |
| 52 | + | border-radius: var(--radius-sm); | |
| 53 | + | box-shadow: var(--bevel-raised), 0 calc(-1 * var(--shadow-offset)) 0 var(--shadow-color); | |
| 54 | + | padding: var(--gap-bound); | |
| 55 | + | min-width: 160px; | |
| 56 | + | } | |
| 57 | + | .mobile-more-popover.visible { display: block; } | |
| 58 | + | .mobile-more-popover button { | |
| 59 | + | display: block; | |
| 60 | + | width: 100%; | |
| 61 | + | padding: var(--gap-peer) var(--gap-group); | |
| 62 | + | border: none; | |
| 63 | + | border-radius: var(--radius-sm); | |
| 64 | + | background: none; | |
| 65 | + | color: var(--content); | |
| 66 | + | font-family: inherit; | |
| 67 | + | font-size: var(--font-size-base); | |
| 68 | + | text-align: left; | |
| 69 | + | cursor: pointer; | |
| 70 | + | } | |
| 71 | + | .mobile-more-popover button:hover { | |
| 72 | + | background-color: var(--surface-sunken); | |
| 73 | + | } | |
| 74 | + | ||
| 75 | + | /* === 35. Mobile search bar ============================================= */ | |
| 76 | + | .mobile-search-bar { | |
| 77 | + | display: none; | |
| 78 | + | position: sticky; | |
| 79 | + | top: 0; | |
| 80 | + | z-index: 10; | |
| 81 | + | padding: var(--gap-peer) var(--gap-group); | |
| 82 | + | gap: var(--gap-peer); | |
| 83 | + | background-color: var(--surface-page); | |
| 84 | + | border-bottom: var(--border-width) solid var(--border); | |
| 85 | + | } | |
| 86 | + | /* Both carry `.field`. They were the only two fields in the app with no bevel | |
| 87 | + | at all, filled with --surface-overlay against the mobile bar's own page | |
| 88 | + | fill, so mobile and desktop disagreed about what an input looks like. They | |
| 89 | + | agree now, and the fill and edge come from the same place as everywhere | |
| 90 | + | else. */ | |
| 91 | + | .mobile-search-input { | |
| 92 | + | flex: 1; | |
| 93 | + | font-family: inherit; | |
| 94 | + | } | |
| 95 | + | .mobile-sort-select { | |
| 96 | + | font-family: inherit; | |
| 97 | + | font-size: var(--font-size-md); | |
| 98 | + | } | |
| 99 | + | ||
| 100 | + | /* === 36. Mobile back button ============================================ */ | |
| 101 | + | .mobile-back-btn { display: none; } | |
| 102 | + | ||
| 103 | + | /* === 37. Pull-to-refresh indicator ===================================== */ | |
| 104 | + | .pull-to-refresh-indicator { | |
| 105 | + | display: none; | |
| 106 | + | text-align: center; | |
| 107 | + | padding: var(--gap-group); | |
| 108 | + | font-size: var(--font-size-md); | |
| 109 | + | color: var(--content-muted); | |
| 110 | + | } | |
| 111 | + | .pull-to-refresh-indicator.visible { | |
| 112 | + | display: block; | |
| 113 | + | } | |
| 114 | + | ||
| 115 | + | /* === 38. Source-badge on items (mobile-only display) ================= | |
| 116 | + | Hidden on desktop; shown inside the @media (max-width: 599px) block below. | |
| 117 | + | ========================================================================== */ | |
| 118 | + | .article-source-badge { display: none; } | |
| 119 | + | ||
| 120 | + | /* === 39. @media (max-width: 599px): mobile layout ==================== | |
| 121 | + | 599px is SizeClass::Compact's upper edge, not a number chosen here. It was | |
| 122 | + | 768px until 2026-08-11, which is a boundary no size class has: a window | |
| 123 | + | between 600 and 767 got the single-pane shell while every generated | |
| 124 | + | stylesheet beside it was still laying out the wide one. build.rs checks this | |
| 125 | + | against makeover-geometry now, so the two cannot part again. | |
| 126 | + | ========================================================================== */ |
| @@ -1,0 +1,167 @@ | |||
| 1 | + | @media (max-width: 599px) { | |
| 2 | + | body { | |
| 3 | + | padding-top: env(safe-area-inset-top); | |
| 4 | + | padding-bottom: calc(52px + env(safe-area-inset-bottom)); | |
| 5 | + | } | |
| 6 | + | ||
| 7 | + | .mobile-tab-bar { display: flex; } | |
| 8 | + | .header { display: none; } | |
| 9 | + | .main { flex-direction: column; } | |
| 10 | + | ||
| 11 | + | .sidebar { | |
| 12 | + | display: none; | |
| 13 | + | width: 100%; | |
| 14 | + | min-width: 0; | |
| 15 | + | border-right: none; | |
| 16 | + | } | |
| 17 | + | .sidebar.mobile-visible { | |
| 18 | + | display: flex; | |
| 19 | + | flex: 1; | |
| 20 | + | } | |
| 21 | + | ||
| 22 | + | .items-panel { width: 100%; } | |
| 23 | + | .items-panel.mobile-hidden { display: none; } | |
| 24 | + | ||
| 25 | + | .detail-panel { | |
| 26 | + | position: fixed; | |
| 27 | + | top: 0; | |
| 28 | + | left: 0; | |
| 29 | + | right: 0; | |
| 30 | + | bottom: 0; | |
| 31 | + | width: 100%; | |
| 32 | + | min-width: 0; | |
| 33 | + | z-index: 1050; | |
| 34 | + | border-left: none; | |
| 35 | + | } | |
| 36 | + | ||
| 37 | + | body.mobile-detail-open .mobile-tab-bar { display: none; } | |
| 38 | + | body.mobile-detail-open { padding-bottom: 0; } | |
| 39 | + | body.mobile-detail-open .detail-panel { | |
| 40 | + | padding-top: env(safe-area-inset-top); | |
| 41 | + | padding-bottom: env(safe-area-inset-bottom); | |
| 42 | + | } | |
| 43 | + | ||
| 44 | + | .mobile-back-btn { | |
| 45 | + | display: inline-flex; | |
| 46 | + | margin-right: auto; | |
| 47 | + | font-size: var(--font-size-md); | |
| 48 | + | } | |
| 49 | + | ||
| 50 | + | .mobile-search-bar { display: flex; } | |
| 51 | + | ||
| 52 | + | /* Larger tap targets. Spacing is not listed here: the touch preset in | |
| 53 | + | css/geometry.css opens every gap at once under (hover: none). What | |
| 54 | + | remains is the parts a density preset cannot express. */ | |
| 55 | + | .star-btn { font-size: var(--font-size-2xl); } | |
| 56 | + | .source-item { min-height: 3rem; } | |
| 57 | + | ||
| 58 | + | /* Modal as bottom sheet */ | |
| 59 | + | .modal-overlay { align-items: flex-end; } | |
| 60 | + | .modal-content { | |
| 61 | + | width: 100%; | |
| 62 | + | max-width: 100%; | |
| 63 | + | border-radius: var(--radius-lg) var(--radius-lg) 0 0; | |
| 64 | + | max-height: 85vh; | |
| 65 | + | } | |
| 66 | + | .modal-content::before { | |
| 67 | + | content: ''; | |
| 68 | + | display: block; | |
| 69 | + | width: 36px; | |
| 70 | + | height: 4px; | |
| 71 | + | background-color: var(--border-strong); | |
| 72 | + | border-radius: var(--radius-xs); | |
| 73 | + | margin: var(--gap-peer) auto; | |
| 74 | + | } | |
| 75 | + | .modal-header { border-radius: 0; } | |
| 76 | + | ||
| 77 | + | /* Toast above tab bar */ | |
| 78 | + | .toast-container { | |
| 79 | + | bottom: calc(60px + env(safe-area-inset-bottom)); | |
| 80 | + | right: 0.75rem; | |
| 81 | + | left: 0.75rem; | |
| 82 | + | } | |
| 83 | + | ||
| 84 | + | /* Update banner above tab bar */ | |
| 85 | + | .update-banner { | |
| 86 | + | bottom: calc(60px + env(safe-area-inset-bottom)); | |
| 87 | + | } | |
| 88 | + | ||
| 89 | + | /* Source badge on items */ | |
| 90 | + | .article-source-badge { | |
| 91 | + | display: inline-block; | |
| 92 | + | background-color: var(--surface-sunken); | |
| 93 | + | color: var(--content-secondary); | |
| 94 | + | font-size: var(--font-size-xxs); | |
| 95 | + | font-weight: 600; | |
| 96 | + | padding: 0 var(--step-snug); | |
| 97 | + | border-radius: var(--radius-sm); | |
| 98 | + | margin-top: var(--step-hair); | |
| 99 | + | } | |
| 100 | + | ||
| 101 | + | /* Bold unread item titles */ | |
| 102 | + | .row--article.is-unread .row-primary { font-weight: 700; } | |
| 103 | + | ||
| 104 | + | /* Sticky detail header */ | |
| 105 | + | .detail-header { | |
| 106 | + | position: sticky; | |
| 107 | + | top: 0; | |
| 108 | + | z-index: 5; | |
| 109 | + | } | |
| 110 | + | .detail-panel { -webkit-overflow-scrolling: touch; } | |
| 111 | + | .detail-body { line-height: 1.8; } | |
| 112 | + | ||
| 113 | + | /* Horizontal-scroll tag filter bar */ | |
| 114 | + | .tag-filter-bar { | |
| 115 | + | flex-wrap: nowrap; | |
| 116 | + | overflow-x: auto; | |
| 117 | + | -webkit-overflow-scrolling: touch; | |
| 118 | + | scrollbar-width: none; | |
| 119 | + | } | |
| 120 | + | .tag-filter-bar::-webkit-scrollbar { display: none; } | |
| 121 | + | ||
| 122 | + | /* Help shortcuts single column */ | |
| 123 | + | .help-shortcuts { grid-template-columns: 1fr; } | |
| 124 | + | } | |
| 125 | + | ||
| 126 | + | /* === 40. @media (hover: none): touch overrides ====================== */ | |
| 127 | + | @media (hover: none) { | |
| 128 | + | .row--article:hover { background-color: transparent; } | |
| 129 | + | .row--article.is-unread:hover { background-color: color-mix(in srgb, var(--danger) 6%, var(--surface-page)); } | |
| 130 | + | .row--article.is-selected:hover { background-color: var(--surface-sunken); } | |
| 131 | + | .row--article.is-unread.is-selected:hover { background-color: color-mix(in srgb, var(--danger) 12%, var(--surface-page)); } | |
| 132 | + | .source-item:hover { background-color: transparent; } | |
| 133 | + | .source-item.active:hover { background-color: var(--surface-sunken); } | |
| 134 | + | .sidebar-saved:hover { background-color: transparent; } | |
| 135 | + | .sidebar-saved.active:hover { background-color: var(--surface-sunken); } | |
| 136 | + | /* Border only. The `background-color` half was taking back the generated | |
| 137 | + | hover fill, and makeover-webview wraps its own hover in | |
| 138 | + | (hover: hover) and (pointer: fine) now, so on a fingertip there is | |
| 139 | + | nothing left to take back. What remains undoes this file's own | |
| 140 | + | `.button:hover { border-color: var(--border-strong) }` above, which | |
| 141 | + | makeover has no opinion about. */ | |
| 142 | + | .button:hover { border-color: var(--border); } | |
| 143 | + | .button--primary:hover { background-color: var(--action); border-color: var(--action); } | |
| 144 | + | .button--success:hover { background-color: var(--category-four); border-color: var(--category-four); } | |
| 145 | + | .star-btn:hover { color: var(--content-muted); transform: none; } | |
| 146 | + | .star-btn.starred:hover { color: var(--category-four); } | |
| 147 | + | .tag-chip:hover { border-color: var(--border); color: var(--content-secondary); } | |
| 148 | + | .tag-chip.active:hover { border-color: var(--category-four); } | |
| 149 | + | .plugin-item:hover { border-color: var(--border); } | |
| 150 | + | .context-menu-item:hover { background-color: transparent; } | |
| 151 | + | } | |
| 152 | + | ||
| 153 | + | /* === 41. @media (prefers-reduced-motion: reduce) ====================== */ | |
| 154 | + | @media (prefers-reduced-motion: reduce) { | |
| 155 | + | *, *::before, *::after { | |
| 156 | + | animation-duration: 0.01ms !important; | |
| 157 | + | animation-iteration-count: 1 !important; | |
| 158 | + | transition-duration: 0.01ms !important; | |
| 159 | + | } | |
| 160 | + | } | |
| 161 | + | ||
| 162 | + | /* === 42. Webkit scrollbar ============================================== */ | |
| 163 | + | ::-webkit-scrollbar { width: 6px; height: 6px; } | |
| 164 | + | /* The clearest place in the app to read the bevel pair against each other. */ | |
| 165 | + | ::-webkit-scrollbar-track { background: var(--surface-overlay); box-shadow: var(--bevel-inset); } | |
| 166 | + | ::-webkit-scrollbar-thumb { background: var(--border-strong); border-radius: var(--radius-xs); box-shadow: var(--bevel-raised); } | |
| 167 | + | ::-webkit-scrollbar-thumb:hover { background: var(--content-muted); } |
| @@ -1,0 +1,275 @@ | |||
| 1 | + | /* === 4. Layout shell: body & #app ==================================== */ | |
| 2 | + | body { | |
| 3 | + | font-family: var(--font-sans); | |
| 4 | + | background-color: var(--surface-page); | |
| 5 | + | color: var(--content); | |
| 6 | + | line-height: 1.5; | |
| 7 | + | overflow: hidden; | |
| 8 | + | } | |
| 9 | + | ||
| 10 | + | #app { | |
| 11 | + | display: flex; | |
| 12 | + | flex-direction: column; | |
| 13 | + | height: 100vh; | |
| 14 | + | } | |
| 15 | + | ||
| 16 | + | /* === 4. Layout shell: main =========================================== */ | |
| 17 | + | .main { | |
| 18 | + | display: flex; | |
| 19 | + | flex: 1; | |
| 20 | + | overflow: hidden; | |
| 21 | + | } | |
| 22 | + | ||
| 23 | + | /* === 5. Header ========================================================== */ | |
| 24 | + | .header { | |
| 25 | + | display: flex; | |
| 26 | + | justify-content: space-between; | |
| 27 | + | align-items: center; | |
| 28 | + | padding: var(--gap-peer) var(--gap-section); | |
| 29 | + | background-color: var(--surface-overlay); | |
| 30 | + | border-bottom: var(--border-width) solid var(--border); | |
| 31 | + | -webkit-user-select: none; | |
| 32 | + | user-select: none; | |
| 33 | + | position: relative; | |
| 34 | + | } | |
| 35 | + | ||
| 36 | + | .header-actions { | |
| 37 | + | display: flex; | |
| 38 | + | gap: var(--gap-peer); | |
| 39 | + | align-items: center; | |
| 40 | + | } | |
| 41 | + | ||
| 42 | + | /* Carries `.field` in the markup: shape is below, fill and inset edge come | |
| 43 | + | from the generated layout.css. Width is the only thing that made this a | |
| 44 | + | different rule from a form field, and it is all that is left here. */ | |
| 45 | + | .search-input { | |
| 46 | + | width: 180px; | |
| 47 | + | } | |
| 48 | + | ||
| 49 | + | /* `.search-spinner` and `@keyframes spin` are gone (2026-08-26). They were the | |
| 50 | + | only rotation left in this file, and a spinner turns at a rate it invented: | |
| 51 | + | it reads as progress on a wait that has none to report. Search here is a | |
| 52 | + | local query behind a 300ms debounce, so under rule 4 of wiki | |
| 53 | + | `loading-and-progress-standard` there was nothing for it to say. */ | |
| 54 | + | ||
| 55 | + | .search-input::placeholder { color: var(--content-muted); } | |
| 56 | + | ||
| 57 | + | /* `.sort-select` is gone: every property it set is `.field` plus the | |
| 58 | + | `select.field` cursor, and its `:focus { outline: none }` was killing the | |
| 59 | + | generated `.field:focus-visible` ring on the one control that had no ring of | |
| 60 | + | its own to replace it with. The call sites carry `.field`. */ | |
| 61 | + | ||
| 62 | + | /* === 6. Sidebar ======================================================== */ | |
| 63 | + | .sidebar { | |
| 64 | + | width: 220px; | |
| 65 | + | min-width: 220px; | |
| 66 | + | background-color: var(--surface-overlay); | |
| 67 | + | border-right: var(--border-width) solid var(--border); | |
| 68 | + | display: flex; | |
| 69 | + | flex-direction: column; | |
| 70 | + | overflow-y: auto; | |
| 71 | + | scrollbar-gutter: stable; | |
| 72 | + | } | |
| 73 | + | ||
| 74 | + | .sidebar h2 { | |
| 75 | + | padding: var(--gap-peer) var(--gap-group); | |
| 76 | + | font-size: var(--font-size-sm); | |
| 77 | + | font-weight: 700; | |
| 78 | + | color: var(--content-muted); | |
| 79 | + | text-transform: uppercase; | |
| 80 | + | letter-spacing: 0.5px; | |
| 81 | + | border-bottom: var(--border-width) solid var(--border); | |
| 82 | + | } | |
| 83 | + | ||
| 84 | + | /* === 7. Items panel ==================================================== */ | |
| 85 | + | .items-panel { | |
| 86 | + | flex: 1; | |
| 87 | + | display: flex; | |
| 88 | + | flex-direction: column; | |
| 89 | + | overflow-y: auto; | |
| 90 | + | background-color: var(--surface-page); | |
| 91 | + | } | |
| 92 | + | ||
| 93 | + | /* `.items-list`, `.plugin-list` and `.sources-list` each wrote | |
| 94 | + | `list-style: none` by hand. All three carry `.list` now, which is the | |
| 95 | + | generated reset; `.sources-list` keeps its padding and the other two had | |
| 96 | + | nothing else to keep. */ | |
| 97 | + | ||
| 98 | + | /* Article rows: feed items + reading-list bookmarks. Uses the renderRow .row | |
| 99 | + | layout (icon / content / actions, flex + gap) plus article appearance + | |
| 100 | + | read/unread/selected/pinned state. Replaces the legacy .item-* layout. */ | |
| 101 | + | .row--article { | |
| 102 | + | padding: var(--gap-peer) var(--gap-section); | |
| 103 | + | border-bottom: var(--border-width) solid var(--border); | |
| 104 | + | /* 3px, wider than a frame on purpose: this edge is the state marker. */ | |
| 105 | + | border-left: 3px solid transparent; | |
| 106 | + | cursor: pointer; | |
| 107 | + | } | |
| 108 | + | .row--article:hover { background-color: var(--surface-overlay); } | |
| 109 | + | .row--article.is-unread { | |
| 110 | + | border-left-color: var(--danger); | |
| 111 | + | background-color: color-mix(in srgb, var(--danger) 6%, var(--surface-page)); | |
| 112 | + | } | |
| 113 | + | .row--article.is-read { color: var(--content-secondary); } | |
| 114 | + | .row--article.is-selected { | |
| 115 | + | background-color: var(--surface-sunken); | |
| 116 | + | border-left-color: var(--category-four); | |
| 117 | + | } | |
| 118 | + | .row--article.is-unread.is-selected { | |
| 119 | + | border-left-color: var(--danger); | |
| 120 | + | background-color: color-mix(in srgb, var(--danger) 12%, var(--surface-page)); | |
| 121 | + | } | |
| 122 | + | .row--article.is-pinned { border-left-color: var(--action); } | |
| 123 | + | .empty-icon { | |
| 124 | + | display: flex; | |
| 125 | + | justify-content: center; | |
| 126 | + | margin-bottom: var(--gap-section); | |
| 127 | + | color: var(--content-muted); | |
| 128 | + | } | |
| 129 | + | .empty-icon svg { width: 2.5rem; height: 2.5rem; } | |
| 130 | + | ||
| 131 | + | /* Standalone empty-state block. The items/bookmarks empty rows use | |
| 132 | + | `<li class="empty-state">`; new surfaces use BB.ui.renderEmptyState(). | |
| 133 | + | <div class="empty-state"> | |
| 134 | + | <div class="empty-state-icon">...</div> | |
| 135 | + | <p class="empty-state-text">No items</p> | |
| 136 | + | <button class="button button--primary">Add one</button> | |
| 137 | + | </div> | |
| 138 | + | */ | |
| 139 | + | .empty-state { | |
| 140 | + | display: flex; | |
| 141 | + | flex-direction: column; | |
| 142 | + | align-items: center; | |
| 143 | + | justify-content: center; | |
| 144 | + | text-align: center; | |
| 145 | + | padding: var(--gap-page) var(--gap-pane); | |
| 146 | + | color: var(--content-muted); | |
| 147 | + | line-height: 1.6; | |
| 148 | + | gap: var(--gap-section); | |
| 149 | + | } | |
| 150 | + | .empty-state-icon { | |
| 151 | + | font-size: 2.5rem; | |
| 152 | + | opacity: 0.6; | |
| 153 | + | } | |
| 154 | + | .empty-state-text { | |
| 155 | + | font-size: var(--font-size-base); | |
| 156 | + | color: var(--content-secondary); | |
| 157 | + | margin: 0; | |
| 158 | + | } | |
| 159 | + | .empty-state--compact { | |
| 160 | + | padding: var(--gap-pane) var(--gap-group); | |
| 161 | + | gap: var(--gap-peer); | |
| 162 | + | } | |
| 163 | + | .empty-state--compact .empty-state-icon { font-size: var(--font-size-3xl); } | |
| 164 | + | ||
| 165 | + | /* Control stack (star / pin + read dot) passed into renderRow's icon slot. | |
| 166 | + | The .row layout provides the gap to the content; no margin needed here. */ | |
| 167 | + | .article-controls { | |
| 168 | + | display: flex; | |
| 169 | + | flex-direction: column; | |
| 170 | + | align-items: center; | |
| 171 | + | min-width: 24px; | |
| 172 | + | } | |
| 173 | + | ||
| 174 | + | .star-btn { | |
| 175 | + | background: none; | |
| 176 | + | border: none; | |
| 177 | + | cursor: pointer; | |
| 178 | + | font-size: var(--font-size-xl); | |
| 179 | + | color: var(--content-muted); | |
| 180 | + | padding: var(--step-tight) var(--step-snug); | |
| 181 | + | margin: calc(-1 * var(--step-tight)) calc(-1 * var(--step-snug)); | |
| 182 | + | line-height: 1; | |
| 183 | + | } | |
| 184 | + | .star-btn:hover { color: var(--category-four); transform: scale(1.1); } | |
| 185 | + | .star-btn.starred { color: var(--category-four); } | |
| 186 | + | ||
| 187 | + | .read-indicator { | |
| 188 | + | width: 7px; | |
| 189 | + | height: 7px; | |
| 190 | + | border-radius: var(--radius-full); | |
| 191 | + | margin-top: var(--gap-bound); | |
| 192 | + | } | |
| 193 | + | .read-indicator.unread { background-color: var(--danger); } | |
| 194 | + | ||
| 195 | + | /* Content column is renderRow's .row-content (flex:1; min-width:0). */ | |
| 196 | + | ||
| 197 | + | /* Header slot holds the author/time meta line (+ mobile source badge). */ | |
| 198 | + | .article-meta { | |
| 199 | + | display: flex; | |
| 200 | + | justify-content: space-between; | |
| 201 | + | align-items: flex-start; | |
| 202 | + | margin-bottom: var(--step-hair); | |
| 203 | + | } | |
| 204 | + | .article-author { | |
| 205 | + | font-weight: 700; | |
| 206 | + | color: var(--danger); | |
| 207 | + | font-size: var(--font-size-md); | |
| 208 | + | } | |
| 209 | + | ||
| 210 | + | /* Title: two-line clamp (overrides .row-primary's single flex line). */ | |
| 211 | + | .row--article .row-primary { | |
| 212 | + | display: -webkit-box; | |
| 213 | + | -webkit-line-clamp: 2; | |
| 214 | + | -webkit-box-orient: vertical; | |
| 215 | + | overflow: hidden; | |
| 216 | + | font-weight: 400; | |
| 217 | + | line-height: 1.4; | |
| 218 | + | font-size: var(--font-size-base); | |
| 219 | + | } | |
| 220 | + | .row--article .row-secondary { | |
| 221 | + | font-size: var(--font-size-md); | |
| 222 | + | margin-top: var(--step-hair); | |
| 223 | + | } | |
| 224 | + | /* .article-source-badge (mobile-only) lives in section 38. */ | |
| 225 | + | ||
| 226 | + | .load-more { padding: var(--gap-group); text-align: center; } | |
| 227 | + | ||
| 228 | + | /* === 8. Detail panel =================================================== */ | |
| 229 | + | .detail-panel { | |
| 230 | + | width: 400px; | |
| 231 | + | min-width: 400px; | |
| 232 | + | background-color: var(--surface-overlay); | |
| 233 | + | border-left: var(--border-width) solid var(--border); | |
| 234 | + | display: none; | |
| 235 | + | flex-direction: column; | |
| 236 | + | overflow-y: auto; | |
| 237 | + | } | |
| 238 | + | ||
| 239 | + | .detail-header { | |
| 240 | + | display: flex; | |
| 241 | + | justify-content: flex-end; | |
| 242 | + | gap: var(--gap-bound); | |
| 243 | + | padding: var(--gap-peer); | |
| 244 | + | border-bottom: var(--border-width) solid var(--border); | |
| 245 | + | background-color: var(--surface-sunken); | |
| 246 | + | } | |
| 247 | + | ||
| 248 | + | .item-detail { padding: var(--gap-group); } | |
| 249 | + | ||
| 250 | + | .detail-title { | |
| 251 | + | font-size: var(--font-size-xl); | |
| 252 | + | margin-bottom: var(--gap-section); | |
| 253 | + | color: var(--content); | |
| 254 | + | font-weight: 700; | |
| 255 | + | line-height: 1.4; | |
| 256 | + | } | |
| 257 | + | ||
| 258 | + | .detail-meta { | |
| 259 | + | display: flex; | |
| 260 | + | flex-wrap: wrap; | |
| 261 | + | gap: var(--gap-peer); | |
| 262 | + | margin-bottom: var(--gap-peer); | |
| 263 | + | color: var(--content-secondary); | |
| 264 | + | font-size: var(--font-size-md); | |
| 265 | + | padding-bottom: var(--gap-section); | |
| 266 | + | border-bottom: var(--border-width) solid var(--border); | |
| 267 | + | } | |
| 268 | + | ||
| 269 | + | .detail-tags { | |
| 270 | + | display: flex; | |
| 271 | + | flex-wrap: wrap; | |
| 272 | + | gap: var(--gap-peer); | |
| 273 | + | margin-bottom: var(--gap-section); | |
| 274 | + | } | |
| 275 | + |