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
10 changes: 6 additions & 4 deletions docs/state-vocabulary.md
Original file line number Diff line number Diff line change
Expand Up @@ -80,10 +80,12 @@ reader that announces text and nothing else.
`--check-fail`. A metric with no value shows its reason *in place of* the
value and is styled as unknown, so an unrun metric cannot look like a failed
one.
3. **No brand colour carries financial meaning.** `--brand-*` (the green) is the
identity colour *and* the `GOOD`/`PASS`/`DIRECT` role by historical alias. A
future contributor must not lean on `--accent` to mean "recommended"; the
recommendation is a sentence, not a colour.
3. **No brand colour carries financial meaning.** Brand primitives are only
used through named roles: `--structural-*` for market provenance,
`--severity-*` for pricing outcomes, and `--check-*` for counterparty
results. In particular, `--accent` is an interactive-control affordance,
never a recommendation or investment signal. Recommendations are expressed
in text, not by borrowing a brand colour.
4. **`UNDETERMINED` and `UNAVAILABLE` are distinct.** Undetermined: we reached
the source and could not establish the fact. Unavailable: we could not
complete the request. The first is a grey with a reason; the second is an
Expand Down
34 changes: 22 additions & 12 deletions server/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,10 @@
--warning-soft: #FEF3C7;
--neutral: #475569;
--neutral-soft: #E2E8F0;
/* Structural provenance is its own family, not a verdict or warning. */
--structural-direct: var(--brand-navy);
--structural-derivative: var(--brand-slate);
--structural-nomarket: var(--neutral);
--inset: #E2E8F0;
--bg: var(--brand-cloud);
--panel: #FFFFFF;
Expand All @@ -82,12 +86,12 @@
paints data with a verdict colour. */
--chart-threshold: var(--critical);
/* Structural integrity states, not severities (docs/state-vocabulary.md).
They share the brand primitives the badges use so the same state reads
They share the structural roles used by badges so the same state reads
the same colour in both places, and they are deliberately distinct from
--ok/--warn/--bad: NO-MARKET must not read as failure. */
--chart-direct: var(--brand-navy);
--chart-derivative: var(--brand-slate);
--chart-nomarket: var(--brand-coral);
--chart-direct: var(--structural-direct);
--chart-derivative: var(--structural-derivative);
--chart-nomarket: var(--structural-nomarket);
/* Movement decomposition (#637): the corridor's own move and the
benchmark's move are two data series, painted from chart roles
in the same family as the integrity marks — never from a
Expand Down Expand Up @@ -155,6 +159,9 @@
--bad-soft: rgba(185, 28, 28, 0.18);
--unknown: #CBD5E1;
--unknown-soft: rgba(148, 163, 184, 0.18);
--structural-direct: #F8FAFC;
--structural-derivative: #CBD5E1;
--structural-nomarket: #94A3B8;
--grid: #1E293B;
--chart-grid: #334155;
--chart-line: #E2E8F0;
Expand Down Expand Up @@ -196,6 +203,9 @@
--bad-soft: rgba(185, 28, 28, 0.16);
--unknown: #CBD5E1;
--unknown-soft: rgba(148, 163, 184, 0.14);
--structural-direct: #F8FAFC;
--structural-derivative: #CBD5E1;
--structural-nomarket: #94A3B8;
--grid: #1E293B;
--chart-grid: #334155;
--chart-line: #E2E8F0;
Expand Down Expand Up @@ -372,16 +382,16 @@
box-shadow: inset 0 0 0 1px rgba(15, 23, 42, 0.02);
}
.badge::before { font-size: 1rem; line-height: 1; }
.b-direct { color: var(--unknown); background: var(--neutral-soft); }
.b-direct { color: var(--structural-direct); background: var(--neutral-soft); }
.b-direct::before { content: "◆"; }
.b-derivative { color: var(--warn); background: var(--warning-soft); }
.b-derivative { color: var(--structural-derivative); background: var(--neutral-soft); }
.b-derivative::before { content: "↪"; }
.b-nomarket { color: var(--brand-coral); background: rgba(249, 115, 22, 0.12); }
.b-nomarket { color: var(--structural-nomarket); background: var(--neutral-soft); }
.b-nomarket::before { content: "∅"; }
.integrity-copy { min-width: 0; }
.integrity-title { font-weight: 700; margin: 0; }
.integrity-help { color: var(--muted); font-size: .88rem; margin: var(--space-1) 0 0; }
.integrity-dependency { font-family: var(--mono); font-size: .8rem; margin: var(--space-1) 0 0; color: var(--warn); }
.integrity-dependency { font-family: var(--mono); font-size: .8rem; margin: var(--space-1) 0 0; color: var(--structural-derivative); }
.integrity-card .badge[title] { cursor: help; }
@media (max-width: 520px) { .integrity-card { grid-template-columns: 1fr; gap: var(--space-2); } }
.finding { font-size: 1.02rem; margin: var(--space-3) 0 0; }
Expand Down Expand Up @@ -488,15 +498,15 @@
.integrity-cell { white-space: nowrap; }
.integrity-cell .badge { font-size: .65rem; padding: var(--space-1) var(--space-2); border-width: 1px; }
.integrity-cell .badge::before { font-size: .8rem; }
.integrity-cell .integrity-via { font-family: var(--mono); font-size: .72rem; color: var(--warn); margin-left: var(--space-1); }
.integrity-cell .integrity-via { font-family: var(--mono); font-size: .72rem; color: var(--structural-derivative); margin-left: var(--space-1); }
.case strong { color: var(--ink); font-weight: 600; }
.cs-info { font-size: .8rem; color: var(--muted); margin-left: var(--space-1); }
.cs-info .cs-state { font: 600 .72rem/1 var(--mono); letter-spacing: .04em;
padding: var(--space-1) var(--space-2); border-radius: var(--radius-xs); border: 1px solid currentColor;
vertical-align: middle; }
.cs-info .cs-state.cs-direct { color: var(--unknown); background: var(--neutral-soft); }
.cs-info .cs-state.cs-derivative { color: var(--warn); background: var(--warning-soft); }
.cs-info .cs-state.cs-nomarket { color: var(--brand-coral); background: rgba(249, 115, 22, 0.12); }
.cs-info .cs-state.cs-direct { color: var(--structural-direct); background: var(--neutral-soft); }
.cs-info .cs-state.cs-derivative { color: var(--structural-derivative); background: var(--neutral-soft); }
.cs-info .cs-state.cs-nomarket { color: var(--structural-nomarket); background: var(--neutral-soft); }
svg { width: 100%; height: auto; display: block; }
.axis { fill: var(--muted); font: 10px var(--mono); }

Expand Down
81 changes: 81 additions & 0 deletions server/ui_integrity_palette_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
package server

import (
"strings"
"testing"
)

func TestUIIntegrityStatesUseStructuralPalette(t *testing.T) {
css := styleBlock(t)

for _, role := range []string{"direct", "derivative", "nomarket"} {
token := "--structural-" + role
if !strings.Contains(css, token+":") {
t.Errorf("missing structural palette token %q", token)
}
}

for _, scheme := range []struct {
name string
start string
}{
{name: "system dark", start: "@media (prefers-color-scheme: dark)"},
{name: "explicit dark", start: ":root[data-theme=\"dark\"]"},
} {
start := strings.Index(css, scheme.start)
if start < 0 {
t.Fatalf("missing %s palette", scheme.name)
}
end := strings.Index(css[start:], "}")
if end < 0 {
t.Fatalf("unterminated %s palette", scheme.name)
}
block := css[start : start+end]
for _, role := range []string{"direct", "derivative", "nomarket"} {
if !strings.Contains(block, "--structural-"+role+":") {
t.Errorf("%s palette is missing structural %s", scheme.name, role)
}
}
}

for _, role := range []string{"direct", "derivative", "nomarket"} {
token := "--structural-" + role
selector := ".b-" + role + " { color: var(" + token + "); background: var(--neutral-soft); }"
if !strings.Contains(css, selector) {
t.Errorf("integrity badge %q must use its structural token on a neutral background", role)
}
}

for _, selector := range []string{
".b-direct {", ".b-derivative {", ".b-nomarket {",
".cs-info .cs-state.cs-direct {", ".cs-info .cs-state.cs-derivative {",
".cs-info .cs-state.cs-nomarket {", ".integrity-dependency {",
".integrity-cell .integrity-via {",
} {
start := strings.Index(css, selector)
if start < 0 {
t.Fatalf("missing integrity style %q", selector)
}
end := strings.Index(css[start:], "}")
if end < 0 {
t.Fatalf("unterminated integrity style %q", selector)
}
rule := css[start : start+end]
for _, forbidden := range []string{"var(--ok)", "var(--warn)", "var(--bad)", "var(--success)", "var(--warning)", "var(--critical)"} {
if strings.Contains(rule, forbidden) {
t.Errorf("integrity style %q borrows financial severity token %q", selector, forbidden)
}
}
}
for _, selector := range []string{".integrity-dependency {", ".integrity-cell .integrity-via {"} {
start := strings.Index(css, selector)
end := strings.Index(css[start:], "}")
if !strings.Contains(css[start:start+end], "var(--structural-derivative)") {
t.Errorf("derivative detail %q must use the structural palette", selector)
}
}

if !strings.Contains(css, ".m-state {") || !strings.Contains(css, ".m-unknown { color: var(--unknown); }") {
t.Error("undetermined metrics must keep their neutral unknown styling")
}
}
Loading