diff --git a/board/AGENTS.md b/board/AGENTS.md index 58153e40..22e2cff7 100644 --- a/board/AGENTS.md +++ b/board/AGENTS.md @@ -71,9 +71,11 @@ rather than drifting page by page. Each organ has an accent sampled from its own logo, and every board opens with a dark hero reproducing that logo's wordmark — white `PyAuto`, the organ -name in its accent, the logo's tagline underneath. Below the hero the page -goes back to being a plain readable document, because these are lists people -scan on a phone before breakfast. +name in its accent, the logo's tagline underneath. Below the hero the organ +keeps speaking: the accent is the page's **type colour**, so section +headings, disclosure summaries, code spans and the emphasised head of a row +are all in the organ's hue. Only the running prose stays near-black, because +these are lists people scan on a phone before breakfast. *A phone before breakfast* is a size, not a mood: the sheet makes wrapping the page **default** (`overflow-wrap` on `body`, inherited), so a run URL or a @@ -103,6 +105,16 @@ already wrote, so a task looks like itself on both pages. Above the sections, a header strip carries one number per section that can ask something of a human; a source that could not be read counts `–`, never a zero. +The accent is the page's **type colour**, not merely its link colour: the +things that give a page its shape — section headings, disclosure summaries, +code spans, the emphasised head of a row — are set in the organ's hue, so a +board reads as its organ from top to bottom instead of as grey GitHub chrome +under a coloured masthead. Only the running prose stays near-black. That +reservation is literal in the stylesheet: the accent rule is `b:not([class])`, +so an element carrying a semantic class — `ok`, `warn`, `bad`, `muted`, a pill +tone — keeps the colour that class means, and only unclassed emphasis takes +the organ's hue. + ## Configuration Instance vocabulary lives in `config/policy.yaml` under `board:` (the declared diff --git a/board/_theme.py b/board/_theme.py index 883e3ed5..098316a3 100644 --- a/board/_theme.py +++ b/board/_theme.py @@ -14,9 +14,10 @@ The design in one line: *the page wears its organ's logo*. Every board opens with a dark hero that redraws the logo it sits under: the organ's own mark in line art, the wordmark (white `PyAuto`, the organ name in its accent), the -hairline-and-dot rule, and the letterspaced tagline. Below the hero the page -returns to a plain readable document, because these are lists people scan on -a phone before breakfast, not dashboards to admire. +hairline-and-dot rule, and the letterspaced tagline. Below the hero the organ +keeps speaking: the accent runs on through the page's *type* rather than +stopping at the masthead. Body copy stays plain, because these are lists +people scan on a phone before breakfast, not dashboards to admire. The mark is **drawn, not borrowed**. An emoji stood in for it at first, which read as a cartoon beside the real logo — 🧠 is not the Brain's circuit brain. @@ -24,15 +25,27 @@ it inherits the accent, stays sharp at any size, and costs ~1KB in the one page that uses it, where the 80-150KB `logo.png` would be most of the page. -Colour carries meaning, never decoration: +The accent is the page's **type colour**, not merely its link colour: the +things that give a page its shape — section headings, disclosure summaries, +code spans, the emphasised head of a row — are all set in the organ's hue, so +a board reads as its organ from top to bottom instead of as grey GitHub +chrome under a coloured masthead. Only the running prose stays near-black. -* the **accent** is organ identity — one hue per board, used for links, - section rules, stats and the hero; +Colour is doing two jobs at once now, and the decorative one never overwrites +the semantic one: + +* the **accent** is organ identity — links, headings, summaries, code, the + section rules, the stats and the hero; * **pills** are the task facets (target, difficulty, autonomy, priority), so a backlog can be scanned by colour instead of read word by word; * **ok / warn / bad** stay reserved for the verdict semantics they already carry on the Heart-style boards. +That reservation is literal in the stylesheet: the accent rule is +`b:not([class])`, so an element carrying a semantic class — `ok`, `warn`, +`bad`, `muted`, a pill tone — keeps the colour that class means, and only +unclassed emphasis takes the organ's hue. + `ORGANS` is the palette table, keyed by the same board names as `config/policy.yaml` `board: boards:` — that mapping is the declared config surface an adopting fork replaces, so no repo is named here. Five of the six @@ -315,6 +328,13 @@ def mark(key): pre{overflow-x:auto} a{color:var(--accent);text-decoration:none} a:hover{text-decoration:underline} +/* The accent is the page's *type* colour, not just its link colour: the + things that give a page its shape — headings, disclosure summaries, code + spans, the emphasised head of a row — are all set in the organ's hue, so + the page reads as its organ instead of as grey GitHub chrome. Semantics + are untouched: anything carrying a class (ok/warn/bad, the pills, muted) + keeps the colour that class means. */ +b:not([class]),strong:not([class]){color:var(--accent)} .muted{color:var(--muted)} .ok{color:var(--ok)}.warn{color:var(--warn)}.bad{color:var(--bad)} /* A bare list is page text, not a quotation. The UA's 40px indent steps it in @@ -325,8 +345,8 @@ def mark(key): specificity. */ ul,ol{padding-left:1.15rem} code{font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:.92em; - background:var(--btn);border:1px solid var(--line);padding:.05em .35em; - border-radius:5px} + color:var(--accent);background:var(--tint);border:1px solid var(--edge); + padding:.05em .35em;border-radius:5px} /* --- hero: the logo, rendered as type ---------------------------------- */ .hero{margin:0 -1rem 1.4rem;padding:2.1rem 1.4rem 1.7rem;position:relative; overflow:hidden;text-align:center;color:#fff;background:var(--hero-base); @@ -365,16 +385,25 @@ def mark(key): .lede{margin:0 0 .9rem} /* --- sections ---------------------------------------------------------- */ h2{font-size:1.1rem;margin:2.1rem 0 .3rem;padding:0 0 .35rem;font-weight:650; - border-bottom:2px solid var(--edge)} + position:relative;color:var(--accent);border-bottom:2px solid var(--edge)} +/* The hero's lit rule, quieted and reused: the hairline under a section + starts at full accent and fades into the edge tone. Purely the ::after, + so a browser that skips it still gets the plain hairline. */ +h2::after{content:"";position:absolute;left:0;bottom:-2px;width:44%%;height:2px; + background:linear-gradient(90deg,var(--accent),var(--accent) 15%%,transparent)} +h2 a{color:inherit} +/* The heading is the accent; its parenthetical stays a quiet aside rather + than competing at the same weight. */ +h2 .muted{font-weight:400} h3{font-size:.98rem;margin:1.3rem 0 .2rem;font-weight:650;color:var(--accent)} /* --- rows -------------------------------------------------------------- */ .task{display:flex;gap:.6rem;align-items:flex-start;padding:.45rem .35rem; margin:0 -.35rem;border-bottom:1px solid var(--line);border-radius:7px} -.task:hover{background:var(--tint)} +.task:hover{background:var(--tint);box-shadow:inset 2px 0 0 var(--accent)} .task p{margin:.25rem 0 0;flex:1} button.copy{flex:0 0 auto;width:2.6rem;height:2.6rem;font-size:1.1rem; - border:1px solid var(--line);border-radius:9px;background:var(--btn); - cursor:pointer;color:var(--fg);transition:border-color .12s,color .12s} + border:1px solid var(--edge);border-radius:9px;background:var(--tint); + cursor:pointer;color:var(--accent);transition:border-color .12s,color .12s} button.copy:hover{border-color:var(--accent);color:var(--accent)} button.copy.ok{color:var(--ok);border-color:var(--ok);background:transparent} button.copy.term{font-size:.95rem} @@ -383,7 +412,7 @@ def mark(key): color:var(--muted);cursor:pointer;font:inherit;font-size:.9em} button.more:hover{color:var(--accent);border-style:solid} details{margin:.5rem 0} -summary{cursor:pointer;font-weight:600;padding:.4rem 0} +summary{cursor:pointer;font-weight:600;padding:.4rem 0;color:var(--accent)} summary::marker{color:var(--accent)} /* --- facet pills: the backlog, scannable by colour --------------------- */ .facets{color:var(--muted);font-size:.85em} @@ -416,7 +445,7 @@ def mark(key): .stats b{display:block;font-size:1.15rem;line-height:1.2;color:var(--accent); font-variant-numeric:tabular-nums} .stats span{font-size:.68rem;letter-spacing:.1em;text-transform:uppercase; - color:var(--muted)} + color:var(--accent);opacity:.75} /* --- verdict banner ---------------------------------------------------- */ .verdict{margin:0 0 1rem;padding:.7rem .85rem;border-radius:10px; border:1px solid var(--line);border-left:4px solid var(--muted); @@ -425,6 +454,9 @@ def mark(key): .verdict.warn{border-left-color:var(--warn)} .verdict.bad{border-left-color:var(--bad)} .verdict b{display:block;font-size:1.02rem} +.verdict.ok b{color:var(--ok)} +.verdict.warn b{color:var(--warn)} +.verdict.bad b{color:var(--bad)} /* --- tables ------------------------------------------------------------ */ table.recent{width:100%%;border-collapse:collapse;font-size:.95em} table.recent td{border-bottom:1px solid var(--line); @@ -432,8 +464,8 @@ def mark(key): table.recent tr:hover td{background:var(--tint)} table.recent td.when{white-space:nowrap;color:var(--muted); font-variant-numeric:tabular-nums} -table.recent td.what{white-space:nowrap;color:var(--muted);font-size:.85em; - padding-top:.58rem} +table.recent td.what{white-space:nowrap;color:var(--accent);font-size:.85em; + padding-top:.58rem;opacity:.8} table.recent td.pick{width:2.6rem;padding-right:0} table.recent button.copy{width:2.2rem;height:2.2rem;font-size:.95rem} /* On a phone a four-column row is not a row, it is a ribbon. Measured at a diff --git a/tests/test_board_theme.py b/tests/test_board_theme.py index d8c62a2a..ca8c4444 100644 --- a/tests/test_board_theme.py +++ b/tests/test_board_theme.py @@ -68,6 +68,31 @@ def test_css_substitutes_every_placeholder(): assert "prefers-color-scheme:dark" in sheet +def test_the_accent_is_the_pages_type_colour_not_only_its_link_colour(): + # The organ is meant to carry on past the masthead: the elements that give + # a page its shape are all set in the accent, so a board reads as its + # organ top to bottom rather than as grey chrome under a coloured hero. + sheet = _theme.css("brain") + for rule in ("h2{", "h3{", "summary{", "code{"): + block = sheet[sheet.index(rule):sheet.index("}", sheet.index(rule))] + assert "color:var(--accent)" in block, rule + # …and unclassed emphasis — the head of a row, a count, a label. + assert "b:not([class]),strong:not([class]){color:var(--accent)}" in sheet + + +def test_a_semantic_class_keeps_its_own_colour_against_the_accent(): + # The accent is the *default* type colour, never an override: the rule is + # class-guarded, so ok/warn/bad and the pills still say what they mean. + sheet = _theme.css("brain") + assert "b:not([class])" in sheet and "b{color:var(--accent)}" not in sheet + for cls, var in ((".ok", "--ok"), (".warn", "--warn"), (".bad", "--bad")): + assert f"{cls}{{color:var({var})}}" in sheet, cls + # The verdict banner's headline says its verdict in the verdict's colour, + # at a specificity the unclassed-bold rule cannot reach. + for tone in ("ok", "warn", "bad"): + assert f".verdict.{tone} b{{color:var(--{tone})}}" in sheet + + def test_each_board_wears_its_own_accent(): mind, brain = _theme.css("mind"), _theme.css("brain") assert _theme.ORGANS["brain"]["glow"] not in mind