Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 15 additions & 3 deletions board/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
64 changes: 48 additions & 16 deletions board/_theme.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,25 +14,38 @@
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.
`MARKS` holds each logo's glyph as inline SVG line art traced off the file:
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
Expand Down Expand Up @@ -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
Expand All @@ -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);
Expand Down Expand Up @@ -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}
Expand All @@ -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}
Expand Down Expand Up @@ -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);
Expand All @@ -425,15 +454,18 @@ 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);
padding:.45rem .4rem .45rem 0;vertical-align:top}
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
Expand Down
25 changes: 25 additions & 0 deletions tests/test_board_theme.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading