diff --git a/board/_theme.py b/board/_theme.py index 18ad7ed..364c51f 100644 --- a/board/_theme.py +++ b/board/_theme.py @@ -11,12 +11,18 @@ import it and ask for the stylesheet, the hero and the small components; they keep owning what goes in the rows. -The design in one line: *the page wears its organ's logo*. Each organ has an -accent taken from its logo's glyph colour, and every board opens with a dark -hero reproducing that logo's wordmark — white `PyAuto`, accent-coloured organ -name, letterspaced tagline underneath, inside the logo's ring. 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. +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. + +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: @@ -30,8 +36,9 @@ `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 -palettes are sampled from the actual logo files: the glyph colour and the -wordmark's own tagline, straight off `logo.png` in each organ's repo. The +palettes are sampled from the actual logo files: the mark, the glyph colour +and the wordmark's own tagline, straight off `logo.png` in each organ's repo. +The umbrella board has no logo, so its accent and tagline are designed to sit in the family instead. Every accent is checked for >= 4.5:1 contrast against its own background (light on `#fff`, dark on `#0d1117`). @@ -41,50 +48,215 @@ # ink_light / ink_dark: the accent as *text*, per colour scheme. # glow: the logo's own glyph colour — used only on the dark hero, where it # needs to sing rather than pass a contrast check against white. +# glow2: the far end of the wordmark's gradient, for the two logos that set +# their organ name in one (Brain runs blue → violet, Memory violet → +# purple). Omitted where the logo's wordmark is a flat colour. # hero: (lift, base) — the hero's radial gradient, echoing each logo's # vignette: a tinted lift behind the wordmark falling to near-black. ORGANS = { "mind": { "organ": "Mind", - "glyph": "\U0001f4cb", "tagline": "Intent. Priority. Flow.", + "tagline": "Intent. Priority. Flow.", "ink_light": "#0a7d72", "ink_dark": "#2ee6cf", "glow": "#00d1ba", - "hero": ("#0a3f39", "#000000"), + "hero": ("#07302b", "#000000"), }, "brain": { "organ": "Brain", - "glyph": "\U0001f9e0", "tagline": "Reason. Plan. Decide.", + "tagline": "Reason. Plan. Decide.", "ink_light": "#2159c9", "ink_dark": "#6f9dff", "glow": "#4d8bff", - "hero": ("#1b2a6b", "#000312"), + "glow2": "#a855f7", + "hero": ("#16234f", "#000312"), }, "heart": { "organ": "Heart", - "glyph": "❤️", "tagline": "Check. Validate. Protect.", + "tagline": "Check. Validate. Protect.", "ink_light": "#b50f1a", "ink_dark": "#ff6b73", "glow": "#ff1c28", - "hero": ("#4a0a10", "#000000"), + "hero": ("#37080d", "#000000"), }, "hands": { "organ": "Hands", - "glyph": "\U0001f6e0️", "tagline": "Build. Execute. Deliver.", + "tagline": "Build. Execute. Deliver.", "ink_light": "#9a5400", "ink_dark": "#ffa733", "glow": "#ff9201", - "hero": ("#4a2c00", "#000000"), + "hero": ("#372100", "#000000"), }, "memory": { "organ": "Memory", - "glyph": "\U0001f4da", "tagline": "Remember. Learn. Evolve.", + "tagline": "Remember. Learn. Evolve.", "ink_light": "#6b34d6", "ink_dark": "#b37eff", "glow": "#b37eff", - "hero": ("#2e1a5c", "#000000"), + "glow2": "#8b5cf6", + "hero": ("#221345", "#000000"), }, # The umbrella is the one board with no logo to sample: the accent and # the tagline are designed to sit in the family rather than read off a # file. Replace both from the logo if that repo ever grows one. "organism": { "organ": "Scientist", - "glyph": "\U0001f9ec", "tagline": "Describe. Build. Release.", + "tagline": "Describe. Build. Release.", "ink_light": "#0b6fa4", "ink_dark": "#5ec7f5", "glow": "#38bdf8", - "hero": ("#0c3f5c", "#00070d"), + "hero": ("#092e43", "#00070d"), }, } + +# ----------------------------------------------------------------- marks --- +# Each organ's logo glyph, traced off `logo.png` as line art on a 48x48 grid: +# a ring (closed, or left open where the logo opens it), the glyph inside it, +# and the small filled nodes the family's drawing style uses. Stroke colour +# is inherited, so one mark serves the hero, light mode and dark mode alike. +# +# Why drawn rather than embedded: the boards are self-contained single files +# read on phones over mobile data. The logos are 80-150KB of PNG each; these +# are ~1KB of path data, stay sharp on any screen, and take the accent. +# Keep them that way — a mark is a logo's glyph in line art, not a rendering. +MARKS = { + # Mind — a left-facing head profile, a check inside the skull, three + # node-tipped signal lines leaving the back of the head, inside a ring. + "mind": ( + '' + '' + '' + '' + '' + '' + '' + '' + '' + ), + # Brain — a circuit brain: two lobed hemispheres either side of a spine, + # each branching into elbowed, node-tipped traces, inside a ring. + "brain": ( + '' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '' + ), + # Heart — a heart outline crossed by an ECG trace, with the check badge + # sitting in the ring's lower-right gap. + "heart": ( + '' + '' + '' + '' + '' + ), + # Hands — an open palm offering a gear, speed lines behind the cuff, + # inside a ring left open at the lower-left where the lines enter. + "hands": ( + '' + '' + '' + '' + '' + '' + '' + ), + # Memory — an open book with a node-tree growing out of its spine and a + # scatter of stars, under an arc open at the bottom. + "memory": ( + '' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '' + ), + # Umbrella — no logo to sample, so the family language (ring, line-art, + # node dots) applied to the double helix the board already carried. + "organism": ( + '' + '' + '' + '' + '' + '' + '' + '' + ), +} + import html as _html WORD = "PyAuto" # the half of every wordmark that stays white @@ -100,6 +272,18 @@ def organ(key): return ORGANS.get(key, ORGANS["organism"]) +def mark(key): + """The organ's logo glyph as an inline SVG, drawn in the current colour. + + Unknown keys fall back with `organ()`, so an adopting fork's board wears + the umbrella's mark rather than rendering an empty ring. + """ + art = MARKS.get(key) or MARKS["organism"] + return ('') + + # -------------------------------------------------------------------- css --- # One stylesheet, two colour schemes, one substituted accent. Kept in the # same dense hand-wrapped style as the renderers that inline it: this ships @@ -108,7 +292,8 @@ def organ(key): :root{color-scheme:light dark;--bg:#fff;--fg:#1f2328;--muted:#59636e; --line:#d8dee4;--btn:#f6f8fa;--ok:#1a7f37;--warn:#9a6700;--bad:#d1242f; --accent:%(ink_light)s;--tint:%(ink_light)s14;--edge:%(ink_light)s3d; - --hero-lift:%(hero_lift)s;--hero-base:%(hero_base)s;--glow:%(glow)s} + --hero-lift:%(hero_lift)s;--hero-base:%(hero_base)s;--glow:%(glow)s; + --glow2:%(glow2)s} @media(prefers-color-scheme:dark){:root{--bg:#0d1117;--fg:#f0f6fc; --muted:#9198a1;--line:#2c333c;--btn:#151b23;--ok:#3fb950;--warn:#d29922; --bad:#f85149;--accent:%(ink_dark)s;--tint:%(ink_dark)s1f; @@ -127,23 +312,37 @@ def organ(key): /* --- 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); - background-image:radial-gradient(105%% 130%% at 50%% -25%%, - var(--hero-lift) 0%%,var(--hero-base) 68%%)} + background-image:radial-gradient(78%% 104%% at 50%% 14%%, + var(--hero-lift) 0%%,var(--hero-base) 72%%)} @media(min-width:46rem){.hero{margin:1rem 0 1.6rem;border-radius:16px}} .hero::after{content:"";position:absolute;left:12%%;right:12%%;bottom:0; height:2px;background:linear-gradient(90deg,transparent,var(--glow), transparent);opacity:.75} -.orb{display:inline-flex;align-items:center;justify-content:center; - width:3.1rem;height:3.1rem;margin-bottom:.85rem;border-radius:50%%; - font-size:1.45rem;line-height:1;border:1.5px solid var(--glow); - background:radial-gradient(circle,#ffffff14 0%%,transparent 70%%); - box-shadow:0 0 22px -4px var(--glow)} +/* The mark carries its own ring, so the frame is light and glow only — + the logos set their glyph on black with a halo, not in a chip. */ +.orb{display:block;width:5.6rem;height:5.6rem;margin:0 auto .75rem; + color:var(--glow);filter:drop-shadow(0 0 9px %(glow)s7a)} +.orb svg{display:block;width:100%%;height:100%%} .hero h1{margin:0;font-size:1.85rem;line-height:1.1;font-weight:700; letter-spacing:-.022em;color:#fff} -.hero h1 b{color:var(--glow);font-weight:700} +/* Two wordmarks run their organ name as a gradient; for the rest --glow2 + repeats --glow, so the same rule paints a flat colour. */ +.hero h1 b{font-weight:700;color:var(--glow); + background:linear-gradient(96deg,var(--glow),var(--glow2)); + -webkit-background-clip:text;background-clip:text} +@supports(-webkit-background-clip:text){ + .hero h1 b{-webkit-text-fill-color:transparent}} .hero .kind{display:block;margin-top:.5rem;font-size:.66rem;font-weight:600; letter-spacing:.26em;text-transform:uppercase;color:#ffffffa6} -.hero .tag{margin:.85rem 0 0;font-size:.63rem;font-weight:600; +/* The hairline with a lit dot at its centre: every logo separates wordmark + from tagline with one, and it is the detail that reads as "same mark". */ +.hero .rule{position:relative;width:11.5rem;height:1px;margin:1rem auto .75rem; + background:linear-gradient(90deg,transparent,var(--glow),transparent); + opacity:.7} +.hero .rule::after{content:"";position:absolute;left:50%%;top:50%%; + width:.4rem;height:.4rem;margin:-.2rem 0 0 -.2rem;border-radius:50%%; + background:var(--glow);box-shadow:0 0 7px 1px var(--glow)} +.hero .tag{margin:0;font-size:.63rem;font-weight:600; letter-spacing:.3em;text-transform:uppercase;color:var(--glow);opacity:.9} .lede{margin:0 0 .9rem} /* --- sections ---------------------------------------------------------- */ @@ -231,7 +430,8 @@ def css(key): o = organ(key) lift, base = o["hero"] sheet = _CSS % {"ink_light": o["ink_light"], "ink_dark": o["ink_dark"], - "glow": o["glow"], "hero_lift": lift, "hero_base": base} + "glow": o["glow"], "glow2": o.get("glow2", o["glow"]), + "hero_lift": lift, "hero_base": base} chips = "".join(_CHIP % {"sel": f'.boards a[data-organ="{k}"]', "light": v["ink_light"], "dark": v["ink_dark"]} for k, v in ORGANS.items()) @@ -240,16 +440,17 @@ def css(key): # ------------------------------------------------------------- components --- def hero(key, kind, lede_html=""): - """The masthead: the organ's logo re-drawn as type. + """The masthead: the organ's logo re-drawn — mark, wordmark, rule, tagline. `kind` is what this page *is* under the wordmark ("Dashboard", "Board"), so the mark stays the organ's and the page keeps its own name. """ o = organ(key) lede = f'

{lede_html}

' if lede_html else "" - return (f'
{o["glyph"]}' + return (f'
{mark(key)}' f'

{WORD}{o["organ"]}' f'{kind}

' + f'
' f'

{o["tagline"]}

{lede}') diff --git a/tests/test_board_theme.py b/tests/test_board_theme.py index 760b250..d4b4b94 100644 --- a/tests/test_board_theme.py +++ b/tests/test_board_theme.py @@ -30,10 +30,13 @@ def test_every_declared_board_has_a_complete_palette_entry(): for key in POLICY_BOARDS: assert key in _theme.ORGANS, key o = _theme.ORGANS[key] - for field in ("organ", "glyph", "tagline", + for field in ("organ", "tagline", "ink_light", "ink_dark", "glow", "hero"): assert o.get(field), f"{key}.{field}" assert len(o["hero"]) == 2 + # The mark is the logo's own glyph; a board without one would render + # an empty ring where its logo should be. + assert key in _theme.MARKS, key def _luminance(hex_colour): @@ -71,11 +74,30 @@ def test_each_board_wears_its_own_accent(): assert _theme.ORGANS["mind"]["glow"] not in brain +def test_every_mark_is_line_art_that_takes_the_accent(): + # The marks replaced emoji precisely so they inherit the organ colour and + # stay sharp; a hard-coded fill or a stray emoji would undo both. + for key, art in _theme.MARKS.items(): + assert "currentColor" in art or "