| 13 |
13 |
|
section}, .stack +
|
| 14 |
14 |
|
.stack-{peer,group,
|
| 15 |
15 |
|
section})
|
| 16 |
|
- |
3. Use a COMPONENT PRIMITIVE for a UI element (.button, .card, .modal,
|
| 17 |
|
- |
.form-input,
|
| 18 |
|
- |
.form-select, .badge,
|
|
16 |
+ |
3. Use a COMPONENT PRIMITIVE for a UI element (.button, .field,
|
|
17 |
+ |
.raised, .well,
|
|
18 |
+ |
.modal, .badge,
|
| 19 |
19 |
|
.tag, .toast)
|
| 20 |
|
- |
4. Extend a primitive with a modifier (.button--primary, .button--sm,
|
| 21 |
|
- |
.form-select--ghost)
|
|
20 |
+ |
4. Extend a primitive with a modifier (.button--primary, .button--sm)
|
| 22 |
21 |
|
5. ONLY THEN consider a new class, and add it to the right BAND below
|
| 23 |
22 |
|
|
| 24 |
23 |
|
A new class is a smell, not a goal. Before writing one:
|
| 63 |
62 |
|
|
| 64 |
63 |
|
BAND: COMPONENT PRIMITIVES
|
| 65 |
64 |
|
9. Buttons: .button family
|
| 66 |
|
- |
10. Forms: .form-group, .form-input
|
|
65 |
+ |
10. Forms: .form-group, .field
|
| 67 |
66 |
|
11. Modal: .modal-overlay/.modal-content
|
| 68 |
67 |
|
12. Toast: .toast
|
| 69 |
68 |
|
13. Tag: .tag
|
| 317 |
316 |
|
align-items: center;
|
| 318 |
317 |
|
}
|
| 319 |
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. */
|
| 320 |
322 |
|
.search-input {
|
| 321 |
|
- |
padding: var(--gap-peer) var(--gap-group);
|
| 322 |
|
- |
border: var(--border-width) solid var(--border);
|
| 323 |
|
- |
border-radius: var(--radius-sm);
|
| 324 |
|
- |
background-color: var(--surface-page);
|
| 325 |
|
- |
color: var(--content);
|
| 326 |
323 |
|
width: 180px;
|
| 327 |
|
- |
font-size: var(--font-size-base);
|
| 328 |
|
- |
box-shadow: var(--bevel-inset);
|
| 329 |
324 |
|
}
|
| 330 |
325 |
|
|
| 331 |
326 |
|
/* Search spinner */
|
| 346 |
341 |
|
|
| 347 |
342 |
|
.search-input::placeholder { color: var(--content-muted); }
|
| 348 |
343 |
|
|
| 349 |
|
- |
.sort-select {
|
| 350 |
|
- |
padding: var(--gap-peer) var(--gap-group);
|
| 351 |
|
- |
border: var(--border-width) solid var(--border);
|
| 352 |
|
- |
border-radius: var(--radius-sm);
|
| 353 |
|
- |
background-color: var(--surface-page);
|
| 354 |
|
- |
color: var(--content);
|
| 355 |
|
- |
cursor: pointer;
|
| 356 |
|
- |
font-size: var(--font-size-base);
|
| 357 |
|
- |
box-shadow: var(--bevel-inset);
|
| 358 |
|
- |
}
|
| 359 |
|
- |
.sort-select:focus { outline: none; border-color: var(--focus-ring); }
|
|
344 |
+ |
/* `.sort-select` is gone: every property it set is `.field` plus the
|
|
345 |
+ |
`select.field` cursor, and its `:focus { outline: none }` was killing the
|
|
346 |
+ |
generated `.field:focus-visible` ring on the one control that had no ring of
|
|
347 |
+ |
its own to replace it with. The call sites carry `.field`. */
|
| 360 |
348 |
|
|
| 361 |
349 |
|
/* === 6. Sidebar ======================================================== */
|
| 362 |
350 |
|
.sidebar {
|
| 389 |
377 |
|
background-color: var(--surface-page);
|
| 390 |
378 |
|
}
|
| 391 |
379 |
|
|
| 392 |
|
- |
.items-list { list-style: none; }
|
|
380 |
+ |
/* `.items-list`, `.plugin-list` and `.sources-list` each wrote
|
|
381 |
+ |
`list-style: none` by hand. All three carry `.list` now, which is the
|
|
382 |
+ |
generated reset; `.sources-list` keeps its padding and the other two had
|
|
383 |
+ |
nothing else to keep. */
|
| 393 |
384 |
|
|
| 394 |
385 |
|
/* Article rows: feed items + reading-list bookmarks. Uses the renderRow .row
|
| 395 |
386 |
|
layout (icon / content / actions, flex + gap) plus article appearance +
|
| 707 |
698 |
|
}
|
| 708 |
699 |
|
|
| 709 |
700 |
|
/* === 10. Forms =========================================================
|
| 710 |
|
- |
Canonical: .form-group + .form-input. Charter target (F5): split
|
| 711 |
|
- |
.form-input into .form-input / .form-select / .form-textarea so visual
|
| 712 |
|
- |
treatment per kind is targetable; add .form-label class on labels;
|
| 713 |
|
- |
add BB.ui.renderFormField helper.
|
|
701 |
+ |
Canonical: .form-group + .field.
|
|
702 |
+ |
|
|
703 |
+ |
One class for every field kind, with the kind riding on the element
|
|
704 |
+ |
(`select.field`, `textarea.field`) rather than on a second class. The F2
|
|
705 |
+ |
split into .form-input / .form-select / .form-textarea is retired: all
|
|
706 |
+ |
twenty-odd call sites had .form-input on an <input>, .form-select on a
|
|
707 |
+ |
<select> and .form-textarea on a <textarea>, with no exceptions, so the
|
|
708 |
+ |
class was saying what the tag already said, and an element selector cannot
|
|
709 |
+ |
be forgotten at a call site the way a modifier class can.
|
|
710 |
+ |
|
|
711 |
+ |
Fill and inset edge now come from the generated layout.css, where a field
|
|
712 |
+ |
is a Depth::Well. Every rule here used to write `background-color:
|
|
713 |
+ |
var(--surface-page)` under `box-shadow: var(--bevel-inset)` -- a page fill
|
|
714 |
+ |
beneath a recessed edge -- and so did .search-input, .sort-select and (with
|
|
715 |
+ |
no bevel at all) the two mobile fields. Five hand-written answers to one
|
|
716 |
+ |
question. goingson had the same pair and retired it the same way; its
|
|
717 |
+ |
.field block carries the long form of this note.
|
| 714 |
718 |
|
========================================================================== */
|
| 715 |
719 |
|
.form-group { margin-bottom: var(--gap-section); }
|
| 716 |
720 |
|
|
| 717 |
|
- |
.form-input {
|
| 718 |
|
- |
width: 100%;
|
| 719 |
|
- |
padding: var(--gap-peer);
|
|
721 |
+ |
/* Shape only. The wider padding is what .search-input and .sort-select
|
|
722 |
+ |
carried; a field inside a form group keeps the tighter one, below. */
|
|
723 |
+ |
.field {
|
|
724 |
+ |
padding: var(--gap-peer) var(--gap-group);
|
| 720 |
725 |
|
border: var(--border-width) solid var(--border);
|
| 721 |
726 |
|
border-radius: var(--radius-sm);
|
| 722 |
|
- |
background-color: var(--surface-page);
|
| 723 |
727 |
|
color: var(--content);
|
| 724 |
728 |
|
font-size: var(--font-size-base);
|
| 725 |
|
- |
box-shadow: var(--bevel-inset);
|
| 726 |
729 |
|
}
|
| 727 |
730 |
|
|
| 728 |
|
- |
/* F2 additions (2026-06-02): split .form-input into kind-specific
|
| 729 |
|
- |
primitives so visual treatment per kind is targetable. .form-select and
|
| 730 |
|
- |
.form-textarea inherit .form-input today; future surface-specific tweaks
|
| 731 |
|
- |
land on the kind-specific class. .form-label replaces the unclassed
|
| 732 |
|
- |
`<label>` under `.form-group`. */
|
|
731 |
+ |
/* A field in a form group fills it. The three fields outside one carry their
|
|
732 |
+ |
own width, which is why this is not on .field itself. */
|
|
733 |
+ |
.form-group .field {
|
|
734 |
+ |
width: 100%;
|
|
735 |
+ |
padding: var(--gap-peer);
|
|
736 |
+ |
}
|
|
737 |
+ |
|
|
738 |
+ |
select.field { cursor: pointer; }
|
|
739 |
+ |
|
|
740 |
+ |
/* layout.css draws the disabled field -- muted content, not-allowed cursor --
|
|
741 |
+ |
but the two rules above set colour and cursor at rest in an unlayered sheet,
|
|
742 |
+ |
so the resting rules win and the generated disabled arm never applies. An
|
|
743 |
+ |
app must state the disabled variant of every property it sets at rest.
|
|
744 |
+ |
Found by adopting the generated states, not reported. The fill and the edge
|
|
745 |
+ |
come from layout.css and are not restated here. */
|
|
746 |
+ |
.field:disabled,
|
|
747 |
+ |
.field[aria-disabled="true"] {
|
|
748 |
+ |
color: var(--content-muted);
|
|
749 |
+ |
cursor: not-allowed;
|
|
750 |
+ |
}
|
|
751 |
+ |
|
|
752 |
+ |
textarea.field {
|
|
753 |
+ |
font-family: inherit;
|
|
754 |
+ |
resize: vertical;
|
|
755 |
+ |
min-height: 4rem;
|
|
756 |
+ |
}
|
|
757 |
+ |
|
|
758 |
+ |
/* .form-label replaces the unclassed `<label>` under `.form-group`. */
|
| 733 |
759 |
|
.form-group label,
|
| 734 |
760 |
|
.settings-content .form-group label,
|
| 735 |
761 |
|
.form-label {
|
| 739 |
765 |
|
font-size: var(--font-size-md);
|
| 740 |
766 |
|
font-weight: 700;
|
| 741 |
767 |
|
}
|
| 742 |
|
- |
.form-select {
|
| 743 |
|
- |
width: 100%;
|
| 744 |
|
- |
padding: var(--gap-peer);
|
| 745 |
|
- |
border: var(--border-width) solid var(--border);
|
| 746 |
|
- |
border-radius: var(--radius-sm);
|
| 747 |
|
- |
background-color: var(--surface-page);
|
| 748 |
|
- |
color: var(--content);
|
| 749 |
|
- |
font-size: var(--font-size-base);
|
| 750 |
|
- |
cursor: pointer;
|
| 751 |
|
- |
box-shadow: var(--bevel-inset);
|
| 752 |
|
- |
}
|
| 753 |
|
- |
.form-textarea {
|
| 754 |
|
- |
width: 100%;
|
| 755 |
|
- |
padding: var(--gap-peer);
|
| 756 |
|
- |
border: var(--border-width) solid var(--border);
|
| 757 |
|
- |
border-radius: var(--radius-sm);
|
| 758 |
|
- |
background-color: var(--surface-page);
|
| 759 |
|
- |
color: var(--content);
|
| 760 |
|
- |
font-size: var(--font-size-base);
|
| 761 |
|
- |
font-family: inherit;
|
| 762 |
|
- |
resize: vertical;
|
| 763 |
|
- |
min-height: 4rem;
|
| 764 |
|
- |
box-shadow: var(--bevel-inset);
|
| 765 |
|
- |
}
|
| 766 |
768 |
|
/* .form-row: horizontal grouping of two related fields (e.g. label + button) */
|
| 767 |
769 |
|
|
| 768 |
770 |
|
.form-actions {
|
| 832 |
834 |
|
|
| 833 |
835 |
|
.modal-body { padding: var(--gap-pane); }
|
| 834 |
836 |
|
|
| 835 |
|
- |
.plugin-list { list-style: none; }
|
| 836 |
837 |
|
|
|
838 |
+ |
|
|
839 |
+ |
/* Carries `.card`: a pressable raised object with a hover fill and an inset
|
|
840 |
+ |
pressed state, which is what these three rules wrote by hand. The fill, the
|
|
841 |
+ |
bevel, the hover and the press all come from layout.css now, wrapped in
|
|
842 |
+ |
(hover: hover) and (pointer: fine) there so a fingertip gets no phantom
|
|
843 |
+ |
hover. What is left here is the box and the accent border. */
|
| 837 |
844 |
|
.plugin-item {
|
| 838 |
845 |
|
padding: var(--gap-group);
|
| 839 |
846 |
|
border: var(--border-width) solid var(--border);
|
| 840 |
847 |
|
border-radius: var(--radius-sm);
|
| 841 |
848 |
|
margin-bottom: var(--gap-peer);
|
| 842 |
849 |
|
cursor: pointer;
|
| 843 |
|
- |
background-color: var(--surface-raised);
|
| 844 |
|
- |
box-shadow: var(--bevel-raised);
|
| 845 |
850 |
|
}
|
| 846 |
851 |
|
.plugin-item:hover {
|
| 847 |
852 |
|
border-color: var(--category-four);
|
| 848 |
|
- |
background-color: var(--surface-overlay);
|
| 849 |
|
- |
}
|
| 850 |
|
- |
.plugin-item:active {
|
| 851 |
|
- |
box-shadow: var(--bevel-inset);
|
| 852 |
853 |
|
}
|
| 853 |
854 |
|
|
| 854 |
855 |
|
/* F5 (2026-06-02): dropped .plugin-name and .plugin-desc; plugin-list
|
| 1010 |
1011 |
|
Source tags use .tag.tag--xs[data-color="muted"]; .bookmark-tag composes on
|
| 1011 |
1012 |
|
.tag with a click-to-filter modifier.
|
| 1012 |
1013 |
|
========================================================================== */
|
|
1014 |
+ |
/* Carries `.raised`, which is where the fill and the bevel come from now.
|
|
1015 |
+ |
Left standing on purpose: a tag is a label and not pressable, which is the
|
|
1016 |
+ |
argument that flattened `.badge` on 2026-08-05, so `.tag` being an object at
|
|
1017 |
+ |
all is worth a second look. That is a decision about how the app should
|
|
1018 |
+ |
look, not a place the design system was being restated, so it is recorded
|
|
1019 |
+ |
rather than taken here. */
|
| 1013 |
1020 |
|
.tag {
|
| 1014 |
|
- |
background-color: var(--surface-raised);
|
| 1015 |
1021 |
|
border: var(--border-width) solid var(--border);
|
| 1016 |
1022 |
|
border-radius: var(--radius-sm);
|
| 1017 |
1023 |
|
padding: var(--step-hair) var(--step-base);
|
| 1018 |
1024 |
|
font-size: var(--font-size-sm);
|
| 1019 |
1025 |
|
color: var(--content-secondary);
|
| 1020 |
|
- |
box-shadow: var(--bevel-raised);
|
| 1021 |
1026 |
|
}
|
| 1022 |
1027 |
|
|
| 1023 |
1028 |
|
/* F2 additions (2026-06-02): color variants via [data-color] attribute.
|
| 1332 |
1337 |
|
/* === 20. Sources list: sidebar entries ================================
|
| 1333 |
1338 |
|
Charter target (F5): .source-item collapses onto .card --row.
|
| 1334 |
1339 |
|
========================================================================== */
|
| 1335 |
|
- |
.sources-list { list-style: none; padding: var(--gap-bound) 0; }
|
|
1340 |
+ |
.sources-list { padding: var(--gap-bound) 0; }
|
| 1336 |
1341 |
|
|
| 1337 |
1342 |
|
.source-item {
|
| 1338 |
1343 |
|
display: flex;
|
| 1360 |
1365 |
|
}
|
| 1361 |
1366 |
|
|
| 1362 |
1367 |
|
|
|
1368 |
+ |
/* Carries `.raised`; same as `.tag`, the fill and the bevel are the design
|
|
1369 |
+ |
system's now. */
|
| 1363 |
1370 |
|
.source-count {
|
| 1364 |
|
- |
background-color: var(--surface-raised);
|
| 1365 |
1371 |
|
padding: var(--step-hair) var(--step-snug);
|
| 1366 |
1372 |
|
border-radius: var(--radius-xl);
|
| 1367 |
1373 |
|
font-size: var(--font-size-xs);
|
| 1368 |
1374 |
|
color: var(--content-muted);
|
| 1369 |
1375 |
|
border: var(--border-width) solid var(--border);
|
| 1370 |
|
- |
box-shadow: var(--bevel-raised);
|
| 1371 |
1376 |
|
min-width: 1.8rem;
|
| 1372 |
1377 |
|
text-align: center;
|
| 1373 |
1378 |
|
}
|
| 1398 |
1403 |
|
border-bottom: var(--border-width) solid var(--border);
|
| 1399 |
1404 |
|
background-color: var(--surface-overlay);
|
| 1400 |
1405 |
|
}
|
|
1406 |
+ |
/* Carries `.chip`, which is what a filter chip is: raised at rest, hover,
|
|
1407 |
+ |
press, and a `.latched` state for the one that is on. The fill and both
|
|
1408 |
+ |
bevels come from layout.css; the pill shape, the type and the accent are
|
|
1409 |
+ |
this app's. */
|
| 1401 |
1410 |
|
.tag-chip {
|
| 1402 |
|
- |
background-color: var(--surface-raised);
|
| 1403 |
1411 |
|
border: var(--border-width) solid var(--border);
|
| 1404 |
1412 |
|
border-radius: var(--radius-xl);
|
| 1405 |
1413 |
|
padding: var(--step-hair) var(--step-base);
|
| 1407 |
1415 |
|
color: var(--content-secondary);
|
| 1408 |
1416 |
|
cursor: pointer;
|
| 1409 |
1417 |
|
white-space: nowrap;
|
| 1410 |
|
- |
box-shadow: var(--bevel-raised);
|
| 1411 |
1418 |
|
}
|
| 1412 |
1419 |
|
.tag-chip:hover { border-color: var(--category-four); color: var(--content); }
|
| 1413 |
|
- |
/* An active filter chip is latched down, same inversion as a pressed button. */
|
|
1420 |
+ |
/* An active filter chip is latched down, same inversion as a pressed button.
|
|
1421 |
+ |
The inset bevel comes from `.latched`, which the markup sets alongside
|
|
1422 |
+ |
`.active`; the accent fill stays here, because which colour a latched chip
|
|
1423 |
+ |
takes is this app's call and the depth is not. */
|
| 1414 |
1424 |
|
.tag-chip.active {
|
| 1415 |
1425 |
|
background-color: var(--category-four);
|
| 1416 |
1426 |
|
border-color: var(--category-four);
|
| 1417 |
1427 |
|
color: var(--content);
|
| 1418 |
1428 |
|
font-weight: 600;
|
| 1419 |
|
- |
box-shadow: var(--bevel-inset);
|
| 1420 |
1429 |
|
}
|
| 1421 |
1430 |
|
.source-tags {
|
| 1422 |
1431 |
|
display: flex;
|
| 1501 |
1510 |
|
gap: var(--gap-bound);
|
| 1502 |
1511 |
|
margin-bottom: var(--gap-peer);
|
| 1503 |
1512 |
|
}
|
| 1504 |
|
- |
.condition-row .sort-select,
|
| 1505 |
|
- |
.condition-row .search-input {
|
|
1513 |
+ |
.condition-row .field {
|
| 1506 |
1514 |
|
flex: 1;
|
| 1507 |
1515 |
|
min-width: 0;
|
| 1508 |
1516 |
|
font-size: var(--font-size-md);
|
| 1706 |
1714 |
|
.source-delete:focus-visible,
|
| 1707 |
1715 |
|
.row--article:focus-visible,
|
| 1708 |
1716 |
|
.plugin-item:focus-visible,
|
| 1709 |
|
- |
.sort-select:focus-visible,
|
| 1710 |
1717 |
|
.toast-action:focus-visible {
|
| 1711 |
1718 |
|
outline: 2px solid var(--focus-ring);
|
| 1712 |
1719 |
|
outline-offset: 2px;
|
| 1713 |
1720 |
|
}
|
| 1714 |
1721 |
|
|
| 1715 |
|
- |
/* One focus treatment for every field kind and both focus states.
|
| 1716 |
|
- |
Was six identical rules scattered across the sheet. */
|
| 1717 |
|
- |
.search-input:focus, .search-input:focus-visible,
|
| 1718 |
|
- |
.form-input:focus, .form-input:focus-visible,
|
| 1719 |
|
- |
.form-select:focus, .form-select:focus-visible,
|
| 1720 |
|
- |
.form-textarea:focus, .form-textarea:focus-visible,
|
| 1721 |
|
- |
.mobile-search-input:focus, .mobile-search-input:focus-visible {
|
| 1722 |
|
- |
outline: none;
|
| 1723 |
|
- |
border-color: var(--focus-ring);
|
| 1724 |
|
- |
box-shadow: var(--bevel-inset), 0 0 0 2px var(--focus-ring);
|
| 1725 |
|
- |
}
|
|
1722 |
+ |
/* A field's focus ring comes from layout.css now. What stood here was this
|
|
1723 |
+ |
app's own third answer to the same question: `outline: none` plus a composed
|
|
1724 |
+ |
box-shadow that had to restate the bevel beside it. `outline: none` also
|
|
1725 |
+ |
killed the generated `.field:focus-visible`, so keeping this rule would have
|
|
1726 |
+ |
left fields with no ring rather than with two. Same 2px, and --focus-ring
|
|
1727 |
+ |
resolves to the same colour; the generated ring sits inside the well
|
|
1728 |
+ |
(`outline-offset: calc(-1 * 2px)`), which is where a well's ring belongs.
|
|
1729 |
+ |
|
|
1730 |
+ |
`:focus-visible` rather than `:focus` is not a behaviour change for a text
|
|
1731 |
+ |
field: a focused text input always matches `:focus-visible`. */
|
| 1726 |
1732 |
|
|
| 1727 |
1733 |
|
/* === 33. Mobile tab bar ================================================ */
|
| 1728 |
1734 |
|
.mobile-tab-bar {
|
| 1809 |
1815 |
|
background-color: var(--surface-page);
|
| 1810 |
1816 |
|
border-bottom: var(--border-width) solid var(--border);
|
| 1811 |
1817 |
|
}
|
|
1818 |
+ |
/* Both carry `.field`. They were the only two fields in the app with no bevel
|
|
1819 |
+ |
at all, filled with --surface-overlay against the mobile bar's own page
|
|
1820 |
+ |
fill, so mobile and desktop disagreed about what an input looks like. They
|
|
1821 |
+ |
agree now, and the fill and edge come from the same place as everywhere
|
|
1822 |
+ |
else. */
|
| 1812 |
1823 |
|
.mobile-search-input {
|
| 1813 |
1824 |
|
flex: 1;
|
| 1814 |
|
- |
padding: var(--gap-peer) var(--gap-group);
|
| 1815 |
|
- |
border: var(--border-width) solid var(--border);
|
| 1816 |
|
- |
border-radius: var(--radius-sm);
|
| 1817 |
|
- |
background-color: var(--surface-overlay);
|
| 1818 |
|
- |
color: var(--content);
|
| 1819 |
1825 |
|
font-family: inherit;
|
| 1820 |
|
- |
font-size: var(--font-size-base);
|
| 1821 |
1826 |
|
}
|
| 1822 |
1827 |
|
.mobile-sort-select {
|
| 1823 |
|
- |
padding: var(--gap-peer);
|
| 1824 |
|
- |
border: var(--border-width) solid var(--border);
|
| 1825 |
|
- |
border-radius: var(--radius-sm);
|
| 1826 |
|
- |
background-color: var(--surface-overlay);
|
| 1827 |
|
- |
color: var(--content);
|
| 1828 |
1828 |
|
font-family: inherit;
|
| 1829 |
1829 |
|
font-size: var(--font-size-md);
|
| 1830 |
1830 |
|
}
|
| 2004 |
2004 |
|
.star-btn.starred:hover { color: var(--category-four); }
|
| 2005 |
2005 |
|
.tag-chip:hover { border-color: var(--border); color: var(--content-secondary); }
|
| 2006 |
2006 |
|
.tag-chip.active:hover { border-color: var(--category-four); }
|
| 2007 |
|
- |
.plugin-item:hover { border-color: var(--border); background-color: var(--surface-raised); box-shadow: var(--bevel-raised); }
|
|
2007 |
+ |
.plugin-item:hover { border-color: var(--border); }
|
| 2008 |
2008 |
|
.context-menu-item:hover { background-color: transparent; }
|
| 2009 |
2009 |
|
}
|
| 2010 |
2010 |
|
|