diff --git a/.gitignore b/.gitignore
index 0bd34b3..8a4699d 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,2 +1,5 @@
.task/
screens/img/
+_output/
+ui-hill-climb/rounds/
+ui-hill-climb/references/*.png
diff --git a/README.md b/README.md
index bd70710..cbfa4a0 100644
--- a/README.md
+++ b/README.md
@@ -15,7 +15,7 @@ through protanopia, deuteranopia or tritanopia, with every pair the palette is h
|---|---|
| `palette.toml` | `dist/`: `tokens.css` (every role as `--spin-*`), `starlight.css` (the website's `--sl-color-*`), `dashboard.css` (shadcn's HSL variables, light on `:root`, dark on `.dark`, and the dashboard's radius, scrim, dense type scale, heading tracking and motion), `tokens.json`, and `perception.js` and `simulate.svg` for the preview |
| spin's `dashboard/e2e/catalog.spec.ts` | `screens/`: every route, tab, dialog and menu of the app in both themes at 1440 and 390 px, and what each draws that this palette does not have - a colour, a face, a size, a radius, contrast, overflow, a target too small (`task screens SCREENS=
`). The catalog and the audit are committed; the images, hundreds a release, are an archive on a release of this repository that `screens.json` names and Pages unpacks. [See them](https://spin-stack.github.io/brand/screens.html) |
-| `assets/mark.png` | `logos/`: the stack of disks, cut from the mark as it was drawn (1024 px on a clear ground): the tile on the brand's navy (512, 256; the touch icon at 180, the favicon at 64 and 32) for a dark ground, and clear (512, 256) for a light one. Each is the drawing reduced; a size it would have to enlarge fails |
+| `assets/mark.png` | `logos/`: the stack of disks, cut from the mark as it was drawn (1024 px on a clear ground): the tile on the brand's navy (512, 256; the touch icon at 180, the favicon at 64 and 32) for a dark ground, clear (512, 256) for a light one, and clear with its shadow drawn for the dark page (`mark-dark-*`, 512, 256), where the light one's grey shadow read as a pale ring. Each is the drawing reduced; a size it would have to enlarge fails |
```
go tool task # palette and mark: check, and write dist/ and logos/
diff --git a/cmd/mark/main.go b/cmd/mark/main.go
index 5c80396..e8d5b62 100644
--- a/cmd/mark/main.go
+++ b/cmd/mark/main.go
@@ -25,20 +25,24 @@ import (
)
// logo is one file logos/ holds: the mark at a size something shows it at, on the navy (a tile)
-// or on nothing (clear).
+// or on nothing (clear); clear and dark is on nothing too, its shadow drawn for the dark page.
type logo struct {
size int
clear bool
+ dark bool
}
var sizes = map[string]logo{
- "mark-512.png": {512, false}, // the link preview, a dark hero
- "mark-256.png": {256, false}, // a dark header
- "mark-clear-512.png": {512, true}, // on any ground: a light page, a light hero
- "mark-clear-256.png": {256, true}, // a light header
- "apple-touch-icon.png": {180, false}, // a phone's home screen
- "favicon-64.png": {64, false}, // a browser's tab, whose ground is the browser's
- "favicon-32.png": {32, false},
+ "mark-512.png": {size: 512, clear: false}, // the link preview, a dark hero
+ "mark-256.png": {size: 256, clear: false}, // a dark header
+ "mark-clear-512.png": {size: 512, clear: true}, // on any ground: a light page, a light hero
+ "mark-clear-256.png": {size: 256, clear: true}, // a light header
+ // On a dark page with no tile: the clear mark's light grey shadow read there as a pale ring.
+ "mark-dark-512.png": {size: 512, clear: true, dark: true}, // a dark hero
+ "mark-dark-256.png": {size: 256, clear: true, dark: true}, // a dark header
+ "apple-touch-icon.png": {size: 180, clear: false}, // a phone's home screen
+ "favicon-64.png": {size: 64, clear: false}, // a browser's tab, whose ground is the browser's
+ "favicon-32.png": {size: 32, clear: false},
}
// margin is the square's side over the stack's longer one: a fifth of it around the stack.
@@ -58,7 +62,7 @@ func run() error {
check := flag.Bool("check", false, "write nothing; fail if logos/ is not what the drawing makes")
flag.Parse()
- navy, err := ground(*palette)
+ navy, page, err := ground(*palette)
if err != nil {
return err
}
@@ -77,11 +81,15 @@ func run() error {
}
onNavy := shadowOn(drawing, stack, navy)
+ onDark := shadowOn(drawing, stack, page)
var stale []string
for name, l := range sizes {
src := onNavy
- if l.clear {
+ switch {
+ case l.dark:
+ src = onDark
+ case l.clear:
src = drawing
}
img, err := cut(src, stack, l.size, l.clear, navy)
@@ -113,19 +121,30 @@ func run() error {
return nil
}
-// ground is the palette's brand navy, the tile's ground and the dark page's.
-func ground(palette string) (color.RGBA, error) {
- var p struct{ Brand map[string]string }
+// ground is the palette's brand navy, the tile's ground, and its dark page, the ground of the
+// clear mark drawn for a dark page.
+func ground(palette string) (navy, page color.RGBA, err error) {
+ var p struct {
+ Brand map[string]string
+ Dark struct{ Page string }
+ }
if _, err := toml.DecodeFile(palette, &p); err != nil {
- return color.RGBA{}, err
+ return navy, page, err
+ }
+ if navy, err = rgbOf(palette, "brand.navy", p.Brand["navy"]); err != nil {
+ return navy, page, err
}
- hex := p.Brand["navy"]
+ page, err = rgbOf(palette, "dark.page", p.Dark.Page)
+ return navy, page, err
+}
+
+func rgbOf(palette, key, hex string) (color.RGBA, error) {
if len(hex) != 7 || hex[0] != '#' {
- return color.RGBA{}, fmt.Errorf("%s: brand.navy %q is no #rrggbb", palette, hex)
+ return color.RGBA{}, fmt.Errorf("%s: %s %q is no #rrggbb", palette, key, hex)
}
v, err := strconv.ParseUint(hex[1:], 16, 32)
if err != nil {
- return color.RGBA{}, fmt.Errorf("%s: brand.navy %q is no #rrggbb", palette, hex)
+ return color.RGBA{}, fmt.Errorf("%s: %s %q is no #rrggbb", palette, key, hex)
}
return color.RGBA{uint8(v >> 16), uint8(v >> 8), uint8(v), 255}, nil
}
diff --git a/cmd/palette/checks.go b/cmd/palette/checks.go
index 207a13f..aa3757f 100644
--- a/cmd/palette/checks.go
+++ b/cmd/palette/checks.go
@@ -102,6 +102,12 @@ func holds(p palette) ([]result, error) {
hold("text", st+" on "+s, c[st], c[s], text)
}
}
+ // A state's badge: its colour as text on its colour as a tint, wherever a badge sits.
+ for _, s := range []string{"page", "surface", "raised"} {
+ for _, st := range []string{"success", "warning", "error", "accent_text"} {
+ hold("text", st+" on its badge over "+s, c[st], c[st].over(c[s], p.Tint.Badge), text)
+ }
+ }
for _, s := range []string{"page", "surface"} {
hold("text", "label on "+s, c["label"], c[s], text)
sel := c["accent"].over(c[s], p.Tint.Selected)
@@ -110,6 +116,13 @@ func holds(p palette) ([]result, error) {
}
hold("text", "on_accent on accent", c["on_accent"], c["accent"], text)
hold("text", "white on destructive", white, c["destructive"], text)
+ st := controlStates(p, c, m.name == "dark")
+ hold("text", "on_accent on accent under the pointer", c["on_accent"], st.accentHover, text)
+ hold("text", "on_accent on accent pressed", c["on_accent"], st.accentActive, text)
+ hold("text", "white on destructive under the pointer", white, st.destructiveHover, text)
+ hold("text", "white on destructive pressed", white, st.destructiveActive, text)
+ hold("text", "text on a secondary button", c["text"], st.control, bodyText)
+ hold("text", "text on a secondary button under the pointer", c["text"], st.controlHover, bodyText)
for _, s := range []string{"page", "raised"} {
hold("non-text", "border_strong on "+s, c["border_strong"], c[s], nonText)
hold("non-text", "accent (the focus ring) on "+s, c["accent"], c[s], nonText)
@@ -124,6 +137,10 @@ func holds(p palette) ([]result, error) {
apart("state", "selected tint over "+s+" | "+s, c["accent"].over(c[s], p.Tint.Selected), c[s], 0)
apart("hover", "hover tint over "+s+" | "+s, c["accent"].over(c[s], p.Tint.Hover), c[s], 0)
}
+ apart("hover", "accent under the pointer | accent", st.accentHover, c["accent"], 0)
+ apart("hover", "destructive under the pointer | destructive", st.destructiveHover, c["destructive"], 0)
+ apart("hover", "secondary under the pointer | secondary", st.controlHover, st.control, 0)
+ apart("edge", "secondary button | page", st.control, c["page"], 0)
for _, pair := range [][2]string{
{"success", "error"}, {"success", "warning"}, {"warning", "error"},
{"accent", "success"}, {"accent", "error"},
diff --git a/cmd/palette/controls.go b/cmd/palette/controls.go
new file mode 100644
index 0000000..0e8f457
--- /dev/null
+++ b/cmd/palette/controls.go
@@ -0,0 +1,29 @@
+package main
+
+// The fills a button takes under the pointer and when pressed, derived from the roles so that a
+// consumer does not invent them (the dashboard wrote primary/90: the accent at 90% over the page,
+// lighter in dark and darker in light, and its white label under 4.5:1 in one of them).
+//
+// A solid fill - the accent, the destructive - darkens: its label is white, and a darker fill
+// keeps it legible in both themes. A quiet fill - a secondary button's - steps one surface up.
+type states struct {
+ accentHover, accentActive rgb
+ destructiveHover, destructiveActive rgb
+ control, controlHover rgb
+}
+
+func controlStates(p palette, c map[string]rgb, isDark bool) states {
+ black := rgb{0, 0, 0}
+ darken := func(fill rgb, n float64) rgb { return black.over(fill, p.Tint.Fill*n) }
+ s := states{
+ accentHover: darken(c["accent"], 1), accentActive: darken(c["accent"], 2),
+ destructiveHover: darken(c["destructive"], 1), destructiveActive: darken(c["destructive"], 2),
+ // A secondary button is a step off the page: raised in dark, the surface in light, where
+ // raised read as a disabled grey. Under the pointer, the next step.
+ control: c["surface"], controlHover: c["raised"],
+ }
+ if isDark {
+ s.control, s.controlHover = c["raised"], c["overlay"]
+ }
+ return s
+}
diff --git a/cmd/palette/main.go b/cmd/palette/main.go
index 94bb555..fcf8c12 100644
--- a/cmd/palette/main.go
+++ b/cmd/palette/main.go
@@ -33,8 +33,10 @@ type palette struct {
Brand map[string]string
Dark mode
Light mode
- Tint struct{ Hover, Selected, Scrim float64 }
- Type struct {
+ Tint struct{ Hover, Selected, Scrim, Fill, Badge float64 }
+ // What lifts a menu or a dialog off what is under it, per theme: a CSS box-shadow.
+ Elevation struct{ Dark, Light string }
+ Type struct {
Sans, Mono string
FallbackSans string `toml:"fallback_sans"`
FallbackMono string `toml:"fallback_mono"`
@@ -44,6 +46,9 @@ type palette struct {
LineHeight float64 `toml:"line_height"`
HeadingWeight int `toml:"heading_weight"`
HeadingTracking float64 `toml:"heading_tracking"`
+ Heading string
+ Fluid map[string][2]int
+ Numeric string
Wordmark string
}
Layout struct {
@@ -58,6 +63,11 @@ type palette struct {
WidthDetail int `toml:"width_detail"`
WidthWide int `toml:"width_wide"`
}
+ // A control's geometry: the heights of a button or an input, the icon in it, and the gap
+ // between an icon and its word.
+ Controls struct {
+ Sm, Md, Lg, Icon, Gap int
+ }
Motion struct {
Fast, Base, Slow, Draw int
Ease string
diff --git a/cmd/palette/outputs.go b/cmd/palette/outputs.go
index 8f4848c..9511aad 100644
--- a/cmd/palette/outputs.go
+++ b/cmd/palette/outputs.go
@@ -22,7 +22,7 @@ func outputs(p palette, results []result) (map[string][]byte, error) {
j, err := json.MarshalIndent(map[string]any{
"brand": p.Brand, "dark": hexes(dark), "light": hexes(light),
- "tint": p.Tint, "type": p.Type, "layout": p.Layout, "motion": p.Motion,
+ "tint": p.Tint, "type": p.Type, "layout": p.Layout, "controls": p.Controls, "elevation": p.Elevation, "motion": p.Motion,
}, "", " ")
if err != nil {
return nil, err
@@ -60,19 +60,32 @@ func sortedKeys(m map[string]rgb) []string {
func tokensCSS(p palette, dark, light map[string]rgb) string {
var b strings.Builder
b.WriteString(header)
- block := func(sel string, c map[string]rgb) {
+ block := func(sel string, c map[string]rgb, shadow string, isDark bool) {
fmt.Fprintf(&b, "%s {\n", sel)
for _, k := range sortedKeys(c) {
fmt.Fprintf(&b, "\t--spin-%s: %s;\n", strings.ReplaceAll(k, "_", "-"), c[k].hex())
}
+ st := controlStates(p, c, isDark)
+ for _, v := range [][2]string{
+ {"accent-fill-hover", st.accentHover.hex()}, {"accent-fill-active", st.accentActive.hex()},
+ {"destructive-hover", st.destructiveHover.hex()}, {"destructive-active", st.destructiveActive.hex()},
+ {"control", st.control.hex()}, {"control-hover", st.controlHover.hex()},
+ } {
+ fmt.Fprintf(&b, "\t--spin-%s: %s;\n", v[0], v[1])
+ }
+ fmt.Fprintf(&b, "\t--spin-shadow-overlay: %s;\n", shadow)
+ // A state's badge: its colour at tint.badge over whatever is under it.
+ for _, k := range []string{"success", "warning", "error", "accent-text"} {
+ fmt.Fprintf(&b, "\t--spin-%s-badge: color-mix(in srgb, var(--spin-%s) %g%%, transparent);\n", k, k, p.Tint.Badge*100)
+ }
fmt.Fprintf(&b, "\t--spin-accent-hover: %s;\n", c["accent"].over(c["page"], p.Tint.Hover).hex())
fmt.Fprintf(&b, "\t--spin-accent-selected: %s;\n", c["accent"].over(c["page"], p.Tint.Selected).hex())
b.WriteString("}\n")
}
// .spin-dark is a part that is dark in either theme - a terminal, a drawing on the navy - and
// takes the dark roles in place of the page's.
- block(":root, .spin-dark", dark)
- block(":root[data-theme='light']", light)
+ block(":root, .spin-dark", dark, p.Elevation.Dark, true)
+ block(":root[data-theme='light']", light, p.Elevation.Light, false)
b.WriteString(":root {\n")
names := make([]string, 0, len(p.Brand))
for k := range p.Brand {
@@ -88,9 +101,16 @@ func tokensCSS(p palette, dark, light map[string]rgb) string {
fmt.Fprintf(&b, "\t--spin-text-reading: %dpx;\n\t--spin-text-dense: %dpx;\n", p.Type.ReadingSize, p.Type.DenseSize)
fmt.Fprintf(&b, "\t--spin-line-height: %g;\n", p.Type.LineHeight)
fmt.Fprintf(&b, "\t--spin-heading-weight: %d;\n\t--spin-heading-tracking: %gem;\n", p.Type.HeadingWeight, p.Type.HeadingTracking)
+ fmt.Fprintf(&b, "\t--spin-font-heading: var(--spin-font-%s);\n", p.Type.Heading)
+ fmt.Fprintf(&b, "\t--spin-numeric: %s;\n", p.Type.Numeric)
+ for _, k := range []string{"display", "h1", "h2", "h3"} {
+ fmt.Fprintf(&b, "\t--spin-text-%s: %s;\n", k, fluid(p, k))
+ }
fmt.Fprintf(&b, "\t--spin-measure: %dch;\n\t--spin-sidebar: %drem;\n", p.Layout.Measure, p.Layout.Sidebar)
fmt.Fprintf(&b, "\t--spin-radius-sm: %dpx;\n\t--spin-radius: %dpx;\n\t--spin-radius-lg: %dpx;\n", p.Layout.RadiusSm, p.Layout.Radius, p.Layout.RadiusLg)
fmt.Fprintf(&b, "\t--spin-target: %dpx;\n\t--spin-target-touch: %dpx;\n", p.Layout.Target, p.Layout.TargetTouch)
+ fmt.Fprintf(&b, "\t--spin-control-sm: %dpx;\n\t--spin-control-md: %dpx;\n\t--spin-control-lg: %dpx;\n", p.Controls.Sm, p.Controls.Md, p.Controls.Lg)
+ fmt.Fprintf(&b, "\t--spin-control-icon: %dpx;\n\t--spin-control-gap: %dpx;\n", p.Controls.Icon, p.Controls.Gap)
fmt.Fprintf(&b, "\t--spin-width-contained: %dpx;\n\t--spin-width-detail: %dpx;\n\t--spin-width-wide: %dpx;\n", p.Layout.WidthContained, p.Layout.WidthDetail, p.Layout.WidthWide)
fmt.Fprintf(&b, "\t--spin-duration-fast: %dms;\n\t--spin-duration: %dms;\n", p.Motion.Fast, p.Motion.Base)
fmt.Fprintf(&b, "\t--spin-duration-slow: %dms;\n\t--spin-duration-draw: %dms;\n", p.Motion.Slow, p.Motion.Draw)
@@ -148,6 +168,13 @@ func starlightCSS(p palette, dark, light map[string]rgb) string {
block(":root[data-theme='light']", light, false)
fmt.Fprintf(&b, ":root {\n\t--sl-font: '%s', %s;\n\t--sl-font-mono: '%s', %s;\n}\n",
p.Type.Sans, p.Type.FallbackSans, p.Type.Mono, p.Type.FallbackMono)
+ // The headings' sizes, fluid from a phone's width to a desktop's: Starlight's own scale is a
+ // step for each breakpoint, and between them a heading jumped.
+ b.WriteString(":root {\n")
+ for _, h := range [][2]string{{"h1", "display"}, {"h2", "h1"}, {"h3", "h2"}, {"h4", "h3"}} {
+ fmt.Fprintf(&b, "\t--sl-text-%s: %s;\n", h[0], fluid(p, h[1]))
+ }
+ b.WriteString("}\n")
return b.String()
}
@@ -174,21 +201,36 @@ func dashboardCSS(p palette, dark, light map[string]rgb) string {
return rgb{1, 1, 1}
}
// Hover and the navigation's selection are neutral: the accent is the primary action's
- // and the focus ring's alone.
- neutral := c["surface"]
+ // and the focus ring's alone. Raised in both themes: light's surface was a step off white
+ // that a selected item in the sidebar did not show.
+ neutral := c["raised"]
+ st := controlStates(p, c, isDark)
+ // A step off the page - a code block, a secondary button's fill: raised in dark; in light
+ // the surface, since raised there read as a grey slab and a button on it as disabled.
+ step := c["surface"]
if isDark {
- neutral = c["raised"]
+ step = c["raised"]
}
vars := [][2]string{
{"background", hsl(c["page"])}, {"foreground", hsl(c["text"])},
{"card", hsl(card)}, {"card-foreground", hsl(c["text"])},
- {"muted", hsl(c["raised"])}, {"muted-foreground", hsl(c["muted"])},
+ // A menu's or a dialog's plane: overlay, the lightest step in dark, so that it is lifted
+ // by its own lightness rather than by a shadow; white in light.
+ {"popover", hsl(c["overlay"])}, {"popover-foreground", hsl(c["text"])},
+ {"muted", hsl(step)}, {"muted-foreground", hsl(c["muted"])},
{"border", hsl(c["border"])}, {"input", hsl(c["border_strong"])}, {"ring", hsl(c["accent"])},
{"primary", hsl(c["accent"])}, {"primary-foreground", hsl(c["on_accent"])},
- {"secondary", hsl(c["raised"])}, {"secondary-foreground", hsl(c["text"])},
+ {"primary-hover", hsl(st.accentHover)}, {"primary-active", hsl(st.accentActive)},
+ {"destructive-hover", hsl(st.destructiveHover)}, {"destructive-active", hsl(st.destructiveActive)},
+ {"secondary-hover", hsl(st.controlHover)},
+ {"secondary", hsl(st.control)}, {"secondary-foreground", hsl(c["text"])},
{"accent", hsl(neutral)}, {"accent-foreground", hsl(c["text"])},
{"destructive", hsl(c["destructive"])}, {"destructive-foreground", hsl(on(c["destructive"]))},
{"error", hsl(c["error"])}, {"label", hsl(c["label"])},
+ // A badge's fill: the state at tint.badge over the card it sits on.
+ {"success-subtle", hsl(c["success"].over(card, p.Tint.Badge))},
+ {"warning-subtle", hsl(c["warning"].over(card, p.Tint.Badge))},
+ {"error-subtle", hsl(c["error"].over(card, p.Tint.Badge))},
{"success", hsl(c["success"])}, {"success-foreground", hsl(on(c["success"]))},
{"warning", hsl(c["warning"])}, {"warning-foreground", hsl(on(c["warning"]))},
}
@@ -196,6 +238,11 @@ func dashboardCSS(p palette, dark, light map[string]rgb) string {
for _, v := range vars {
fmt.Fprintf(&b, "\t--%s: %s;\n", v[0], v[1])
}
+ shadow := p.Elevation.Light
+ if isDark {
+ shadow = p.Elevation.Dark
+ }
+ fmt.Fprintf(&b, "\t--shadow-overlay: %s;\n", shadow)
b.WriteString("}\n")
}
block(":root", light, false)
@@ -206,15 +253,15 @@ func dashboardCSS(p palette, dark, light map[string]rgb) string {
// Its theme (dashboard/src/styles/globals.css) maps Tailwind's names onto these.
b.WriteString(":root {\n")
fmt.Fprintf(&b, "\t--radius: %gpx;\n", float64(p.Layout.RadiusSm))
+ fmt.Fprintf(&b, "\t--spin-control-sm: %dpx;\n\t--spin-control-md: %dpx;\n\t--spin-control-lg: %dpx;\n", p.Controls.Sm, p.Controls.Md, p.Controls.Lg)
+ fmt.Fprintf(&b, "\t--spin-control-icon: %dpx;\n\t--spin-control-gap: %dpx;\n", p.Controls.Icon, p.Controls.Gap)
// How wide a page may grow, by what it shows (the dashboard's page-width.ts reads these).
fmt.Fprintf(&b, "\t--spin-width-contained: %dpx;\n\t--spin-width-detail: %dpx;\n\t--spin-width-wide: %dpx;\n", p.Layout.WidthContained, p.Layout.WidthDetail, p.Layout.WidthWide)
- // The dim behind a dialog: the mark's navy, in either theme, at the palette's scrim.
- navy, err := parseHex(p.Brand["navy"])
- if err != nil {
- panic("palette.toml's brand.navy: " + err.Error())
- }
+ // The dim behind a dialog: the dark page, in either theme, at the palette's scrim - a neutral
+ // dim, not the mark's navy, which tinted everything behind a dialog blue.
+ dim := dark["page"]
b255 := func(v float64) int { return int(math.Round(v * 255)) }
- fmt.Fprintf(&b, "\t--scrim: rgb(%d %d %d / %g);\n", b255(navy.r), b255(navy.g), b255(navy.b), p.Tint.Scrim)
+ fmt.Fprintf(&b, "\t--scrim: rgb(%d %d %d / %g);\n", b255(dim.r), b255(dim.g), b255(dim.b), p.Tint.Scrim)
// The word the sidebar writes beside the mark: the app's catalog audit holds its text to this.
fmt.Fprintf(&b, "\t--spin-wordmark: %q;\n", p.Type.Wordmark)
fmt.Fprintf(&b, "\t--spin-font-sans: '%s', %s;\n\t--spin-font-mono: '%s', %s;\n", p.Type.Sans, p.Type.FallbackSans, p.Type.Mono, p.Type.FallbackMono)
@@ -226,6 +273,7 @@ func dashboardCSS(p palette, dark, light map[string]rgb) string {
fmt.Fprintf(&b, "\t--spin-text-%s: %dpx;\n\t--spin-text-%s-lh: %dpx;\n", k, step[0], k, step[1])
}
fmt.Fprintf(&b, "\t--spin-heading-tracking: %gem;\n", p.Type.HeadingTracking)
+ fmt.Fprintf(&b, "\t--spin-numeric: %s;\n", p.Type.Numeric)
fmt.Fprintf(&b, "\t--spin-duration-fast: %dms;\n\t--spin-duration: %dms;\n", p.Motion.Fast, p.Motion.Base)
fmt.Fprintf(&b, "\t--spin-duration-slow: %dms;\n\t--spin-ease: %s;\n", p.Motion.Slow, p.Motion.Ease)
b.WriteString("}\n")
@@ -299,3 +347,16 @@ func simulateSVG() string {
b.WriteString("\n")
return b.String()
}
+
+// fluid is a size that grows with the viewport from the phone's width to the desktop's, as a
+// clamp() in rem, so that a reader's zoom still scales it: [min, max] in px at those widths.
+func fluid(p palette, name string) string {
+ s, ok := p.Type.Fluid[name]
+ if !ok {
+ panic("palette.toml's type.fluid has no " + name)
+ }
+ lo, hi := float64(p.Layout.Phone), float64(p.Layout.Desktop)
+ slope := float64(s[1]-s[0]) / (hi - lo)
+ base := float64(s[0]) - slope*lo
+ return fmt.Sprintf("clamp(%.4grem, %.4grem + %.4gvw, %.4grem)", float64(s[0])/16, base/16, slope*100, float64(s[1])/16)
+}
diff --git a/dist/dashboard.css b/dist/dashboard.css
index 65c2b95..3dd2700 100644
--- a/dist/dashboard.css
+++ b/dist/dashboard.css
@@ -4,57 +4,84 @@
--foreground: 220.9 39.3% 11.0%;
--card: 0.0 0.0% 100.0%;
--card-foreground: 220.9 39.3% 11.0%;
- --muted: 214.7 40.4% 90.8%;
- --muted-foreground: 218.5 20.9% 36.7%;
- --border: 214.8 28.4% 78.6%;
- --input: 217.5 16.0% 51.0%;
+ --popover: 0.0 0.0% 100.0%;
+ --popover-foreground: 220.9 39.3% 11.0%;
+ --muted: 217.5 40.0% 96.1%;
+ --muted-foreground: 219.4 16.3% 38.4%;
+ --border: 216.0 20.4% 80.8%;
+ --input: 221.1 14.8% 53.5%;
--ring: 200.4 88.4% 33.9%;
--primary: 200.4 88.4% 33.9%;
--primary-foreground: 0.0 0.0% 100.0%;
- --secondary: 214.7 40.4% 90.8%;
+ --primary-hover: 200.4 88.4% 29.9%;
+ --primary-active: 200.4 88.4% 25.8%;
+ --destructive-hover: 353.8 60.0% 41.4%;
+ --destructive-active: 353.8 60.0% 35.8%;
+ --secondary-hover: 216.0 33.3% 91.2%;
+ --secondary: 217.5 40.0% 96.1%;
--secondary-foreground: 220.9 39.3% 11.0%;
- --accent: 213.3 42.9% 95.9%;
+ --accent: 216.0 33.3% 91.2%;
--accent-foreground: 220.9 39.3% 11.0%;
- --destructive: 343.9 67.4% 42.2%;
+ --destructive: 353.8 60.0% 47.1%;
--destructive-foreground: 0.0 0.0% 100.0%;
- --error: 318.8 70.3% 35.7%;
- --label: 216.0 83.3% 28.2%;
- --success: 149.3 57.7% 27.8%;
+ --error: 328.3 66.5% 36.3%;
+ --label: 202.4 36.6% 31.6%;
+ --success-subtle: 155.7 22.8% 90.8%;
+ --warning-subtle: 36.4 34.9% 91.1%;
+ --error-subtle: 328.3 37.8% 92.4%;
+ --success: 155.7 74.8% 23.3%;
--success-foreground: 0.0 0.0% 100.0%;
- --warning: 39.1 100.0% 27.1%;
+ --warning: 36.4 100.0% 25.9%;
--warning-foreground: 0.0 0.0% 100.0%;
+ --shadow-overlay: 0 12px 32px rgb(16 19 24 / 0.14), 0 2px 6px rgb(16 19 24 / 0.08);
}
.dark {
- --background: 220.9 39.3% 11.0%;
- --foreground: 213.8 47.1% 93.3%;
- --card: 222.0 39.5% 14.9%;
- --card-foreground: 213.8 47.1% 93.3%;
- --muted: 221.5 37.9% 20.2%;
- --muted-foreground: 217.5 24.7% 68.2%;
- --border: 218.2 30.1% 28.6%;
- --input: 217.8 22.1% 52.2%;
- --ring: 192.7 80.8% 53.1%;
- --primary: 192.7 80.8% 53.1%;
- --primary-foreground: 220.9 39.3% 11.0%;
- --secondary: 221.5 37.9% 20.2%;
- --secondary-foreground: 213.8 47.1% 93.3%;
- --accent: 221.5 37.9% 20.2%;
- --accent-foreground: 213.8 47.1% 93.3%;
- --destructive: 347.3 60.3% 47.5%;
+ --background: 220.0 37.5% 6.3%;
+ --foreground: 215.0 31.6% 92.5%;
+ --card: 220.0 34.6% 10.2%;
+ --card-foreground: 215.0 31.6% 92.5%;
+ --popover: 218.6 28.0% 19.6%;
+ --popover-foreground: 215.0 31.6% 92.5%;
+ --muted: 218.2 30.6% 14.1%;
+ --muted-foreground: 218.0 17.4% 66.3%;
+ --border: 218.6 23.7% 23.1%;
+ --input: 219.4 14.5% 47.3%;
+ --ring: 201.6 73.9% 40.6%;
+ --primary: 201.6 73.9% 40.6%;
+ --primary-foreground: 0.0 0.0% 100.0%;
+ --primary-hover: 201.6 73.9% 35.7%;
+ --primary-active: 201.6 73.9% 30.8%;
+ --destructive-hover: 350.1 55.6% 43.5%;
+ --destructive-active: 350.1 55.6% 37.6%;
+ --secondary-hover: 218.6 28.0% 19.6%;
+ --secondary: 218.2 30.6% 14.1%;
+ --secondary-foreground: 215.0 31.6% 92.5%;
+ --accent: 218.2 30.6% 14.1%;
+ --accent-foreground: 215.0 31.6% 92.5%;
+ --destructive: 350.1 55.6% 49.4%;
--destructive-foreground: 0.0 0.0% 100.0%;
- --error: 331.7 86.0% 72.0%;
- --label: 174.1 70.2% 63.1%;
- --success: 154.0 69.4% 55.1%;
- --success-foreground: 220.9 39.3% 11.0%;
- --warning: 40.4 100.0% 67.1%;
- --warning-foreground: 220.9 39.3% 11.0%;
+ --error: 332.3 77.2% 70.8%;
+ --label: 200.7 32.5% 66.9%;
+ --success-subtle: 189.6 32.6% 14.0%;
+ --warning-subtle: 39.0 2.9% 16.1%;
+ --error-subtle: 270.2 19.5% 17.0%;
+ --success: 151.3 49.8% 53.1%;
+ --success-foreground: 220.0 37.5% 6.3%;
+ --warning: 39.9 73.1% 59.2%;
+ --warning-foreground: 220.0 37.5% 6.3%;
+ --shadow-overlay: 0 0 0 1px rgb(0 0 0 / 0.4);
}
:root {
- --radius: 4px;
+ --radius: 6px;
+ --spin-control-sm: 28px;
+ --spin-control-md: 32px;
+ --spin-control-lg: 36px;
+ --spin-control-icon: 16px;
+ --spin-control-gap: 6px;
--spin-width-contained: 1200px;
--spin-width-detail: 1152px;
--spin-width-wide: 1600px;
- --scrim: rgb(17 24 39 / 0.6);
+ --scrim: rgb(10 14 22 / 0.64);
--spin-wordmark: "Spinbox";
--spin-font-sans: 'IBM Plex Sans', system-ui, -apple-system, 'Segoe UI', sans-serif;
--spin-font-mono: 'IBM Plex Mono', ui-monospace, 'SF Mono', Menlo, monospace;
@@ -73,6 +100,7 @@
--spin-text-3xl: 24px;
--spin-text-3xl-lh: 32px;
--spin-heading-tracking: -0.02em;
+ --spin-numeric: tabular-nums;
--spin-duration-fast: 120ms;
--spin-duration: 200ms;
--spin-duration-slow: 400ms;
diff --git a/dist/perception.js b/dist/perception.js
index 693f1d6..1150236 100644
--- a/dist/perception.js
+++ b/dist/perception.js
@@ -1,2 +1,2 @@
// Written by `task palette`: every pair palette.toml is held to, and how it held.
-export const perception = [{"Mode":"dark","Kind":"text","What":"text on page","Contrast":15.04,"Floor":7,"Worst":0,"OK":true},{"Mode":"dark","Kind":"text","What":"muted on page","Contrast":7.46,"Floor":4.5,"Worst":0,"OK":true},{"Mode":"dark","Kind":"text","What":"accent_text on page","Contrast":8.2,"Floor":4.5,"Worst":0,"OK":true},{"Mode":"dark","Kind":"text","What":"success on page","Contrast":10.04,"Floor":4.5,"Worst":0,"OK":true},{"Mode":"dark","Kind":"text","What":"warning on page","Contrast":11.53,"Floor":4.5,"Worst":0,"OK":true},{"Mode":"dark","Kind":"text","What":"error on page","Contrast":7.03,"Floor":4.5,"Worst":0,"OK":true},{"Mode":"dark","Kind":"text","What":"text on surface","Contrast":13.76,"Floor":7,"Worst":0,"OK":true},{"Mode":"dark","Kind":"text","What":"muted on surface","Contrast":6.82,"Floor":4.5,"Worst":0,"OK":true},{"Mode":"dark","Kind":"text","What":"accent_text on surface","Contrast":7.5,"Floor":4.5,"Worst":0,"OK":true},{"Mode":"dark","Kind":"text","What":"success on surface","Contrast":9.18,"Floor":4.5,"Worst":0,"OK":true},{"Mode":"dark","Kind":"text","What":"warning on surface","Contrast":10.55,"Floor":4.5,"Worst":0,"OK":true},{"Mode":"dark","Kind":"text","What":"error on surface","Contrast":6.43,"Floor":4.5,"Worst":0,"OK":true},{"Mode":"dark","Kind":"text","What":"text on raised","Contrast":11.77,"Floor":7,"Worst":0,"OK":true},{"Mode":"dark","Kind":"text","What":"muted on raised","Contrast":5.84,"Floor":4.5,"Worst":0,"OK":true},{"Mode":"dark","Kind":"text","What":"accent_text on raised","Contrast":6.42,"Floor":4.5,"Worst":0,"OK":true},{"Mode":"dark","Kind":"text","What":"success on raised","Contrast":7.86,"Floor":4.5,"Worst":0,"OK":true},{"Mode":"dark","Kind":"text","What":"warning on raised","Contrast":9.02,"Floor":4.5,"Worst":0,"OK":true},{"Mode":"dark","Kind":"text","What":"error on raised","Contrast":5.5,"Floor":4.5,"Worst":0,"OK":true},{"Mode":"dark","Kind":"text","What":"text on overlay","Contrast":10.37,"Floor":7,"Worst":0,"OK":true},{"Mode":"dark","Kind":"text","What":"muted on overlay","Contrast":5.14,"Floor":4.5,"Worst":0,"OK":true},{"Mode":"dark","Kind":"text","What":"accent_text on overlay","Contrast":5.65,"Floor":4.5,"Worst":0,"OK":true},{"Mode":"dark","Kind":"text","What":"success on overlay","Contrast":6.92,"Floor":4.5,"Worst":0,"OK":true},{"Mode":"dark","Kind":"text","What":"warning on overlay","Contrast":7.95,"Floor":4.5,"Worst":0,"OK":true},{"Mode":"dark","Kind":"text","What":"error on overlay","Contrast":4.85,"Floor":4.5,"Worst":0,"OK":true},{"Mode":"dark","Kind":"text","What":"label on page","Contrast":11.36,"Floor":4.5,"Worst":0,"OK":true},{"Mode":"dark","Kind":"text","What":"accent_text on the selected tint over page","Contrast":5.34,"Floor":4.5,"Worst":0,"OK":true},{"Mode":"dark","Kind":"text","What":"text on the selected tint over page","Contrast":9.81,"Floor":4.5,"Worst":0,"OK":true},{"Mode":"dark","Kind":"text","What":"label on surface","Contrast":10.39,"Floor":4.5,"Worst":0,"OK":true},{"Mode":"dark","Kind":"text","What":"accent_text on the selected tint over surface","Contrast":4.84,"Floor":4.5,"Worst":0,"OK":true},{"Mode":"dark","Kind":"text","What":"text on the selected tint over surface","Contrast":8.89,"Floor":4.5,"Worst":0,"OK":true},{"Mode":"dark","Kind":"text","What":"on_accent on accent","Contrast":8.2,"Floor":4.5,"Worst":0,"OK":true},{"Mode":"dark","Kind":"text","What":"white on destructive","Contrast":5.48,"Floor":4.5,"Worst":0,"OK":true},{"Mode":"dark","Kind":"non-text","What":"border_strong on page","Contrast":4.31,"Floor":3,"Worst":0,"OK":true},{"Mode":"dark","Kind":"non-text","What":"accent (the focus ring) on page","Contrast":8.2,"Floor":3,"Worst":0,"OK":true},{"Mode":"dark","Kind":"non-text","What":"border_strong on raised","Contrast":3.37,"Floor":3,"Worst":0,"OK":true},{"Mode":"dark","Kind":"non-text","What":"accent (the focus ring) on raised","Contrast":6.42,"Floor":3,"Worst":0,"OK":true},{"Mode":"dark","Kind":"edge","What":"page | surface","Distance":{"deutan":3.5,"deutan+ips":3.5,"dim":3.5,"drawn":3.6,"ips":3.6,"protan":3.6,"protan+ips":3.5,"tritan":3.4},"Floor":3,"Worst":2,"OK":true},{"Mode":"dark","Kind":"edge","What":"surface | raised","Distance":{"deutan":4.2,"deutan+ips":4.1,"dim":4,"drawn":4.3,"ips":4.2,"protan":4.4,"protan+ips":4.2,"tritan":4.4},"Floor":3,"Worst":2,"OK":true},{"Mode":"dark","Kind":"edge","What":"page | raised","Distance":{"deutan":7.7,"deutan+ips":7.5,"dim":7.3,"drawn":7.8,"ips":7.6,"protan":7.9,"protan+ips":7.7,"tritan":7.7},"Floor":3,"Worst":2,"OK":true},{"Mode":"dark","Kind":"line","What":"border on page","Distance":{"deutan":14,"deutan+ips":13.3,"dim":12.8,"drawn":14.3,"ips":13.6,"protan":14.6,"protan+ips":13.9,"tritan":14.7},"Floor":6,"Worst":4,"OK":true},{"Mode":"dark","Kind":"line","What":"border on raised","Distance":{"deutan":7.1,"deutan+ips":6.7,"dim":6.7,"drawn":7.5,"ips":7.1,"protan":7.3,"protan+ips":7,"tritan":7.3},"Floor":6,"Worst":4,"OK":true},{"Mode":"dark","Kind":"state","What":"selected tint over page | page","Distance":{"deutan":10.6,"deutan+ips":10,"dim":12.3,"drawn":14.4,"ips":13.4,"protan":11.4,"protan+ips":10.7,"tritan":14.2},"Floor":10,"Worst":6,"OK":true},{"Mode":"dark","Kind":"hover","What":"hover tint over page | page","Distance":{"deutan":4.8,"deutan+ips":4.5,"dim":5.8,"drawn":7.3,"ips":6.5,"protan":5.1,"protan+ips":4.7,"tritan":7.9},"Floor":3,"Worst":2,"OK":true},{"Mode":"dark","Kind":"state","What":"selected tint over surface | surface","Distance":{"deutan":9.8,"deutan+ips":9.4,"dim":12.4,"drawn":14.1,"ips":13.3,"protan":10.8,"protan+ips":10.2,"tritan":12.9},"Floor":10,"Worst":6,"OK":true},{"Mode":"dark","Kind":"hover","What":"hover tint over surface | surface","Distance":{"deutan":4.4,"deutan+ips":4.2,"dim":5.9,"drawn":7,"ips":6.5,"protan":4.8,"protan+ips":4.5,"tritan":6.9},"Floor":3,"Worst":2,"OK":true},{"Mode":"dark","Kind":"meaning","What":"success | error","Distance":{"deutan":17.3,"deutan+ips":17.2,"dim":75.7,"drawn":76.5,"ips":76.2,"protan":36.9,"protan+ips":36.7,"tritan":67.8},"Floor":20,"Worst":10,"OK":true},{"Mode":"dark","Kind":"meaning","What":"success | warning","Distance":{"deutan":16.8,"deutan+ips":16.7,"dim":37.4,"drawn":37.7,"ips":37.6,"protan":10.8,"protan+ips":10.8,"tritan":50.1},"Floor":20,"Worst":10,"OK":true},{"Mode":"dark","Kind":"meaning","What":"warning | error","Distance":{"deutan":31.8,"deutan+ips":31.6,"dim":51.5,"drawn":52.2,"ips":52,"protan":44.9,"protan+ips":44.7,"tritan":16.1},"Floor":20,"Worst":10,"OK":true},{"Mode":"dark","Kind":"meaning","What":"accent | success","Distance":{"deutan":36.1,"deutan+ips":35.9,"dim":34.7,"drawn":35,"ips":34.9,"protan":38,"protan+ips":37.8,"tritan":6.8},"Floor":20,"Worst":10,"OK":true},{"Mode":"dark","Kind":"meaning","What":"accent | error","Distance":{"deutan":17.1,"deutan+ips":17.1,"dim":59.6,"drawn":60.4,"ips":60.1,"protan":11.4,"protan+ips":11.3,"tritan":72.3},"Floor":20,"Worst":10,"OK":true},{"Mode":"light","Kind":"text","What":"text on page","Contrast":17.74,"Floor":7,"Worst":0,"OK":true},{"Mode":"light","Kind":"text","What":"muted on page","Contrast":7.18,"Floor":4.5,"Worst":0,"OK":true},{"Mode":"light","Kind":"text","What":"accent_text on page","Contrast":7.02,"Floor":4.5,"Worst":0,"OK":true},{"Mode":"light","Kind":"text","What":"success on page","Contrast":6.07,"Floor":4.5,"Worst":0,"OK":true},{"Mode":"light","Kind":"text","What":"warning on page","Contrast":5.93,"Floor":4.5,"Worst":0,"OK":true},{"Mode":"light","Kind":"text","What":"error on page","Contrast":7.51,"Floor":4.5,"Worst":0,"OK":true},{"Mode":"light","Kind":"text","What":"text on surface","Contrast":16.06,"Floor":7,"Worst":0,"OK":true},{"Mode":"light","Kind":"text","What":"muted on surface","Contrast":6.5,"Floor":4.5,"Worst":0,"OK":true},{"Mode":"light","Kind":"text","What":"accent_text on surface","Contrast":6.36,"Floor":4.5,"Worst":0,"OK":true},{"Mode":"light","Kind":"text","What":"success on surface","Contrast":5.49,"Floor":4.5,"Worst":0,"OK":true},{"Mode":"light","Kind":"text","What":"warning on surface","Contrast":5.37,"Floor":4.5,"Worst":0,"OK":true},{"Mode":"light","Kind":"text","What":"error on surface","Contrast":6.8,"Floor":4.5,"Worst":0,"OK":true},{"Mode":"light","Kind":"text","What":"text on raised","Contrast":14.1,"Floor":7,"Worst":0,"OK":true},{"Mode":"light","Kind":"text","What":"muted on raised","Contrast":5.71,"Floor":4.5,"Worst":0,"OK":true},{"Mode":"light","Kind":"text","What":"accent_text on raised","Contrast":5.58,"Floor":4.5,"Worst":0,"OK":true},{"Mode":"light","Kind":"text","What":"success on raised","Contrast":4.82,"Floor":4.5,"Worst":0,"OK":true},{"Mode":"light","Kind":"text","What":"warning on raised","Contrast":4.71,"Floor":4.5,"Worst":0,"OK":true},{"Mode":"light","Kind":"text","What":"error on raised","Contrast":5.97,"Floor":4.5,"Worst":0,"OK":true},{"Mode":"light","Kind":"text","What":"text on overlay","Contrast":17.74,"Floor":7,"Worst":0,"OK":true},{"Mode":"light","Kind":"text","What":"muted on overlay","Contrast":7.18,"Floor":4.5,"Worst":0,"OK":true},{"Mode":"light","Kind":"text","What":"accent_text on overlay","Contrast":7.02,"Floor":4.5,"Worst":0,"OK":true},{"Mode":"light","Kind":"text","What":"success on overlay","Contrast":6.07,"Floor":4.5,"Worst":0,"OK":true},{"Mode":"light","Kind":"text","What":"warning on overlay","Contrast":5.93,"Floor":4.5,"Worst":0,"OK":true},{"Mode":"light","Kind":"text","What":"error on overlay","Contrast":7.51,"Floor":4.5,"Worst":0,"OK":true},{"Mode":"light","Kind":"text","What":"label on page","Contrast":10.53,"Floor":4.5,"Worst":0,"OK":true},{"Mode":"light","Kind":"text","What":"accent_text on the selected tint over page","Contrast":5.11,"Floor":4.5,"Worst":0,"OK":true},{"Mode":"light","Kind":"text","What":"text on the selected tint over page","Contrast":12.9,"Floor":4.5,"Worst":0,"OK":true},{"Mode":"light","Kind":"text","What":"label on surface","Contrast":9.53,"Floor":4.5,"Worst":0,"OK":true},{"Mode":"light","Kind":"text","What":"accent_text on the selected tint over surface","Contrast":4.69,"Floor":4.5,"Worst":0,"OK":true},{"Mode":"light","Kind":"text","What":"text on the selected tint over surface","Contrast":11.84,"Floor":4.5,"Worst":0,"OK":true},{"Mode":"light","Kind":"text","What":"on_accent on accent","Contrast":5.5,"Floor":4.5,"Worst":0,"OK":true},{"Mode":"light","Kind":"text","What":"white on destructive","Contrast":6.4,"Floor":4.5,"Worst":0,"OK":true},{"Mode":"light","Kind":"non-text","What":"border_strong on page","Contrast":4.17,"Floor":3,"Worst":0,"OK":true},{"Mode":"light","Kind":"non-text","What":"accent (the focus ring) on page","Contrast":5.5,"Floor":3,"Worst":0,"OK":true},{"Mode":"light","Kind":"non-text","What":"border_strong on raised","Contrast":3.31,"Floor":3,"Worst":0,"OK":true},{"Mode":"light","Kind":"non-text","What":"accent (the focus ring) on raised","Contrast":4.38,"Floor":3,"Worst":0,"OK":true},{"Mode":"light","Kind":"edge","What":"page | surface","Distance":{"deutan":3.7,"deutan+ips":3.7,"dim":3.6,"drawn":3.6,"ips":3.6,"protan":3.4,"protan+ips":3.4,"tritan":4},"Floor":3,"Worst":2,"OK":true},{"Mode":"light","Kind":"edge","What":"surface | raised","Distance":{"deutan":4.3,"deutan+ips":4.3,"dim":4.1,"drawn":4.2,"ips":4.1,"protan":4,"protan+ips":4,"tritan":4.3},"Floor":3,"Worst":2,"OK":true},{"Mode":"light","Kind":"edge","What":"page | raised","Distance":{"deutan":8,"deutan+ips":8,"dim":7.6,"drawn":7.7,"ips":7.7,"protan":7.4,"protan+ips":7.4,"tritan":8.2},"Floor":3,"Worst":2,"OK":true},{"Mode":"light","Kind":"line","What":"border on page","Distance":{"deutan":15.9,"deutan+ips":15.9,"dim":15.3,"drawn":15.5,"ips":15.4,"protan":15,"protan+ips":14.9,"tritan":16},"Floor":6,"Worst":4,"OK":true},{"Mode":"light","Kind":"line","What":"border on raised","Distance":{"deutan":8.4,"deutan+ips":8.3,"dim":8.1,"drawn":8.2,"ips":8.1,"protan":8,"protan+ips":7.9,"tritan":8.3},"Floor":6,"Worst":4,"OK":true},{"Mode":"light","Kind":"state","What":"selected tint over page | page","Distance":{"deutan":10.8,"deutan+ips":10.8,"dim":11.5,"drawn":11.6,"ips":11.5,"protan":9.3,"protan+ips":9.2,"tritan":14.4},"Floor":10,"Worst":6,"OK":true},{"Mode":"light","Kind":"hover","What":"hover tint over page | page","Distance":{"deutan":5.1,"deutan+ips":5.1,"dim":5.6,"drawn":5.6,"ips":5.6,"protan":4.3,"protan+ips":4.3,"tritan":7.4},"Floor":3,"Worst":2,"OK":true},{"Mode":"light","Kind":"state","What":"selected tint over surface | surface","Distance":{"deutan":9.9,"deutan+ips":9.8,"dim":10.5,"drawn":10.6,"ips":10.5,"protan":8.4,"protan+ips":8.4,"tritan":12.9},"Floor":10,"Worst":6,"OK":true},{"Mode":"light","Kind":"hover","What":"hover tint over surface | surface","Distance":{"deutan":4.6,"deutan+ips":4.5,"dim":5,"drawn":5.1,"ips":5,"protan":3.9,"protan+ips":3.8,"tritan":6.5},"Floor":3,"Worst":2,"OK":true},{"Mode":"light","Kind":"meaning","What":"success | error","Distance":{"deutan":21.9,"deutan+ips":21.5,"dim":65.1,"drawn":67.6,"ips":66.6,"protan":37.1,"protan+ips":36.4,"tritan":57.7},"Floor":20,"Worst":10,"OK":true},{"Mode":"light","Kind":"meaning","What":"success | warning","Distance":{"deutan":15.1,"deutan+ips":14.7,"dim":33.5,"drawn":34.8,"ips":34.3,"protan":10.7,"protan+ips":10.1,"tritan":46.4},"Floor":20,"Worst":10,"OK":true},{"Mode":"light","Kind":"meaning","What":"warning | error","Distance":{"deutan":35.9,"deutan+ips":35.2,"dim":45.2,"drawn":48.3,"ips":47,"protan":45.5,"protan+ips":44.4,"tritan":11.4},"Floor":20,"Worst":10,"OK":true},{"Mode":"light","Kind":"meaning","What":"accent | success","Distance":{"deutan":34.1,"deutan+ips":33.7,"dim":34.9,"drawn":35.9,"ips":35.5,"protan":36.3,"protan+ips":35.9,"tritan":8.2},"Floor":20,"Worst":10,"OK":true},{"Mode":"light","Kind":"meaning","What":"accent | error","Distance":{"deutan":12,"deutan+ips":12,"dim":43.1,"drawn":44.5,"ips":43.9,"protan":14.8,"protan+ips":14.5,"tritan":59.5},"Floor":20,"Worst":10,"OK":true}];
+export const perception = [{"Mode":"dark","Kind":"text","What":"text on page","Contrast":16.12,"Floor":7,"Worst":0,"OK":true},{"Mode":"dark","Kind":"text","What":"muted on page","Contrast":7.77,"Floor":4.5,"Worst":0,"OK":true},{"Mode":"dark","Kind":"text","What":"accent_text on page","Contrast":10.07,"Floor":4.5,"Worst":0,"OK":true},{"Mode":"dark","Kind":"text","What":"success on page","Contrast":8.72,"Floor":4.5,"Worst":0,"OK":true},{"Mode":"dark","Kind":"text","What":"warning on page","Contrast":9.73,"Floor":4.5,"Worst":0,"OK":true},{"Mode":"dark","Kind":"text","What":"error on page","Contrast":7.45,"Floor":4.5,"Worst":0,"OK":true},{"Mode":"dark","Kind":"text","What":"text on surface","Contrast":14.97,"Floor":7,"Worst":0,"OK":true},{"Mode":"dark","Kind":"text","What":"muted on surface","Contrast":7.22,"Floor":4.5,"Worst":0,"OK":true},{"Mode":"dark","Kind":"text","What":"accent_text on surface","Contrast":9.35,"Floor":4.5,"Worst":0,"OK":true},{"Mode":"dark","Kind":"text","What":"success on surface","Contrast":8.1,"Floor":4.5,"Worst":0,"OK":true},{"Mode":"dark","Kind":"text","What":"warning on surface","Contrast":9.04,"Floor":4.5,"Worst":0,"OK":true},{"Mode":"dark","Kind":"text","What":"error on surface","Contrast":6.92,"Floor":4.5,"Worst":0,"OK":true},{"Mode":"dark","Kind":"text","What":"text on raised","Contrast":13.49,"Floor":7,"Worst":0,"OK":true},{"Mode":"dark","Kind":"text","What":"muted on raised","Contrast":6.5,"Floor":4.5,"Worst":0,"OK":true},{"Mode":"dark","Kind":"text","What":"accent_text on raised","Contrast":8.42,"Floor":4.5,"Worst":0,"OK":true},{"Mode":"dark","Kind":"text","What":"success on raised","Contrast":7.29,"Floor":4.5,"Worst":0,"OK":true},{"Mode":"dark","Kind":"text","What":"warning on raised","Contrast":8.14,"Floor":4.5,"Worst":0,"OK":true},{"Mode":"dark","Kind":"text","What":"error on raised","Contrast":6.23,"Floor":4.5,"Worst":0,"OK":true},{"Mode":"dark","Kind":"text","What":"text on overlay","Contrast":11.38,"Floor":7,"Worst":0,"OK":true},{"Mode":"dark","Kind":"text","What":"muted on overlay","Contrast":5.49,"Floor":4.5,"Worst":0,"OK":true},{"Mode":"dark","Kind":"text","What":"accent_text on overlay","Contrast":7.11,"Floor":4.5,"Worst":0,"OK":true},{"Mode":"dark","Kind":"text","What":"success on overlay","Contrast":6.16,"Floor":4.5,"Worst":0,"OK":true},{"Mode":"dark","Kind":"text","What":"warning on overlay","Contrast":6.87,"Floor":4.5,"Worst":0,"OK":true},{"Mode":"dark","Kind":"text","What":"error on overlay","Contrast":5.26,"Floor":4.5,"Worst":0,"OK":true},{"Mode":"dark","Kind":"text","What":"success on its badge over page","Contrast":7.29,"Floor":4.5,"Worst":0,"OK":true},{"Mode":"dark","Kind":"text","What":"warning on its badge over page","Contrast":8.05,"Floor":4.5,"Worst":0,"OK":true},{"Mode":"dark","Kind":"text","What":"error on its badge over page","Contrast":6.38,"Floor":4.5,"Worst":0,"OK":true},{"Mode":"dark","Kind":"text","What":"accent_text on its badge over page","Contrast":8.24,"Floor":4.5,"Worst":0,"OK":true},{"Mode":"dark","Kind":"text","What":"success on its badge over surface","Contrast":6.6,"Floor":4.5,"Worst":0,"OK":true},{"Mode":"dark","Kind":"text","What":"warning on its badge over surface","Contrast":7.29,"Floor":4.5,"Worst":0,"OK":true},{"Mode":"dark","Kind":"text","What":"error on its badge over surface","Contrast":5.8,"Floor":4.5,"Worst":0,"OK":true},{"Mode":"dark","Kind":"text","What":"accent_text on its badge over surface","Contrast":7.43,"Floor":4.5,"Worst":0,"OK":true},{"Mode":"dark","Kind":"text","What":"success on its badge over raised","Contrast":5.86,"Floor":4.5,"Worst":0,"OK":true},{"Mode":"dark","Kind":"text","What":"warning on its badge over raised","Contrast":6.46,"Floor":4.5,"Worst":0,"OK":true},{"Mode":"dark","Kind":"text","What":"error on its badge over raised","Contrast":5.17,"Floor":4.5,"Worst":0,"OK":true},{"Mode":"dark","Kind":"text","What":"accent_text on its badge over raised","Contrast":6.58,"Floor":4.5,"Worst":0,"OK":true},{"Mode":"dark","Kind":"text","What":"label on page","Contrast":8.67,"Floor":4.5,"Worst":0,"OK":true},{"Mode":"dark","Kind":"text","What":"accent_text on the selected tint over page","Contrast":7.53,"Floor":4.5,"Worst":0,"OK":true},{"Mode":"dark","Kind":"text","What":"text on the selected tint over page","Contrast":12.05,"Floor":4.5,"Worst":0,"OK":true},{"Mode":"dark","Kind":"text","What":"label on surface","Contrast":8.06,"Floor":4.5,"Worst":0,"OK":true},{"Mode":"dark","Kind":"text","What":"accent_text on the selected tint over surface","Contrast":6.87,"Floor":4.5,"Worst":0,"OK":true},{"Mode":"dark","Kind":"text","What":"text on the selected tint over surface","Contrast":11,"Floor":4.5,"Worst":0,"OK":true},{"Mode":"dark","Kind":"text","What":"on_accent on accent","Contrast":4.53,"Floor":4.5,"Worst":0,"OK":true},{"Mode":"dark","Kind":"text","What":"white on destructive","Contrast":5.22,"Floor":4.5,"Worst":0,"OK":true},{"Mode":"dark","Kind":"text","What":"on_accent on accent under the pointer","Contrast":5.58,"Floor":4.5,"Worst":0,"OK":true},{"Mode":"dark","Kind":"text","What":"on_accent on accent pressed","Contrast":6.94,"Floor":4.5,"Worst":0,"OK":true},{"Mode":"dark","Kind":"text","What":"white on destructive under the pointer","Contrast":6.37,"Floor":4.5,"Worst":0,"OK":true},{"Mode":"dark","Kind":"text","What":"white on destructive pressed","Contrast":7.83,"Floor":4.5,"Worst":0,"OK":true},{"Mode":"dark","Kind":"text","What":"text on a secondary button","Contrast":13.49,"Floor":7,"Worst":0,"OK":true},{"Mode":"dark","Kind":"text","What":"text on a secondary button under the pointer","Contrast":11.38,"Floor":7,"Worst":0,"OK":true},{"Mode":"dark","Kind":"non-text","What":"border_strong on page","Contrast":4.04,"Floor":3,"Worst":0,"OK":true},{"Mode":"dark","Kind":"non-text","What":"accent (the focus ring) on page","Contrast":4.27,"Floor":3,"Worst":0,"OK":true},{"Mode":"dark","Kind":"non-text","What":"border_strong on raised","Contrast":3.38,"Floor":3,"Worst":0,"OK":true},{"Mode":"dark","Kind":"non-text","What":"accent (the focus ring) on raised","Contrast":3.57,"Floor":3,"Worst":0,"OK":true},{"Mode":"dark","Kind":"edge","What":"page | surface","Distance":{"deutan":4.1,"deutan+ips":3.3,"dim":2.9,"drawn":4,"ips":3.2,"protan":4,"protan+ips":3.2,"tritan":4.3},"Floor":3,"Worst":2,"OK":true},{"Mode":"dark","Kind":"edge","What":"surface | raised","Distance":{"deutan":3.2,"deutan+ips":2.9,"dim":2.6,"drawn":3.2,"ips":2.9,"protan":3.2,"protan+ips":2.9,"tritan":3.5},"Floor":3,"Worst":2,"OK":true},{"Mode":"dark","Kind":"edge","What":"page | raised","Distance":{"deutan":6.8,"deutan+ips":6,"dim":5.3,"drawn":6.7,"ips":5.9,"protan":6.8,"protan+ips":6,"tritan":7.6},"Floor":3,"Worst":2,"OK":true},{"Mode":"dark","Kind":"line","What":"border on page","Distance":{"deutan":13.3,"deutan+ips":12.3,"dim":11.2,"drawn":13.4,"ips":12.3,"protan":13.7,"protan+ips":12.6,"tritan":13.9},"Floor":6,"Worst":4,"OK":true},{"Mode":"dark","Kind":"line","What":"border on raised","Distance":{"deutan":7,"deutan+ips":6.6,"dim":6.2,"drawn":7.1,"ips":6.7,"protan":7.2,"protan+ips":6.8,"tritan":7.1},"Floor":6,"Worst":4,"OK":true},{"Mode":"dark","Kind":"state","What":"selected tint over page | page","Distance":{"deutan":11.4,"deutan+ips":10.5,"dim":10.1,"drawn":11.9,"ips":11,"protan":11.3,"protan+ips":10.4,"tritan":14.4},"Floor":10,"Worst":6,"OK":true},{"Mode":"dark","Kind":"hover","What":"hover tint over page | page","Distance":{"deutan":5.2,"deutan+ips":4.2,"dim":3.9,"drawn":5.3,"ips":4.4,"protan":4.8,"protan+ips":4,"tritan":8},"Floor":3,"Worst":2,"OK":true},{"Mode":"dark","Kind":"state","What":"selected tint over surface | surface","Distance":{"deutan":9.6,"deutan+ips":9.3,"dim":9.6,"drawn":10.8,"ips":10.3,"protan":9.9,"protan+ips":9.5,"tritan":12.3},"Floor":10,"Worst":6,"OK":true},{"Mode":"dark","Kind":"hover","What":"hover tint over surface | surface","Distance":{"deutan":3.8,"deutan+ips":3.7,"dim":3.8,"drawn":4.5,"ips":4.1,"protan":3.8,"protan+ips":3.6,"tritan":6.4},"Floor":3,"Worst":2,"OK":true},{"Mode":"dark","Kind":"hover","What":"accent under the pointer | accent","Distance":{"deutan":5.4,"deutan+ips":5.4,"dim":5.6,"drawn":5.7,"ips":5.7,"protan":6.2,"protan+ips":6.1,"tritan":6.1},"Floor":3,"Worst":2,"OK":true},{"Mode":"dark","Kind":"hover","What":"destructive under the pointer | destructive","Distance":{"deutan":5.8,"deutan+ips":5.8,"dim":5.2,"drawn":5.3,"ips":5.2,"protan":3.9,"protan+ips":3.9,"tritan":5.3},"Floor":3,"Worst":2,"OK":true},{"Mode":"dark","Kind":"hover","What":"secondary under the pointer | secondary","Distance":{"deutan":4.3,"deutan+ips":4.1,"dim":3.9,"drawn":4.4,"ips":4.1,"protan":4.5,"protan+ips":4.2,"tritan":4.4},"Floor":3,"Worst":2,"OK":true},{"Mode":"dark","Kind":"edge","What":"secondary button | page","Distance":{"deutan":6.8,"deutan+ips":6,"dim":5.3,"drawn":6.7,"ips":5.9,"protan":6.8,"protan+ips":6,"tritan":7.6},"Floor":3,"Worst":2,"OK":true},{"Mode":"dark","Kind":"meaning","What":"success | error","Distance":{"deutan":14.6,"deutan+ips":14.5,"dim":69,"drawn":69.9,"ips":69.6,"protan":32.4,"protan+ips":32.2,"tritan":64.2},"Floor":20,"Worst":10,"OK":true},{"Mode":"dark","Kind":"meaning","What":"success | warning","Distance":{"deutan":16.7,"deutan+ips":16.6,"dim":35.1,"drawn":35.5,"ips":35.4,"protan":11.5,"protan+ips":11.4,"tritan":51.9},"Floor":20,"Worst":10,"OK":true},{"Mode":"dark","Kind":"meaning","What":"warning | error","Distance":{"deutan":28.9,"deutan+ips":28.8,"dim":47.7,"drawn":48.5,"ips":48.2,"protan":41.5,"protan+ips":41.2,"tritan":11.5},"Floor":20,"Worst":10,"OK":true},{"Mode":"dark","Kind":"meaning","What":"accent | success","Distance":{"deutan":40.6,"deutan+ips":40.3,"dim":41.6,"drawn":42.3,"ips":42,"protan":42.5,"protan+ips":42.2,"tritan":18},"Floor":20,"Worst":10,"OK":true},{"Mode":"dark","Kind":"meaning","What":"accent | error","Distance":{"deutan":27,"deutan+ips":26.7,"dim":46.4,"drawn":47.3,"ips":47,"protan":10.8,"protan+ips":10.7,"tritan":62.5},"Floor":20,"Worst":10,"OK":true},{"Mode":"light","Kind":"text","What":"text on page","Contrast":17.74,"Floor":7,"Worst":0,"OK":true},{"Mode":"light","Kind":"text","What":"muted on page","Contrast":6.63,"Floor":4.5,"Worst":0,"OK":true},{"Mode":"light","Kind":"text","What":"accent_text on page","Contrast":7.46,"Floor":4.5,"Worst":0,"OK":true},{"Mode":"light","Kind":"text","What":"success on page","Contrast":6.81,"Floor":4.5,"Worst":0,"OK":true},{"Mode":"light","Kind":"text","What":"warning on page","Contrast":6.71,"Floor":4.5,"Worst":0,"OK":true},{"Mode":"light","Kind":"text","What":"error on page","Contrast":7.67,"Floor":4.5,"Worst":0,"OK":true},{"Mode":"light","Kind":"text","What":"text on surface","Contrast":16.09,"Floor":7,"Worst":0,"OK":true},{"Mode":"light","Kind":"text","What":"muted on surface","Contrast":6.01,"Floor":4.5,"Worst":0,"OK":true},{"Mode":"light","Kind":"text","What":"accent_text on surface","Contrast":6.76,"Floor":4.5,"Worst":0,"OK":true},{"Mode":"light","Kind":"text","What":"success on surface","Contrast":6.18,"Floor":4.5,"Worst":0,"OK":true},{"Mode":"light","Kind":"text","What":"warning on surface","Contrast":6.09,"Floor":4.5,"Worst":0,"OK":true},{"Mode":"light","Kind":"text","What":"error on surface","Contrast":6.95,"Floor":4.5,"Worst":0,"OK":true},{"Mode":"light","Kind":"text","What":"text on raised","Contrast":14.27,"Floor":7,"Worst":0,"OK":true},{"Mode":"light","Kind":"text","What":"muted on raised","Contrast":5.33,"Floor":4.5,"Worst":0,"OK":true},{"Mode":"light","Kind":"text","What":"accent_text on raised","Contrast":6,"Floor":4.5,"Worst":0,"OK":true},{"Mode":"light","Kind":"text","What":"success on raised","Contrast":5.48,"Floor":4.5,"Worst":0,"OK":true},{"Mode":"light","Kind":"text","What":"warning on raised","Contrast":5.4,"Floor":4.5,"Worst":0,"OK":true},{"Mode":"light","Kind":"text","What":"error on raised","Contrast":6.17,"Floor":4.5,"Worst":0,"OK":true},{"Mode":"light","Kind":"text","What":"text on overlay","Contrast":17.74,"Floor":7,"Worst":0,"OK":true},{"Mode":"light","Kind":"text","What":"muted on overlay","Contrast":6.63,"Floor":4.5,"Worst":0,"OK":true},{"Mode":"light","Kind":"text","What":"accent_text on overlay","Contrast":7.46,"Floor":4.5,"Worst":0,"OK":true},{"Mode":"light","Kind":"text","What":"success on overlay","Contrast":6.81,"Floor":4.5,"Worst":0,"OK":true},{"Mode":"light","Kind":"text","What":"warning on overlay","Contrast":6.71,"Floor":4.5,"Worst":0,"OK":true},{"Mode":"light","Kind":"text","What":"error on overlay","Contrast":7.67,"Floor":4.5,"Worst":0,"OK":true},{"Mode":"light","Kind":"text","What":"success on its badge over page","Contrast":5.68,"Floor":4.5,"Worst":0,"OK":true},{"Mode":"light","Kind":"text","What":"warning on its badge over page","Contrast":5.61,"Floor":4.5,"Worst":0,"OK":true},{"Mode":"light","Kind":"text","What":"error on its badge over page","Contrast":6.25,"Floor":4.5,"Worst":0,"OK":true},{"Mode":"light","Kind":"text","What":"accent_text on its badge over page","Contrast":6.17,"Floor":4.5,"Worst":0,"OK":true},{"Mode":"light","Kind":"text","What":"success on its badge over surface","Contrast":5.18,"Floor":4.5,"Worst":0,"OK":true},{"Mode":"light","Kind":"text","What":"warning on its badge over surface","Contrast":5.12,"Floor":4.5,"Worst":0,"OK":true},{"Mode":"light","Kind":"text","What":"error on its badge over surface","Contrast":5.7,"Floor":4.5,"Worst":0,"OK":true},{"Mode":"light","Kind":"text","What":"accent_text on its badge over surface","Contrast":5.63,"Floor":4.5,"Worst":0,"OK":true},{"Mode":"light","Kind":"text","What":"success on its badge over raised","Contrast":4.63,"Floor":4.5,"Worst":0,"OK":true},{"Mode":"light","Kind":"text","What":"warning on its badge over raised","Contrast":4.57,"Floor":4.5,"Worst":0,"OK":true},{"Mode":"light","Kind":"text","What":"error on its badge over raised","Contrast":5.08,"Floor":4.5,"Worst":0,"OK":true},{"Mode":"light","Kind":"text","What":"accent_text on its badge over raised","Contrast":5.03,"Floor":4.5,"Worst":0,"OK":true},{"Mode":"light","Kind":"text","What":"label on page","Contrast":7.6,"Floor":4.5,"Worst":0,"OK":true},{"Mode":"light","Kind":"text","What":"accent_text on the selected tint over page","Contrast":5.02,"Floor":4.5,"Worst":0,"OK":true},{"Mode":"light","Kind":"text","What":"text on the selected tint over page","Contrast":11.94,"Floor":4.5,"Worst":0,"OK":true},{"Mode":"light","Kind":"text","What":"label on surface","Contrast":6.9,"Floor":4.5,"Worst":0,"OK":true},{"Mode":"light","Kind":"text","What":"accent_text on the selected tint over surface","Contrast":4.63,"Floor":4.5,"Worst":0,"OK":true},{"Mode":"light","Kind":"text","What":"text on the selected tint over surface","Contrast":11.01,"Floor":4.5,"Worst":0,"OK":true},{"Mode":"light","Kind":"text","What":"on_accent on accent","Contrast":5.5,"Floor":4.5,"Worst":0,"OK":true},{"Mode":"light","Kind":"text","What":"white on destructive","Contrast":5.62,"Floor":4.5,"Worst":0,"OK":true},{"Mode":"light","Kind":"text","What":"on_accent on accent under the pointer","Contrast":6.68,"Floor":4.5,"Worst":0,"OK":true},{"Mode":"light","Kind":"text","What":"on_accent on accent pressed","Contrast":8.14,"Floor":4.5,"Worst":0,"OK":true},{"Mode":"light","Kind":"text","What":"white on destructive under the pointer","Contrast":6.83,"Floor":4.5,"Worst":0,"OK":true},{"Mode":"light","Kind":"text","What":"white on destructive pressed","Contrast":8.33,"Floor":4.5,"Worst":0,"OK":true},{"Mode":"light","Kind":"text","What":"text on a secondary button","Contrast":16.09,"Floor":7,"Worst":0,"OK":true},{"Mode":"light","Kind":"text","What":"text on a secondary button under the pointer","Contrast":14.27,"Floor":7,"Worst":0,"OK":true},{"Mode":"light","Kind":"non-text","What":"border_strong on page","Contrast":3.86,"Floor":3,"Worst":0,"OK":true},{"Mode":"light","Kind":"non-text","What":"accent (the focus ring) on page","Contrast":5.5,"Floor":3,"Worst":0,"OK":true},{"Mode":"light","Kind":"non-text","What":"border_strong on raised","Contrast":3.1,"Floor":3,"Worst":0,"OK":true},{"Mode":"light","Kind":"non-text","What":"accent (the focus ring) on raised","Contrast":4.43,"Floor":3,"Worst":0,"OK":true},{"Mode":"light","Kind":"edge","What":"page | surface","Distance":{"deutan":3.6,"deutan+ips":3.6,"dim":3.4,"drawn":3.4,"ips":3.4,"protan":3.3,"protan+ips":3.3,"tritan":3.5},"Floor":3,"Worst":2,"OK":true},{"Mode":"light","Kind":"edge","What":"surface | raised","Distance":{"deutan":3.5,"deutan+ips":3.5,"dim":3.4,"drawn":3.4,"ips":3.4,"protan":3.3,"protan+ips":3.3,"tritan":3.6},"Floor":3,"Worst":2,"OK":true},{"Mode":"light","Kind":"edge","What":"page | raised","Distance":{"deutan":7.1,"deutan+ips":7,"dim":6.8,"drawn":6.8,"ips":6.8,"protan":6.6,"protan+ips":6.6,"tritan":7.1},"Floor":3,"Worst":2,"OK":true},{"Mode":"light","Kind":"line","What":"border on page","Distance":{"deutan":13.1,"deutan+ips":13,"dim":12.7,"drawn":12.8,"ips":12.7,"protan":12.5,"protan+ips":12.4,"tritan":13},"Floor":6,"Worst":4,"OK":true},{"Mode":"light","Kind":"line","What":"border on raised","Distance":{"deutan":6.5,"deutan+ips":6.5,"dim":6.3,"drawn":6.4,"ips":6.4,"protan":6.3,"protan+ips":6.3,"tritan":6.4},"Floor":6,"Worst":4,"OK":true},{"Mode":"light","Kind":"state","What":"selected tint over page | page","Distance":{"deutan":13.2,"deutan+ips":13.1,"dim":13.8,"drawn":13.9,"ips":13.9,"protan":11.3,"protan+ips":11.3,"tritan":17},"Floor":10,"Worst":6,"OK":true},{"Mode":"light","Kind":"hover","What":"hover tint over page | page","Distance":{"deutan":5.1,"deutan+ips":5.1,"dim":5.6,"drawn":5.6,"ips":5.6,"protan":4.3,"protan+ips":4.3,"tritan":7.4},"Floor":3,"Worst":2,"OK":true},{"Mode":"light","Kind":"state","What":"selected tint over surface | surface","Distance":{"deutan":12.1,"deutan+ips":12.1,"dim":12.8,"drawn":12.9,"ips":12.9,"protan":10.4,"protan+ips":10.3,"tritan":15.6},"Floor":10,"Worst":6,"OK":true},{"Mode":"light","Kind":"hover","What":"hover tint over surface | surface","Distance":{"deutan":4.6,"deutan+ips":4.6,"dim":5.1,"drawn":5.2,"ips":5.1,"protan":3.9,"protan+ips":3.9,"tritan":6.6},"Floor":3,"Worst":2,"OK":true},{"Mode":"light","Kind":"hover","What":"accent under the pointer | accent","Distance":{"deutan":4.6,"deutan+ips":4.6,"dim":4.8,"drawn":4.9,"ips":4.8,"protan":5.3,"protan+ips":5.3,"tritan":5.3},"Floor":3,"Worst":2,"OK":true},{"Mode":"light","Kind":"hover","What":"destructive under the pointer | destructive","Distance":{"deutan":5.6,"deutan+ips":5.5,"dim":4.9,"drawn":5,"ips":5,"protan":3.7,"protan+ips":3.6,"tritan":5.2},"Floor":3,"Worst":2,"OK":true},{"Mode":"light","Kind":"hover","What":"secondary under the pointer | secondary","Distance":{"deutan":3.5,"deutan+ips":3.5,"dim":3.4,"drawn":3.4,"ips":3.4,"protan":3.3,"protan+ips":3.3,"tritan":3.6},"Floor":3,"Worst":2,"OK":true},{"Mode":"light","Kind":"edge","What":"secondary button | page","Distance":{"deutan":3.6,"deutan+ips":3.6,"dim":3.4,"drawn":3.4,"ips":3.4,"protan":3.3,"protan+ips":3.3,"tritan":3.5},"Floor":3,"Worst":2,"OK":true},{"Mode":"light","Kind":"meaning","What":"success | error","Distance":{"deutan":10.3,"deutan+ips":10,"dim":61.3,"drawn":64.1,"ips":62.9,"protan":29.7,"protan+ips":29,"tritan":56.5},"Floor":20,"Worst":10,"OK":true},{"Mode":"light","Kind":"meaning","What":"success | warning","Distance":{"deutan":16.1,"deutan+ips":15.6,"dim":35.2,"drawn":36.7,"ips":36.1,"protan":11.5,"protan+ips":10.8,"tritan":46.6},"Floor":20,"Worst":10,"OK":true},{"Mode":"light","Kind":"meaning","What":"warning | error","Distance":{"deutan":24.6,"deutan+ips":24,"dim":37.8,"drawn":41.1,"ips":39.7,"protan":38.6,"protan+ips":37.4,"tritan":9.3},"Floor":20,"Worst":10,"OK":true},{"Mode":"light","Kind":"meaning","What":"accent | success","Distance":{"deutan":32.2,"deutan+ips":31.7,"dim":33.3,"drawn":34.3,"ips":33.9,"protan":34.9,"protan+ips":34.5,"tritan":9.7},"Floor":20,"Worst":10,"OK":true},{"Mode":"light","Kind":"meaning","What":"accent | error","Distance":{"deutan":19.9,"deutan+ips":19.8,"dim":44.2,"drawn":45.6,"ips":45,"protan":16.6,"protan+ips":16.4,"tritan":61.3},"Floor":20,"Worst":10,"OK":true}];
diff --git a/dist/starlight.css b/dist/starlight.css
index 922c601..8fdd894 100644
--- a/dist/starlight.css
+++ b/dist/starlight.css
@@ -1,36 +1,42 @@
/* Written by `task palette` from palette.toml (spin-stack/brand). Edit palette.toml, not this file. */
:root {
- --sl-color-accent-low: #163D51;
- --sl-color-accent: #27BFE8;
- --sl-color-accent-high: #87D6EF;
- --sl-color-white: #F5F8FB;
- --sl-color-gray-1: #EEF2F9;
- --sl-color-gray-2: #E6EDF6;
- --sl-color-gray-3: #9AA9C2;
- --sl-color-gray-4: #6A7EA0;
- --sl-color-gray-5: #33435F;
- --sl-color-gray-6: #172035;
- --sl-color-black: #111827;
- --sl-color-banner-bg: #1B435C;
- --sl-color-banner-text: #E6EDF6;
+ --sl-color-accent-low: #0F2C41;
+ --sl-color-accent: #1B7DB4;
+ --sl-color-accent-high: #A9D9F0;
+ --sl-color-white: #F5F7FA;
+ --sl-color-gray-1: #EEF1F6;
+ --sl-color-gray-2: #E6EBF2;
+ --sl-color-gray-3: #9AA5B8;
+ --sl-color-gray-4: #67738A;
+ --sl-color-gray-5: #2D3749;
+ --sl-color-gray-6: #111723;
+ --sl-color-black: #0A0E16;
+ --sl-color-banner-bg: #132D43;
+ --sl-color-banner-text: #E6EBF2;
}
:root[data-theme='light'] {
- --sl-color-accent-low: #C9DFEB;
+ --sl-color-accent-low: #BDD8E6;
--sl-color-accent: #0A6FA3;
- --sl-color-accent-high: #0D3B5A;
+ --sl-color-accent-high: #0C3957;
--sl-color-white: #0A0E17;
--sl-color-gray-1: #0E131F;
--sl-color-gray-2: #111827;
- --sl-color-gray-3: #4A5871;
- --sl-color-gray-4: #6E7D96;
- --sl-color-gray-5: #B9C6D8;
- --sl-color-gray-6: #DEE6F1;
+ --sl-color-gray-3: #525D72;
+ --sl-color-gray-4: #77829A;
+ --sl-color-gray-5: #C4CCD8;
+ --sl-color-gray-6: #E1E7F0;
--sl-color-black: #FFFFFF;
- --sl-color-banner-bg: #BDD7E6;
+ --sl-color-banner-bg: #BED7E6;
--sl-color-banner-text: #111827;
- --sl-color-gray-7: #F0F4F9;
+ --sl-color-gray-7: #F1F4F9;
}
:root {
--sl-font: 'IBM Plex Sans', system-ui, -apple-system, 'Segoe UI', sans-serif;
--sl-font-mono: 'IBM Plex Mono', ui-monospace, 'SF Mono', Menlo, monospace;
}
+:root {
+ --sl-text-h1: clamp(2rem, 1.452rem + 2.247vw, 3.25rem);
+ --sl-text-h2: clamp(1.625rem, 1.351rem + 1.124vw, 2.25rem);
+ --sl-text-h3: clamp(1.312rem, 1.176rem + 0.5618vw, 1.625rem);
+ --sl-text-h4: clamp(1.062rem, 0.9803rem + 0.3371vw, 1.25rem);
+}
diff --git a/dist/tokens.css b/dist/tokens.css
index da312ec..97df4f2 100644
--- a/dist/tokens.css
+++ b/dist/tokens.css
@@ -1,43 +1,65 @@
/* Written by `task palette` from palette.toml (spin-stack/brand). Edit palette.toml, not this file. */
:root, .spin-dark {
- --spin-accent: #27BFE8;
- --spin-accent-text: #27BFE8;
- --spin-border: #33435F;
- --spin-border-strong: #6A7EA0;
- --spin-destructive: #C2304F;
- --spin-error: #F57AB4;
- --spin-label: #5FE3D6;
- --spin-muted: #9AA9C2;
- --spin-on-accent: #111827;
- --spin-overlay: #273552;
- --spin-page: #111827;
- --spin-raised: #202C47;
- --spin-success: #3DDC97;
- --spin-surface: #172035;
- --spin-text: #E6EDF6;
- --spin-warning: #FFC857;
- --spin-accent-hover: #13293A;
- --spin-accent-selected: #163D51;
+ --spin-accent: #1B7DB4;
+ --spin-accent-text: #6CC6EE;
+ --spin-border: #2D3749;
+ --spin-border-strong: #67738A;
+ --spin-destructive: #C4384F;
+ --spin-error: #EE7BB0;
+ --spin-label: #8FB3C6;
+ --spin-muted: #9AA5B8;
+ --spin-on-accent: #FFFFFF;
+ --spin-overlay: #242E40;
+ --spin-page: #0A0E16;
+ --spin-raised: #19212F;
+ --spin-success: #4CC38A;
+ --spin-surface: #111723;
+ --spin-text: #E6EBF2;
+ --spin-warning: #E3B04B;
+ --spin-accent-fill-hover: #186E9E;
+ --spin-accent-fill-active: #155F89;
+ --spin-destructive-hover: #AC3146;
+ --spin-destructive-active: #952B3C;
+ --spin-control: #19212F;
+ --spin-control-hover: #242E40;
+ --spin-shadow-overlay: 0 0 0 1px rgb(0 0 0 / 0.4);
+ --spin-success-badge: color-mix(in srgb, var(--spin-success) 12%, transparent);
+ --spin-warning-badge: color-mix(in srgb, var(--spin-warning) 12%, transparent);
+ --spin-error-badge: color-mix(in srgb, var(--spin-error) 12%, transparent);
+ --spin-accent-text-badge: color-mix(in srgb, var(--spin-accent-text) 12%, transparent);
+ --spin-accent-hover: #0C1926;
+ --spin-accent-selected: #0F2C41;
}
:root[data-theme='light'] {
--spin-accent: #0A6FA3;
- --spin-accent-text: #085E8C;
- --spin-border: #B9C6D8;
- --spin-border-strong: #6E7D96;
- --spin-destructive: #B4234A;
- --spin-error: #9B1B73;
- --spin-label: #0C3C84;
- --spin-muted: #4A5871;
+ --spin-accent-text: #075A86;
+ --spin-border: #C4CCD8;
+ --spin-border-strong: #77829A;
+ --spin-destructive: #C0303F;
+ --spin-error: #9A1F60;
+ --spin-label: #33586E;
+ --spin-muted: #525D72;
--spin-on-accent: #FFFFFF;
--spin-overlay: #FFFFFF;
--spin-page: #FFFFFF;
- --spin-raised: #DEE6F1;
- --spin-success: #1E7046;
- --spin-surface: #F0F4F9;
+ --spin-raised: #E1E7F0;
+ --spin-success: #0F6844;
+ --spin-surface: #F1F4F9;
--spin-text: #111827;
- --spin-warning: #8A5A00;
+ --spin-warning: #845000;
+ --spin-accent-fill-hover: #09628F;
+ --spin-accent-fill-active: #08547C;
+ --spin-destructive-hover: #A92A37;
+ --spin-destructive-active: #922430;
+ --spin-control: #F1F4F9;
+ --spin-control-hover: #E1E7F0;
+ --spin-shadow-overlay: 0 12px 32px rgb(16 19 24 / 0.14), 0 2px 6px rgb(16 19 24 / 0.08);
+ --spin-success-badge: color-mix(in srgb, var(--spin-success) 12%, transparent);
+ --spin-warning-badge: color-mix(in srgb, var(--spin-warning) 12%, transparent);
+ --spin-error-badge: color-mix(in srgb, var(--spin-error) 12%, transparent);
+ --spin-accent-text-badge: color-mix(in srgb, var(--spin-accent-text) 12%, transparent);
--spin-accent-hover: #E7F1F6;
- --spin-accent-selected: #C9DFEB;
+ --spin-accent-selected: #BDD8E6;
}
:root {
--spin-brand-blue: #2F6BE3;
@@ -54,13 +76,24 @@
--spin-line-height: 1.6;
--spin-heading-weight: 600;
--spin-heading-tracking: -0.02em;
+ --spin-font-heading: var(--spin-font-sans);
+ --spin-numeric: tabular-nums;
+ --spin-text-display: clamp(2rem, 1.452rem + 2.247vw, 3.25rem);
+ --spin-text-h1: clamp(1.625rem, 1.351rem + 1.124vw, 2.25rem);
+ --spin-text-h2: clamp(1.312rem, 1.176rem + 0.5618vw, 1.625rem);
+ --spin-text-h3: clamp(1.062rem, 0.9803rem + 0.3371vw, 1.25rem);
--spin-measure: 72ch;
--spin-sidebar: 17rem;
- --spin-radius-sm: 4px;
+ --spin-radius-sm: 6px;
--spin-radius: 8px;
--spin-radius-lg: 12px;
--spin-target: 24px;
--spin-target-touch: 44px;
+ --spin-control-sm: 28px;
+ --spin-control-md: 32px;
+ --spin-control-lg: 36px;
+ --spin-control-icon: 16px;
+ --spin-control-gap: 6px;
--spin-width-contained: 1200px;
--spin-width-detail: 1152px;
--spin-width-wide: 1600px;
diff --git a/dist/tokens.json b/dist/tokens.json
index 5e2427a..9380e3e 100644
--- a/dist/tokens.json
+++ b/dist/tokens.json
@@ -7,28 +7,39 @@
"mint": "#5FE3D6",
"navy": "#111827"
},
+ "controls": {
+ "Sm": 28,
+ "Md": 32,
+ "Lg": 36,
+ "Icon": 16,
+ "Gap": 6
+ },
"dark": {
- "accent": "#27BFE8",
- "accent_text": "#27BFE8",
- "border": "#33435F",
- "border_strong": "#6A7EA0",
- "destructive": "#C2304F",
- "error": "#F57AB4",
- "label": "#5FE3D6",
- "muted": "#9AA9C2",
- "on_accent": "#111827",
- "overlay": "#273552",
- "page": "#111827",
- "raised": "#202C47",
- "success": "#3DDC97",
- "surface": "#172035",
- "text": "#E6EDF6",
- "warning": "#FFC857"
+ "accent": "#1B7DB4",
+ "accent_text": "#6CC6EE",
+ "border": "#2D3749",
+ "border_strong": "#67738A",
+ "destructive": "#C4384F",
+ "error": "#EE7BB0",
+ "label": "#8FB3C6",
+ "muted": "#9AA5B8",
+ "on_accent": "#FFFFFF",
+ "overlay": "#242E40",
+ "page": "#0A0E16",
+ "raised": "#19212F",
+ "success": "#4CC38A",
+ "surface": "#111723",
+ "text": "#E6EBF2",
+ "warning": "#E3B04B"
+ },
+ "elevation": {
+ "Dark": "0 0 0 1px rgb(0 0 0 / 0.4)",
+ "Light": "0 12px 32px rgb(16 19 24 / 0.14), 0 2px 6px rgb(16 19 24 / 0.08)"
},
"layout": {
"Measure": 72,
"Sidebar": 17,
- "RadiusSm": 4,
+ "RadiusSm": 6,
"Radius": 8,
"RadiusLg": 12,
"Target": 24,
@@ -42,21 +53,21 @@
},
"light": {
"accent": "#0A6FA3",
- "accent_text": "#085E8C",
- "border": "#B9C6D8",
- "border_strong": "#6E7D96",
- "destructive": "#B4234A",
- "error": "#9B1B73",
- "label": "#0C3C84",
- "muted": "#4A5871",
+ "accent_text": "#075A86",
+ "border": "#C4CCD8",
+ "border_strong": "#77829A",
+ "destructive": "#C0303F",
+ "error": "#9A1F60",
+ "label": "#33586E",
+ "muted": "#525D72",
"on_accent": "#FFFFFF",
"overlay": "#FFFFFF",
"page": "#FFFFFF",
- "raised": "#DEE6F1",
- "success": "#1E7046",
- "surface": "#F0F4F9",
+ "raised": "#E1E7F0",
+ "success": "#0F6844",
+ "surface": "#F1F4F9",
"text": "#111827",
- "warning": "#8A5A00"
+ "warning": "#845000"
},
"motion": {
"Fast": 120,
@@ -67,8 +78,10 @@
},
"tint": {
"Hover": 0.1,
- "Selected": 0.22,
- "Scrim": 0.6
+ "Selected": 0.27,
+ "Scrim": 0.64,
+ "Fill": 0.12,
+ "Badge": 0.12
},
"type": {
"Sans": "IBM Plex Sans",
@@ -110,6 +123,26 @@
"LineHeight": 1.6,
"HeadingWeight": 600,
"HeadingTracking": -0.02,
+ "Heading": "sans",
+ "Fluid": {
+ "display": [
+ 32,
+ 52
+ ],
+ "h1": [
+ 26,
+ 36
+ ],
+ "h2": [
+ 21,
+ 26
+ ],
+ "h3": [
+ 17,
+ 20
+ ]
+ },
+ "Numeric": "tabular-nums",
"Wordmark": "Spinbox"
}
}
diff --git a/index.html b/index.html
index 3a446aa..7b43410 100644
--- a/index.html
+++ b/index.html
@@ -21,8 +21,8 @@
.bar label { display: inline-flex; gap: .4rem; align-items: center; color: var(--spin-muted); }
.bar select, .bar button { font: inherit; color: var(--spin-text); background: var(--spin-raised); border: 1px solid var(--spin-border-strong); border-radius: var(--spin-radius-sm); min-height: var(--spin-target); padding: 0 .5rem; }
.wrap { max-width: 1280px; margin: 0 auto; padding: 1.5rem 1.25rem 4rem; }
-h1, h2, h3 { font-family: var(--spin-font-mono); font-weight: var(--spin-heading-weight); letter-spacing: var(--spin-heading-tracking); color: var(--spin-text); }
-.label { font: 600 12px var(--spin-font-mono); letter-spacing: .1em; text-transform: uppercase; color: var(--spin-label); }
+h1, h2, h3 { font-family: var(--spin-font-heading); font-weight: var(--spin-heading-weight); letter-spacing: var(--spin-heading-tracking); color: var(--spin-text); }
+.label { font: 600 13px/1.4 var(--spin-font-sans); color: var(--spin-label); }
section { margin-top: 3rem; }
section > .label { display: block; border-top: 1px solid var(--spin-border); padding-top: 1.5rem; margin-bottom: 1rem; }
.muted { color: var(--spin-muted); }
@@ -32,7 +32,7 @@
/* The masthead: what this page is, and the view's controls. */
.masthead { background: var(--spin-surface); border-bottom: 1px solid var(--spin-border); }
.masthead .wrap { padding-block: 1.5rem 1.25rem; }
-.masthead h1 { font-size: clamp(1.6rem, 1rem + 2vw, 2.4rem); margin: .75rem 0 .25rem; }
+.masthead h1 { font-size: var(--spin-text-h1); margin: .75rem 0 .25rem; }
.masthead p { margin: 0 0 1rem; max-width: 60rem; }
.controls { display: flex; flex-wrap: wrap; gap: .75rem 1.25rem; }
.controls label { display: inline-flex; gap: .5rem; align-items: center; font: 500 13px var(--spin-font-mono); color: var(--spin-muted); }
@@ -53,7 +53,7 @@
/* A part: what follows is sample content, a decision, or a check. */
.part { display: flex; gap: 1rem; align-items: baseline; margin-top: 4rem; padding: 1.25rem 0; border-top: 2px solid var(--spin-border-strong); }
.part .num { font: 600 2rem var(--spin-font-mono); color: var(--spin-label); line-height: 1; }
-.part h2 { margin: 0; font-size: 1.4rem; } .part p { margin: .25rem 0 0; max-width: 52rem; }
+.part h2 { margin: 0; font-size: var(--spin-text-h2); } .part p { margin: .25rem 0 0; max-width: 52rem; }
#page > .part:first-child { margin-top: 1.5rem; border-top: 0; }
.perc-wrap summary { cursor: pointer; font: 500 13px var(--spin-font-mono); padding: .5rem 0; color: var(--spin-text); }
@@ -72,6 +72,42 @@
.donts { display: flex; gap: 1.25rem; flex-wrap: wrap; }
.donts figure { text-align: center; } .donts figcaption::before { content: '✕ '; color: var(--spin-error); }
+
+/* A state as a badge: its shape and word on its tint (--spin-*-badge), the same in a table, a
+ header and a toast. Stopped has no colour of its own: a hollow ring, the muted ink. */
+.badge { display: inline-flex; align-items: center; gap: 6px; height: 22px; padding: 0 8px 0 7px; border-radius: 999px; font: 400 12px/1 var(--spin-font-mono); white-space: nowrap; }
+.badge::before { content: ''; width: 7px; height: 7px; border-radius: 50%; background: currentColor; flex: none; }
+.badge.ok { color: var(--spin-success); background: var(--spin-success-badge); }
+.badge.warn { color: var(--spin-warning); background: var(--spin-warning-badge); } .badge.warn::before { border-radius: 1px; transform: rotate(45deg); width: 6px; height: 6px; }
+.badge.err { color: var(--spin-error); background: var(--spin-error-badge); } .badge.err::before { border-radius: 1px; }
+.badge.busy { color: var(--spin-accent-text); background: var(--spin-accent-text-badge); } .badge.busy::before { background: none; border: 2px solid currentColor; border-right-color: transparent; width: 6px; height: 6px; }
+.badge.idle { color: var(--spin-muted); box-shadow: inset 0 0 0 1px var(--spin-border); } .badge.idle::before { background: none; box-shadow: inset 0 0 0 1.5px currentColor; }
+.specimen { display: grid; grid-template-columns: repeat(auto-fit, minmax(16rem, 1fr)); gap: 1rem; margin-top: 1rem; }
+.specimen > figure { margin: 0; border: 1px solid var(--spin-border); border-radius: var(--spin-radius); padding: 1rem; background: var(--spin-surface); }
+.badges { display: flex; flex-wrap: wrap; gap: .5rem; margin-top: .5rem; }
+/* Fields: the input's edge at 3:1 (border-strong), the focus ring in the accent, an error in its
+ colour with its message under it, a hint in muted. 16 px on a touch screen (iOS zooms below). */
+.field { display: grid; gap: 6px; margin-top: .75rem; }
+.field label { font: 500 13px var(--spin-font-sans); color: var(--spin-text); }
+.field input { font: 13px var(--spin-font-sans); color: var(--spin-text); background: var(--spin-page); border: 1px solid var(--spin-border-strong); border-radius: var(--spin-radius-sm); height: var(--spin-control-md); padding: 0 10px; width: 100%; }
+.field input.mono { font-family: var(--spin-font-mono); }
+.field input:hover, .field input.is-hover { border-color: var(--spin-muted); }
+.field input:focus-visible, .field input.is-focus { outline: 2px solid var(--spin-accent); outline-offset: 0; border-color: var(--spin-accent); }
+.field input[aria-invalid="true"] { border-color: var(--spin-error); }
+.field input:disabled { opacity: .5; }
+.field small { font: 12px var(--spin-font-sans); color: var(--spin-muted); } .field small.err { color: var(--spin-error); }
+@media (pointer: coarse) { .field input { font-size: 16px; height: var(--spin-target-touch); } }
+/* A table of many: headers in the muted sans, rows in the dense size, ids in mono, figures right
+ and tabular, a row under the pointer on the control fill, a selected one on the accent's tint. */
+.grid { width: 100%; border-collapse: collapse; font: 13px/1.45 var(--spin-font-sans); font-variant-numeric: var(--spin-numeric); }
+.grid th { font-weight: 500; font-size: 12px; color: var(--spin-muted); text-align: left; padding: 8px 10px; border-bottom: 1px solid var(--spin-border); white-space: nowrap; }
+.grid td { padding: 8px 10px; border-bottom: 1px solid var(--spin-border); white-space: nowrap; }
+.grid .n { text-align: right; } .grid .id { font-family: var(--spin-font-mono); color: var(--spin-muted); }
+.grid tr.is-hover td { background: var(--spin-control); } .grid tbody tr:hover td { background: var(--spin-control); }
+.grid tr.is-selected td { background: var(--spin-accent-hover); } .grid tr.is-selected td:first-child { box-shadow: inset 2px 0 0 var(--spin-accent); }
+.grid-wrap { overflow-x: auto; border: 1px solid var(--spin-border); border-radius: var(--spin-radius); margin-top: 1rem; }
+.grid-wrap .grid tr:last-child td { border-bottom: 0; }
+
/* The palette, by what each color is for */
.group { display: grid; grid-template-columns: minmax(10rem, 14rem) 1fr; gap: 1rem 1.5rem; padding: 1rem 0; border-top: 1px solid var(--spin-border); }
.group h3 { font-size: 14px; margin: 0 0 .25rem; } .group p { margin: 0; font-size: 13px; }
@@ -99,12 +135,36 @@
.site header .search { margin-left: auto; min-width: 12rem; min-height: var(--spin-target); border: 1px solid var(--spin-border); border-radius: var(--spin-radius); padding: .25rem .75rem; color: var(--spin-muted); background: var(--spin-page); }
.banner { padding: .5rem 1rem; text-align: center; font-size: 14px; background: var(--spin-accent-selected); border-bottom: 1px solid var(--spin-border); }
.hero { display: grid; gap: 2rem; padding: 3rem 2rem; grid-template-columns: 7fr 5fr; align-items: center; }
-.hero h1 { font-size: clamp(2rem, 1rem + 3vw, 3.2rem); line-height: 1.08; margin: .5rem 0 1rem; }
+.hero h1 { font-size: var(--spin-text-display); line-height: 1.08; margin: .5rem 0 1rem; text-wrap: balance; }
.hero img { width: 100%; max-width: 20rem; justify-self: center; border-radius: 22%; }
-.btn { display: inline-flex; align-items: center; justify-content: center; min-height: 40px; padding: 0 1.1rem; border-radius: 999px; font: 500 15px var(--spin-font-sans); border: 1px solid transparent; cursor: pointer; text-decoration: none; }
+/* Buttons: one geometry from the site to the dashboard - the control heights, the small radius,
+ a fill and a hairline - and the states palette.toml derives and checks (dist/tokens.css). */
+.btn { display: inline-flex; align-items: center; justify-content: center; gap: var(--spin-control-gap); height: var(--spin-control-md); padding: 0 12px; border-radius: var(--spin-radius-sm); font: 500 13px/1 var(--spin-font-sans); border: 1px solid transparent; cursor: pointer; text-decoration: none; white-space: nowrap; transition: background-color var(--spin-duration-fast) var(--spin-ease); }
+.btn.sm { height: var(--spin-control-sm); padding: 0 10px; font-size: 12px; }
+.btn.lg { height: var(--spin-control-lg); padding: 0 16px; font-size: 14px; }
+.btn svg { width: var(--spin-control-icon); height: var(--spin-control-icon); flex: none; }
.btn.primary { background: var(--spin-accent); color: var(--spin-on-accent); }
-.btn.ghost { color: var(--spin-text); border-color: var(--spin-border-strong); background: transparent; }
-.btn.danger { background: var(--spin-destructive); color: #fff; border-radius: var(--spin-radius); }
+.btn.primary:hover, .btn.primary.is-hover { background: var(--spin-accent-fill-hover); }
+.btn.primary:active, .btn.primary.is-active { background: var(--spin-accent-fill-active); }
+.btn.secondary, .btn.ghost { color: var(--spin-text); }
+.btn.secondary { background: var(--spin-control); border-color: var(--spin-border); }
+.btn.secondary:hover, .btn.secondary.is-hover, .btn.secondary:active, .btn.secondary.is-active { background: var(--spin-control-hover); }
+.btn.ghost { background: transparent; color: var(--spin-muted); }
+.btn.ghost:hover, .btn.ghost.is-hover, .btn.ghost:active, .btn.ghost.is-active { background: var(--spin-control); color: var(--spin-text); }
+.btn.danger { background: var(--spin-destructive); color: #fff; }
+.btn.danger:hover, .btn.danger.is-hover { background: var(--spin-destructive-hover); }
+.btn.danger:active, .btn.danger.is-active { background: var(--spin-destructive-active); }
+.btn.icon { width: var(--spin-control-md); padding: 0; } .btn.icon.sm { width: var(--spin-control-sm); }
+.btn.is-focus { outline: 2px solid var(--spin-accent); outline-offset: 2px; }
+.btn:disabled { opacity: .45; cursor: not-allowed; }
+.tip { position: relative; display: inline-flex; }
+.tip > span { position: absolute; bottom: calc(100% + 6px); left: 50%; transform: translateX(-50%); white-space: nowrap; font: 12px var(--spin-font-sans); color: var(--spin-text); background: var(--spin-overlay); border: 1px solid var(--spin-border); border-radius: var(--spin-radius-sm); padding: 3px 8px; box-shadow: var(--spin-shadow-overlay); }
+.matrix { display: grid; grid-template-columns: 7rem repeat(5, auto); gap: .75rem 1rem; padding: 4px; align-items: center; justify-content: start; font: 12px var(--spin-font-mono); color: var(--spin-muted); overflow-x: auto; }
+.matrix > b { font-weight: 500; }
+.uses { display: grid; grid-template-columns: repeat(auto-fit, minmax(16rem, 1fr)); gap: 1rem; margin-top: 1.25rem; }
+.use { margin: 0; border: 1px solid var(--spin-border); border-radius: var(--spin-radius); padding: 1rem; background: var(--spin-surface); }
+.use .row { margin-top: .6rem; align-items: center; gap: .5rem; }
+.use figcaption { padding: 0; }
.doc { display: grid; grid-template-columns: var(--spin-sidebar) 1fr 15rem; border-top: 1px solid var(--spin-border); }
.doc nav { background: var(--spin-surface); padding: 1rem; font-size: 14px; border-right: 1px solid var(--spin-border); }
.doc nav a { display: block; color: var(--spin-text); text-decoration: none; padding: .3rem .6rem; border-radius: var(--spin-radius-sm); min-height: var(--spin-target); }
@@ -114,30 +174,31 @@
.doc aside { padding: 1rem; font-size: 13px; border-left: 1px solid var(--spin-border); color: var(--spin-muted); }
pre { background: var(--spin-raised); border: 1px solid var(--spin-border); border-radius: var(--spin-radius-sm); padding: .9rem 1rem; overflow-x: auto; font: 14px/1.55 var(--spin-font-mono); color: var(--spin-text); }
code { font-family: var(--spin-font-mono); }
+p code { white-space: nowrap; }
p code, td code { background: var(--spin-raised); border: 1px solid var(--spin-border); border-radius: 4px; padding: 0 .3em; font-size: .9em; }
.aside { border-left: 3px solid var(--spin-warning); background: var(--spin-raised); padding: .75rem 1rem; border-radius: var(--spin-radius-sm); }
-table { width: 100%; border-collapse: collapse; font-size: 14px; }
+table { width: 100%; border-collapse: collapse; font-size: 14px; font-variant-numeric: var(--spin-numeric); }
th, td { text-align: left; padding: .55rem .6rem; border-bottom: 1px solid var(--spin-border); }
th { color: var(--spin-muted); font-weight: 500; }
/* The dashboard */
.dash { display: grid; grid-template-columns: 14rem 1fr; border: 1px solid var(--spin-border); border-radius: var(--spin-radius-lg); overflow: hidden; font-size: var(--spin-text-dense); line-height: 1.45; }
.dash nav { background: var(--spin-surface); padding: 1rem .75rem; border-right: 1px solid var(--spin-border); }
-.dash nav .cap { font: 600 11px var(--spin-font-mono); color: var(--spin-muted); letter-spacing: .08em; text-transform: uppercase; margin: 1rem .5rem .4rem; }
+.dash nav .cap { font: 500 12px var(--spin-font-sans); color: var(--spin-muted); margin: 1rem .5rem .4rem; }
.dash nav a { display: block; padding: .35rem .5rem; border-radius: var(--spin-radius-sm); color: var(--spin-text); text-decoration: none; }
-.dash nav a.current { background: var(--spin-accent-selected); color: var(--spin-accent-text); }
+.dash nav a.current { background: var(--spin-raised); color: var(--spin-text); }
+.dash h3 { font-family: var(--spin-font-sans); letter-spacing: 0; }
.dash main { padding: 1.25rem 1.5rem; }
.dash .tools { display: flex; gap: .5rem; align-items: center; margin: .75rem 0 1rem; flex-wrap: wrap; }
.dash input { font: inherit; color: var(--spin-text); background: var(--spin-raised); border: 1px solid var(--spin-border-strong); border-radius: var(--spin-radius-sm); min-height: 34px; padding: 0 .6rem; min-width: 14rem; }
.dash input::placeholder { color: var(--spin-muted); }
-.dash .btn { min-height: 34px; border-radius: var(--spin-radius); font-size: 14px; }
-.state { display: inline-flex; align-items: center; gap: .4rem; font: 500 12px var(--spin-font-mono); }
+.state { display: inline-flex; align-items: center; gap: .4rem; font: 400 1em var(--spin-font-mono); }
.state::before { content: ''; width: 8px; height: 8px; border-radius: 50%; background: currentColor; }
.state.ok { color: var(--spin-success); } .state.warn { color: var(--spin-warning); } .state.err { color: var(--spin-error); } .state.idle { color: var(--spin-muted); }
.state.warn::before { border-radius: 1px; transform: rotate(45deg); } .state.err::before { border-radius: 0; }
.panel { background: var(--spin-raised); border: 1px solid var(--spin-border); border-radius: var(--spin-radius); padding: 1rem; }
-.dialog { background: var(--spin-overlay); border: 1px solid var(--spin-border); border-radius: var(--spin-radius-lg); padding: 1.25rem; max-width: 26rem; box-shadow: 0 16px 40px rgb(0 0 0 / .35); }
-.toast { display: inline-flex; gap: .6rem; align-items: center; background: var(--spin-overlay); border: 1px solid var(--spin-border); border-left: 3px solid var(--spin-success); border-radius: var(--spin-radius); padding: .6rem .9rem; }
+.dialog { background: var(--spin-overlay); border: 1px solid var(--spin-border); border-radius: var(--spin-radius-lg); padding: 1.25rem; max-width: 26rem; box-shadow: var(--spin-shadow-overlay); }
+.toast { display: inline-flex; gap: .6rem; align-items: center; background: var(--spin-overlay); border: 1px solid var(--spin-border); border-radius: var(--spin-radius); padding: .6rem .9rem; box-shadow: var(--spin-shadow-overlay); }
.row { display: flex; gap: 1rem; flex-wrap: wrap; align-items: flex-start; margin-top: 1rem; }
/* Widths */
@@ -176,7 +237,7 @@
-
Spinbox
+
Spinbox
Spinbox brand system
One palette, one mark and one type for the site, the dashboard and everything else that draws Spinbox - each color checked here, and copied from here by each of them. This page is the laboratory; the product is at the website.
@@ -188,7 +249,7 @@
Spinbox brand system
@@ -199,16 +260,16 @@
Spinbox brand system
The site
- SpinboxSearch
+ SpinboxSearch
Spinbox is experimental: APIs change and data may be lost.Status lists what is built and what proves it.
spin-stack · self-hosted sandboxes
A virtual machine per workspace. A snapshot before, a diff after.
Sandboxes for people and agents that run in your own AWS account.
It goes to the trash with its disk; you can undelete it until its days are up.
+
Delete agent-1?
It goes to the trash with its disk; you can undelete it until its days are up.
done Snapshot clean published
@@ -272,7 +333,7 @@
Clear space
A quarter of the mark's height on every side, dashed above: no text or edge inside it.
Lockup
-
Spinbox
+
Spinbox
The mark, then Spinbox in IBM Plex Mono 600 - the word the prose uses, capitalised, its cap height the mark's middle disk.
Not
@@ -285,6 +346,61 @@
Not
+
+
+ Buttons
+
+
One primary action per view, in the accent and in words alone. A row's actions are quiet: its likely one as a secondary button and the rest behind ⋯. An icon goes alone, with a tooltip, where a toolbar has no room for words - never an icon and a word on every row. What destroys is in the destructive fill, in a confirmation, after Cancel. Every state here is a token (--spin-accent-fill-hover, --spin-control, …) that task palette derives and holds: a white label at 4.5:1 on a fill under the pointer and pressed, a hover told from rest.
A row and a toolbar take small; a page's header medium; a dialog's footer and the site large. 44 on a touch screen.
+
+ A page's header
+ A row
apirunning
+ A toolbar, icons alone
Refresh
+ A dialog's footer
+
+
+
+
+
+
+ States
+
+
A state is a badge: its shape, its word in the mono, on its own tint (--spin-success-badge and the rest, tint.badge over whatever is under it). The word holds 4.5:1 on its tint over the page, a surface and a raised fill; the shape tells it apart where the hue does not. The same badge in a table, a header and a toast.
+
runningstartingwaiting for a hostfailedstopped
+
+
+
+
+ Fields
+
+
Lowercase, digits and dashes.
+
Focused: the accent's ring, the edge in the accent.
The name does not match agent-1.
+
Disabled: set by the installation.
An input's edge is border-strong, 3:1 on the page (WCAG 1.4.11); 16 px on a touch screen, since iOS zooms into anything smaller.
+
+
+
+
+ Tables
+
+
Name
State
Image
CPU
Memory
Disk
Updated
+
+
api
running
sandbox:v20261014.01
4
8.0 GB
50 GB
2 min ago
+
agent-1
waiting
sandbox:v20261014.01
2
4.0 GB
20 GB
9 min ago
+
ci-job
failed
acme/builder:2026.10
16
32.0 GB
100 GB
1 h ago
+
notes
stopped
sandbox:v20261014.01
1
0.5 GB
10 GB
yesterday
+
+
+
Headers in the muted sans, never uppercase; ids in the mono; figures right-aligned in tabular digits (--spin-numeric); agent-1 under the pointer, ci-job selected. On a phone a table scrolls inside its frame, the page does not.
+
+
Compositions
@@ -295,6 +411,10 @@
Documentation
A Starlight page: one column at the reading measure, the sidebar at --spin-sidebar, headings in the mono. A claim links to the row of the status page that proves it.
Application
The dashboard, read for diagnosis. A page grows only as wide as what it shows: a document or a form to --spin-width-contained, one thing and what is around it to --spin-width-detail, a table of many to --spin-width-wide, an explorer to the viewport. A page is a header - the title, its state, its context and its one primary action on one row - and sections of rows between hairlines; the name beside the mark is --spin-wordmark, which the catalog audit holds the sidebar to.
+
Actions
+
One primary action per view, solid in the accent and in words alone: New workspace on a list, Open terminal on a workspace. A row's actions are quiet: its most likely one as a secondary button (the raised fill, a hairline edge, no icon) and the rest behind ⋯. An icon goes alone, with a tooltip, only where the toolbar has no room for words; never an icon and a word on every row. What destroys is in the destructive fill, in a confirmation, after Cancel.
+
Elevation
+
A menu, a dialog or a toast is on overlay with a hairline edge and --spin-shadow-overlay: a soft shadow in light; in dark only a crisp edge, since a black shadow on a near-black scrim drew a second rectangle around the dialog. The scrim covers the whole viewport.
diff --git a/logos/mark-dark-256.png b/logos/mark-dark-256.png
new file mode 100644
index 0000000..aa66e8b
Binary files /dev/null and b/logos/mark-dark-256.png differ
diff --git a/logos/mark-dark-512.png b/logos/mark-dark-512.png
new file mode 100644
index 0000000..b7e6507
Binary files /dev/null and b/logos/mark-dark-512.png differ
diff --git a/palette.toml b/palette.toml
index f38a98f..ecc9fd4 100644
--- a/palette.toml
+++ b/palette.toml
@@ -5,72 +5,91 @@
# terminal page, and anything else that reads dist/tokens.json. THEMING.md says where each goes.
#
# The colors come from the spin-stack mark: a stack of disks - the layers a workspace's disk is
-# made of - in cyan and blue on navy, with a mint ring. Dark is the default: the mark's own ground.
-# Light keeps the hues and darkens them until text in them reads on white.
+# made of - in cyan and blue on navy, with a mint ring. The interface is navy ink with the cyan,
+# deepened, as its one accent, used sparingly. A royal blue in its place read as any SaaS; navy and
+# cyan are what read as Spinbox. Dark is the default.
[brand]
-navy = "#111827" # the mark's ground, as the avatar has it; the dark page
-cyan = "#27BFE8" # the mark's middle disk; the accent in dark mode
-blue = "#2F6BE3" # the mark's lower disk; drawings only, never text on navy
-deep = "#0C3C84" # the mark's base; drawings, and light mode's strongest accent
-mint = "#5FE3D6" # the ring; labels that name a thing in dark mode (an eyebrow, a command
+navy = "#111827" # the mark's ground, as the avatar and the favicon's tile have it; not a surface
+cyan = "#27BFE8" # the mark's middle disk; the accent is this cyan, deepened until white reads on it
+blue = "#2F6BE3" # the mark's lower disk; drawings only
+deep = "#0C3C84" # the mark's base; drawings only
+mint = "#5FE3D6" # the ring; drawings only (an eyebrow in mint competed with every state
# in a drawing), never a surface
frost = "#EAF0F6" # the mark's top disk; text on navy
# Surfaces, darkest to lightest in dark mode and lightest to darkest in light: page (behind
# everything), surface (navigation and sidebars, a step off the page), raised (cards, code,
-# inputs), overlay (menus, dialogs). Grays carry the navy's hue (~222), so a surface and the mark
-# are one material, not cyan on a stock grey.
+# inputs), overlay (menus, dialogs). Dark is the mark's navy as ink: darker than the avatar's
+# (#0A0E16, L 6%), each step a clear lightness apart (the edge floor), the lines grey rather than
+# blue. The navy the dark page was before (#111827 to #273552 in steps of 4%) blended its planes,
+# and its blue-grey borders were louder than they were; the hue was never the problem.
[dark]
-page = "#111827"
-surface = "#172035"
-raised = "#202C47"
-overlay = "#273552"
-border = "#33435F" # a line between two surfaces, and an input's edge: 3:1 is held on page
-border_strong = "#6A7EA0" # an input's edge where it must read as a control (WCAG 1.4.11)
-text = "#E6EDF6"
-muted = "#9AA9C2" # secondary text: held to 4.5:1 on every surface up to overlay
-accent = "#27BFE8" # the one primary action per view, solid; indicators; the focus ring
-accent_text = "#27BFE8" # links and labels in the accent
-on_accent = "#111827" # text on the solid accent
-label = "#5FE3D6" # the mint: a small label that names a section or a command
-success = "#3DDC97"
-warning = "#FFC857"
-error = "#F57AB4" # leans pink to magenta: a deutan tells it from success by its blue
-destructive = "#C2304F" # the fill of a destructive action, its label white
+page = "#0A0E16"
+surface = "#111723"
+raised = "#19212F"
+overlay = "#242E40"
+border = "#2D3749" # a line between two surfaces, and an input's edge: 3:1 is held on page
+border_strong = "#67738A" # an input's edge where it must read as a control (WCAG 1.4.11)
+text = "#E6EBF2"
+muted = "#9AA5B8" # secondary text: held to 4.5:1 on every surface up to overlay
+accent = "#1B7DB4" # the one primary action per view, solid; indicators; the focus ring
+accent_text = "#6CC6EE" # links and labels in the accent
+on_accent = "#FFFFFF" # text on the solid accent
+label = "#8FB3C6" # a slate: a small label that names a section, quieter than a state
+success = "#4CC38A"
+warning = "#E3B04B"
+error = "#EE7BB0" # leans pink to magenta: a deutan tells it from success by its blue
+destructive = "#C4384F" # the fill of a destructive action, its label white
[light]
page = "#FFFFFF"
-surface = "#F0F4F9"
-raised = "#DEE6F1"
+surface = "#F1F4F9"
+raised = "#E1E7F0"
overlay = "#FFFFFF"
-border = "#B9C6D8"
-border_strong = "#6E7D96"
+border = "#C4CCD8"
+border_strong = "#77829A"
text = "#111827"
-muted = "#4A5871"
+muted = "#525D72"
accent = "#0A6FA3" # the cyan darkened until white text on it, and it on white, read
-accent_text = "#085E8C"
+accent_text = "#075A86"
on_accent = "#FFFFFF"
-label = "#0C3C84" # the mark's deep blue: mint does not read on white
-success = "#1E7046"
-warning = "#8A5A00"
-error = "#9B1B73" # magenta-leaning, as dark's: a deutan tells it from success by its blue
-destructive = "#B4234A"
+label = "#33586E" # the slate, darkened to read on white
+success = "#0F6844"
+warning = "#845000"
+error = "#9A1F60" # magenta-leaning, as dark's: a deutan tells it from success by its blue
+destructive = "#C0303F"
# The accent over a surface, for what is selected or hovered on the site (the current page in the
# sidebar, a hovered link): an alpha, so one value serves every surface. The label on a tint is
# accent_text. The dashboard keeps the accent for its one primary action and the focus ring: its
# hover and its navigation's selection are neutral, the surface a step off the page. The scrim is
-# the navy behind a dialog, the only dim there is.
+# the dark page behind a dialog, the only dim there is.
[tint]
hover = 0.10
-selected = 0.22
-scrim = 0.6
+selected = 0.27 # cyan over navy is close in hue: a stronger tint keeps a selection told apart
+scrim = 0.64
+# A solid fill under the pointer: black over it at this alpha, and twice it when pressed. Darker in
+# both themes, since the label on the accent and on the destructive is white.
+fill = 0.12
+# A state's badge (running, waiting, failed): the state's colour as a tint under its own text, so a
+# state reads as a thing and not as coloured words. Its text holds 4.5:1 on it over every surface.
+badge = 0.12
-# Type. IBM Plex, served by the site that uses it: a sans for reading and a mono for what a
-# machine says - commands, output, ids, a state, a time - and for the site's headings, which set
-# Spinbox in its console's face. The dashboard's page titles are sans; a title that names a thing
-# (a workspace, a runner) is its identifier, in mono.
+# Elevation: what lifts a menu, a dialog or a toast off what is under it. In light, a soft shadow
+# under a white plane. In dark, a shadow is black on near-black: under a scrim it drew a darker
+# rectangle around the dialog that read as a second, smaller overlay. There the plane's own
+# lightness (overlay) and its border lift it, and the shadow is only a crisp edge.
+[elevation]
+light = "0 12px 32px rgb(16 19 24 / 0.14), 0 2px 6px rgb(16 19 24 / 0.08)"
+dark = "0 0 0 1px rgb(0 0 0 / 0.4)"
+
+# Type. IBM Plex, served by the site that uses it: a sans for reading and every heading, and a mono
+# for what a machine says - commands, output, ids, an image, a state, a time. A heading in the mono
+# at 52 px read as a terminal (the November council, all three reviewers): the mono is for what is
+# copied, the sans for what is read. A title that names a thing (a workspace, a runner) is its
+# identifier, in mono. Mono beside sans is set at the sans' size, never smaller: an id or a state
+# at 11 px beside a 13 px name was the least legible thing on every screen.
[type]
sans = "IBM Plex Sans"
mono = "IBM Plex Mono"
@@ -84,7 +103,13 @@ dense_size = 13 # px: the dashboard's body, where a table of runners is
dense_scale = { xs = [12, 16], sm = [13, 20], base = [14, 22], lg = [16, 24], xl = [18, 26], "2xl" = [20, 28], "3xl" = [24, 32] }
line_height = 1.6 # reading; dense text takes 1.45
heading_weight = 600
-heading_tracking = -0.02 # em: mono headings are wide; this tightens them to read as one word
+heading_tracking = -0.02 # em: a large sans set tight reads as one word
+heading = "sans" # the face of every heading, the site's included
+# Headings from a phone's width (layout.phone) to a desktop's (layout.desktop), [px there, px here]:
+# a clamp() in between, so a title neither shouts on a phone nor whispers on a monitor.
+fluid = { display = [32, 52], h1 = [26, 36], h2 = [21, 26], h3 = [17, 20] }
+# Figures in tables and meters line up: every digit the same width.
+numeric = "tabular-nums"
# The name beside the mark, and in every header: the word the prose uses, since a lowercase mark
# beside a capitalised product was two spellings of one name. The dashboard's catalog audit holds
# its sidebar to this; the website's reference holds its title to it.
@@ -95,7 +120,7 @@ wordmark = "Spinbox"
[layout]
measure = 72 # ch: a line of reading text; 45-75 is the range people read without losing it
sidebar = 17 # rem
-radius_sm = 4 # px: inputs, chips, code; every corner of the dashboard, dialogs included
+radius_sm = 6 # px: inputs, chips, code; every corner of the dashboard, dialogs included
radius = 8 # px: the site's cards, buttons, panels
radius_lg = 12 # px: the site's dialogs and hero figures
target = 24 # px: the least a pointer target is (WCAG 2.2, 2.5.8)
@@ -112,6 +137,17 @@ width_contained = 1200 # px
width_detail = 1152 # px
width_wide = 1600 # px
+# Controls: a button's or an input's height, small (a row's action, a toolbar), medium (a form, a
+# page's header) and large (a dialog's footer, the site); the icon in it and the gap to its word.
+# 44 px on a coarse pointer is layout.target_touch. One rule for words and icons: a primary action
+# is words alone; an icon goes alone, with a tooltip, where a toolbar has no room for words.
+[controls]
+sm = 28
+md = 32
+lg = 36
+icon = 16
+gap = 6
+
# Motion. A movement says what something does - a layer sealed on the one under it, a diff drawn
# between two - or that it answered a hand; one that only decorates is left out. Each runs once:
# nothing moves on its own for longer than 5 s (WCAG 2.2.2), and reduced motion asked for is
diff --git a/ui-hill-climb/AUDIT.md b/ui-hill-climb/AUDIT.md
new file mode 100644
index 0000000..756ba19
--- /dev/null
+++ b/ui-hill-climb/AUDIT.md
@@ -0,0 +1,70 @@
+# Audit — round 0 (brand at 7e17029)
+
+Scope: the brand system as `palette.toml` → `dist/` draws it: the app specimen
+(`ui-hill-climb/app.html`, built only from `dist/dashboard.css` the way spin's dashboard uses it),
+and the laboratory's site, dashboard and palette sections. Screens are in `rounds/00/`. The real
+app's catalog (spin v20261019.02) shows the same problems; the specimen reproduces them.
+
+The owner's words: "not only is it square, the tones are so strong there is no contrast"; Beam's
+dark mode "can be understood"; even boxd's is better.
+
+## Colour and contrast (the main problem)
+
+- **Every surface is saturated navy.** page/surface/raised/overlay are hue 221 at S 36–39%
+ (#111827 → #273552). Beam's are S ≈ 6%. A saturated dark ground tints every grey, every border
+ and the text on it blue, so nothing reads as neutral and the eye has no rest (app-list-dark,
+ app-detail-dark, site-dark, dashboard-dark).
+- **The accent is a solid, very bright cyan (#27BFE8, S 81%) with dark text**, on every primary
+ action and on each row (Open terminal ×2 + Start + New workspace on app-list-dark: four glowing
+ blocks). It is the loudest thing on every screen, louder than the data. Beam's fill is a
+ mid blue with white text, used once per view.
+- **Borders that should separate shout instead**: `--input` is `border_strong` #6A7EA0, a
+ blue-grey that every secondary button and every input wears (Stop, View logs, Change size,
+ Cancel, the search field). Next to the cyan, the row of actions reads as a row of boxes.
+- **Mint labels** (#5FE3D6, S 70%) for eyebrows and section labels: a third saturated hue
+ competing with cyan and the status colours (site-dark "THE SITE", "ON THIS PAGE", dashboard-dark
+ "WORK").
+- **Status colours are neon on navy** (#3DDC97, #FFC857, #F57AB4): legible, but at the same
+ intensity as the accent, so state does not stand apart from action.
+- **Low perceived contrast despite passing ratios**: the surface steps (page → surface → raised)
+ are all hue-shifted navy, so cards and code blocks blend into the page (site-dark's `pre`
+ blocks, the dialog on dashboard-dark), while the borders are louder than the surfaces they
+ bound. The hierarchy of planes is inverted: lines are loud, planes are flat.
+- **Light mode**: raised #DEE6F1 and surface #F0F4F9 are blue-tinted greys; the code block on
+ app-detail-light reads as a pale-blue slab. The accent #0A6FA3 teal-blue is fine; border_strong
+ #6E7D96 again outlines every secondary button heavily.
+- The scrim behind a dialog is the navy at 60%: blue-tinted dim.
+
+## Geometry
+
+- **Every corner of the app is 4 px** (`--radius` = `radius_sm`), dialogs included. With heavy
+ borders, buttons and inputs read as hard boxes (app-list-dark actions, app-dialog-dark). Beam's
+ controls are ≈6 px, its groups ≈8–10 px; boxd's cards 12 px.
+- The site uses pill buttons (999 px) beside 4 px code blocks and 12 px frames: three geometries
+ on one page (site-dark hero vs. docs).
+
+## Hierarchy and typography
+
+- Page titles are fine in size, but on app-list-dark the cyan buttons out-rank the title and the
+ workspace names; the names (the content) are the same weight as the actions' labels.
+- Mono uppercase labels in mint with wide tracking are the most saturated text on the site.
+- The site's hero h1 in Plex Mono 600 at ~52 px is heavy; it is a decision of the site, kept for
+ now, but the colour around it makes it heavier.
+
+## Consistency
+
+- Selected navigation: in the dashboard it is neutral (raised), in the site sidebar a cyan tint
+ with cyan text (site-dark "Snapshots and rollback"); in the lab's dashboard sample cyan again.
+ Two rules for the same thing.
+
+## Accessibility (measured, `task report`)
+
+- All 100 pairs hold their floors today. The redesign must keep every floor (body text 7:1,
+ secondary 4.5:1, input edge and focus ring 3:1, the ΔE00 distances in every view). Lowering
+ saturation must not be bought with lower contrast.
+
+## Generic-UI tells
+
+- None of the usual (gradients, glass, shadows everywhere). The problem is the opposite of
+ generic: an identity carried by saturating everything, instead of by a few marks on a neutral
+ ground.
diff --git a/ui-hill-climb/DIRECTION.md b/ui-hill-climb/DIRECTION.md
new file mode 100644
index 0000000..94faa22
--- /dev/null
+++ b/ui-hill-climb/DIRECTION.md
@@ -0,0 +1,47 @@
+# Direction — round 1
+
+The identity moves from "everything is navy and cyan" to "a neutral, quiet ground with the mark's
+blue used rarely". The mark keeps its own colours; the interface stops repeating them everywhere.
+
+## Colour roles
+
+- **Surfaces neutral, faintly cool.** Hue ~215, saturation ≤ 15% (was 39%). Dark:
+ page #0E1013 → surface #171A20 → raised #21252C → overlay #2A2F37. Light: page #FFFFFF →
+ surface #F0F2F5 → raised #E0E4EA. Each step holds ΔE00 ≥ 3 (the `edge` floor), so planes read as
+ planes without a border having to say so.
+- **Borders quiet.** `border` is a hairline one step above the surface it is on (#373E48 dark,
+ #C3CAD3 light); `border_strong` (an input's edge, 3:1) is a neutral grey (#6A7280 / #7A828E),
+ not a blue-grey.
+- **One accent, the mark's cyan deepened, as a fill with white text** (#1B7DB4 dark, #0A6FA3
+ light). Round 1 tried the mark's royal blue: two of three reviewers read it as generic SaaS and
+ the cyan as what made it Spinbox. Links are a lighter/darker tint of the same cyan (#6CC6EE /
+ #075A86). The accent is the primary action, the focus ring, the current tab: scarce by rule.
+- **Labels are a desaturated slate** (#8FB3C6 / #33586E), not mint: a label names, it does not
+ compete with state.
+- **States softened, still apart** (ΔE00 ≥ 20 drawn, ≥ 10 in every deficiency): success #4CC38A,
+ warning #E3B04B, error #EE7BB0 (magenta-leaning so deutans tell it from success), destructive
+ fill #C4384F. Shape still carries state (dot, diamond, square).
+- **Contrast is held, not traded**: text 7:1 on every surface, secondary 4.5:1, input edge and
+ focus 3:1, in both modes.
+
+## Geometry
+
+- Controls 6 px (`radius_sm`), cards and panels 8 px (`radius`), dialogs and hero figures 12 px
+ (`radius_lg`). The dashboard's single `--radius` becomes 6 px: still one radius for a dense
+ console, no longer a hard box.
+
+## Type, spacing, density, motion
+
+Unchanged in this round: IBM Plex Sans/Mono, the dense scale, the measure, targets and motion
+tokens. The change is colour and corners; if the council says hierarchy still suffers, type is
+the next lever.
+
+## Light and dark
+
+Both first-class and derived from the same roles. Dark is a neutral near-black (not navy), light a
+white page with neutral greys (not blue-greys).
+
+## Order of propagation
+
+brand (`palette.toml` → `dist/`, the laboratory) → website (Starlight copy) → the app (spin's
+brand.css copy). This branch changes brand only.
diff --git a/ui-hill-climb/ROUNDS.md b/ui-hill-climb/ROUNDS.md
new file mode 100644
index 0000000..d9c97a0
--- /dev/null
+++ b/ui-hill-climb/ROUNDS.md
@@ -0,0 +1,52 @@
+# Rounds
+
+## 00 — baseline (7e17029)
+Navy surfaces at S 39%, cyan accent with dark text, mint labels, 4 px everywhere.
+Council (claude, codex, gemini × 2 passes): median 6.0, weakest responsive 4, noise 0.5. All three
+name the saturated navy, the accent on every row and the blue-grey light mode.
+
+## 01 — neutral ground, the mark's blue as accent, 6 px controls
+palette.toml: surfaces neutral (S ≤ 15%), borders quiet, accent #356BDE/#2F64D8 with white text,
+labels slate, states softened; radius_sm 4 → 6; scrim from the dark page; hero on the clear mark.
+All 100 pairs hold.
+
+## 02 — selection and the lab's sample follow the app
+Neutral hover/selection is `raised` in both themes (light's selected item was invisible); scrim
+0.6 → 0.5 (0.64 in 06: on navy the dialog needed more of it); the lab's dashboard sample: neutral selection, sans title, Cancel before Delete; ids
+and states do not wrap.
+Council round 1 (on 01's capture): median 7.0 (+1.0), win 40%, but noise 3.5 (control drift).
+Claude and Gemini took the neutral ground in dark; Codex and Claude in light read the royal blue
+as generic SaaS. The `nowrap` on ids clipped the lab's narrow table. Gemini's batch b1 did not parse.
+
+## 03 — the accent back to the mark's cyan, buttons, elevation
+Accent is the cyan deepened to carry white (#1B7DB4 dark, #0A6FA3 light), links #6CC6EE/#075A86;
+selected tint 0.22 → 0.24 to keep its floor. The owner: the dialog's area showed in dark, and an
+icon + word on every row was too much. New `[elevation]` (`--spin-shadow-overlay`,
+`--shadow-overlay`): a soft shadow in light, only a crisp edge in dark; dialogs on `popover`
+(overlay). The specimen and the lab follow the brand's action rule: one primary per view in words
+alone, row actions secondary (raised, hairline) plus `⋯`. nowrap reverted.
+Council round 3: pairs won by claude 12/12, gemini 10/12 (2 ties), codex 3/12 (8 for the
+navy-and-cyan, for identity). Median 7.0. Remaining brand findings: light secondary fills look
+disabled, light code a slab, dark planes too close, failed pink reads friendly.
+
+## 04 — planes apart, light steps lighter
+Dark surface/raised/overlay one step further apart (#171A20, #21252C, #2A2F37), border #373E48,
+border_strong #6A7280 to hold 3:1 on the new raised; error #EE7BB0 (a red fails deutans against
+success, so magenta stays). The dashboard's `muted`/`secondary` are the surface in light.
+Council round 4 (claude, codex; gemini out of quota): claude 8-2 (2 ties, the two "losses" with
+reasons that describe the candidate), codex 6-5. The owner: navy+cyan is the identity, the
+problem was contrast - not to be dropped.
+
+## 05 — B, navy ink; buttons are brand's
+The owner chose B: the navy darkened to #0A0E16, steps apart, grey lines. [controls] (28/32/36,
+icon, gap) and tint.fill: hover and pressed fills derived and held (120 pairs). Lab: Buttons.
+
+## 06 — type system, states, fields, tables, the mark on dark
+Font council (claude, codex, 3 screens × 3 matchups at DPR 1): no family wins (Inter–Plex 3-3,
+Inter–Geist 4-2, Plex–Geist 4-1); both name the system - 11 px mono states, non-tabular figures,
+a 52 px mono headline, uppercase mono labels, broken inline code. Headings are sans
+(type.heading), fluid from 390 to 1280 px (type.fluid, also Starlight's --sl-text-h*), figures
+tabular, labels sentence case, mono at the size of the text beside it. A state is a badge on its
+own tint (tint.badge, held at 4.5:1 over page, surface and raised; light states darkened, success
+toward teal for protans): 144 pairs hold. Lab: States, Fields, Tables. logos/mark-dark-*: the
+clear mark with its shadow drawn for the dark page.
diff --git a/ui-hill-climb/app.html b/ui-hill-climb/app.html
new file mode 100644
index 0000000..75633c9
--- /dev/null
+++ b/ui-hill-climb/app.html
@@ -0,0 +1,190 @@
+
+
+
+
+
+Spinbox app specimen
+
+
+
+
+
+
+
+
+
+
+
Workspaces
Your cloud development environments
+
+
+
+
+ Running 2
+ Stopped 1
+ Failed 1
+
+
+
apirunningghcr.io/spin-stack/spinbox/sandbox:v20261014.014 CPU · 8 GB · 50 GBUpdated 2m ago⋯
+
agent-1runningghcr.io/spin-stack/spinbox/sandbox:v20261014.012 CPU · 4 GB · 20 GBUpdated 9m ago⋯
+
ci-jobfailedghcr.io/acme/builder:2026.108 CPU · 16 GB · 100 GBUpdated 1h ago⋯
+
notesstoppedghcr.io/spin-stack/spinbox/sandbox:v20261014.011 CPU · 0.5 GB · 10 GBUpdated yesterday⋯
spin ssh api
+spin snapshot create api --description clean --wait
+
+
+
+
+
Delete agent-1?
+
It goes to the trash with its disk; you can undelete it until its days are up.
+
+
+
The name does not match agent-1.
+
+
+
+
done Snapshot clean published
+
+
+
+
+
diff --git a/ui-hill-climb/ledger.jsonl b/ui-hill-climb/ledger.jsonl
new file mode 100644
index 0000000..0f2ba70
--- /dev/null
+++ b/ui-hill-climb/ledger.jsonl
@@ -0,0 +1,16 @@
+{"round": 0, "reviewer": "claude/sonnet", "kind": "full", "pass": 1, "scores": {"hierarchy": 6, "typography": 7, "spacing": 6, "color": 6, "consistency": 6, "density": 6, "usability": 6, "polish": 6, "responsive": 4, "identity": 7}, "control": {"hierarchy": 6, "typography": 7, "spacing": 6, "color": 6, "consistency": 6, "density": 6, "usability": 6, "polish": 6, "responsive": 4, "identity": 7}, "pairs": [], "findings": [{"screen": "c01", "location": "Workspaces list rows, centre column", "severity": "structural", "problem": "The image column has no fixed width and is not aligned. ci-job and notes images sit at different x positions than api and agent-1, and the specs and Updated columns also drift. The list reads as ragged floating text rather than a table. There are no column headers.", "fix": "Use a real grid with fixed columns (name+state, image, specs, updated, actions) and column headers. Truncate long image names with an ellipsis and a tooltip."}, {"screen": "c01", "location": "Row actions: Open terminal, Stop, VS Code", "severity": "structural", "problem": "Every running row has a filled cyan primary button, so four rows give four competing accent blocks. Row buttons are as loud as the page's New workspace CTA. The accent stops meaning one primary action per view. Stop is an outline with the same weight as View logs, so a destructive-ish action is not distinguished from navigation.", "fix": "Make row actions quiet: ghost or outline buttons, with the accent reserved for New workspace. Put Stop and Delete behind a row menu, or give them a danger-tinted hover state."}, {"screen": "c01", "location": "Header icon buttons and the filter bar", "severity": "cosmetic", "problem": "The state counts (Running 2, Stopped 1, Failed 1) look like passive labels and not filters. They have no selected state or button affordance. The 'waiting' state is missing from the summary. The search input has the same weight as the rows.", "fix": "Make the counts toggle chips with active and hover states, and add a waiting count. Give the search field a distinct inset surface."}, {"screen": "c01", "location": "Row state markers", "severity": "systemic", "problem": "State is a 6px dot or square plus small mono text. In c01 the stopped ring is the same grey as the body text and the failed pink is faint. The shapes are tiny and read as decoration. In c05 the dimmed state colours lose contrast. The waiting diamond never appears in the app list, only in c09.", "fix": "Enlarge the markers to about 8\u201310px and set the state label in a medium-weight small-caps style. Use the same four shape-and-colour pairs in every screen, including the app list."}, {"screen": "c03", "location": "Overview page: Details and Resources tables, Connect card", "severity": "structural", "problem": "The key/value tables and the Connect card all use the same hairline-and-outline treatment, so contextual detail, content and operations sit at the same level. The Connect card is a bordered box containing another box, which is nested chrome. The tab strip, tables and card have nearly equal weight, and a lot of empty space is left below.", "fix": "Move Connect into a flat, code-only block with a copy affordance. Drop the nested border. Give section headings a stronger style, and put the status and metrics (CPU and memory use) in a summary band under the title."}, {"screen": "c03", "location": "Header: api title and action buttons", "severity": "cosmetic", "problem": "The title is small for a page title. The state, image and actions float on different baselines. VS Code is a bare text link with no affordance, and the Delete action is absent from the detail page, so the destructive action is hidden.", "fix": "Align the actions on one baseline. Give VS Code a ghost button style. Put Delete in a separated danger zone or a '\u2026' menu in the header."}, {"screen": "c05", "location": "Delete dialog", "severity": "cosmetic", "problem": "The dialog is a good model: red Delete, typed confirmation, inline error. But the Delete button is enabled while the typed name does not match. The destructive red (#C2304F) sits close to the failed pink, so 'failed' and 'destructive' share a hue family. The dialog surface barely separates from the dimmed page.", "fix": "Disable Delete until the name matches. Separate the destructive red from the failed colour (a deeper red against a lighter pink-red). Give the dialog a stronger elevation or border."}, {"screen": "c07", "location": "Mobile workspaces list at 390px", "severity": "structural", "problem": "The sidebar is gone, with no menu button or navigation visible. The running rows have a full-width cyan block that takes more room than the name. The failed row has no action, and the stopped row has a different button than the running ones. The image, specs and Updated columns are dropped, so the list loses most of its data.", "fix": "Design a compact card per row: name and state, then the image in mono, then a quiet action. Add a top bar with a menu button. Keep a Stop/Open overflow menu on every row."}, {"screen": "c08", "location": "Docs site: hero", "severity": "structural", "problem": "The hero headline is set in huge mono at around 56px with wide letter-spacing, so it reads as a poster and breaks awkwardly at 'per / workspace'. Meanwhile the glossy 3D logo illustration is stylistically alien to the flat, restrained UI. The site is framed inside a rounded bordered 'device' box with a 'THE SITE' label above it, which is lab-page chrome.", "fix": "Use a mono face only for short labels and code, and a proportional display face at lower weight for the headline. Replace the glossy mark with a flat or simplified version used consistently in the app."}, {"screen": "c08", "location": "Docs table and callout at the bottom of the article", "severity": "cosmetic", "problem": "The comparison table sits directly below the Caution callout with almost no space. It has no border or header treatment beyond a thin rule. The code blocks use the same surface as the caution, so a code block and a warning are hard to tell apart. The callout only has a thin amber left rule.", "fix": "Add 24px of space above the table. Give the table a header row on a tinted surface. Give the caution a distinct amber-tinted surface and a text label."}, {"screen": "c09", "location": "Dashboard lab: toast and dialog placement", "severity": "cosmetic", "problem": "The dialog and toast sit inline under the table as bordered boxes, so the page shows two unrelated systems. The table here has a Host and a Last snapshot column and underlined link names, which the app list (c01) does not use. The Delete/Cancel button order is reversed relative to c05. The list differs between c01 and c09 (names as underlined links versus bold text).", "fix": "Make c09 and the app list one component. Pick a single order (Cancel left, Delete right) and one name style."}, {"screen": "c11", "location": "Palette lab: accent and surfaces", "severity": "systemic", "problem": "The palette is a navy-slate surface stack (#111827, #172035, #202C47, #273552) with a saturated cyan (#27BFE8) and a saturated teal-mint label colour. The four surfaces are one hue and differ only slightly, so cards and page blur together. Hard-saturated green, amber and pink on navy look neon and generic-dashboard. The accent-text equals the accent. The success mint and the label teal are almost the same colour, so accent and success collide.", "fix": "Widen the lightness steps between surfaces or lean on borders. Lower the saturation of the state colours, and move the label colour to a neutral. Keep cyan for actions only and give the success green a different hue."}, {"screen": "c02", "location": "Light-mode workspaces list", "severity": "systemic", "problem": "The light theme is a straight inversion: white page, white sidebar, a pale blue-grey selected nav item that is nearly invisible. The fills of the filled primary buttons (dark teal-blue) are darker and heavier than in dark mode, and the four of them dominate the list. The thin grey hairlines are nearly all the structure there is.", "fix": "Give the light theme its own surface ladder: a tinted page and white raised rows, plus a clearly tinted sidebar and a visible selected-item treatment. Reduce the row buttons to quiet outlines."}, {"screen": "c04", "location": "Light-mode detail", "severity": "cosmetic", "problem": "The code block is a saturated pale blue-grey slab that is the strongest fill on the page. The 'Change' link inside the Network value, the URL link and the Open terminal button all use the same blue, so links and primary actions blur together.", "fix": "Use a neutral light grey for code. Distinguish links by underline and a different weight from filled buttons."}, {"screen": "c10", "location": "Light-mode dashboard lab at mobile width", "severity": "structural", "problem": "The table squeezes into a narrow width: the Last snapshot header wraps, host chips wrap mid-word (runner- / a1), and agent-1 wraps with a hyphen break. The waiting state wraps over three lines. The dialog floats over the table and covers it with a heavy shadow.", "fix": "At narrow widths, stack each row as a card: name and state on one line, host in mono on the next, and keep ids unbroken (white-space: nowrap or a smaller chip). Dock the dialog as a bottom sheet."}, {"screen": "c12", "location": "Palette lab: overall", "severity": "cosmetic", "problem": "The lab page is a long, flat sequence of swatches with the same card treatment for everything. It does not show contrast ratios, the light palette in the same view, or the type scale. Swatch labels are very small, low-contrast mono.", "fix": "Show each pair with its contrast ratio and a pass/fail mark, show the light and dark values side by side, and add a type specimen with the scale."}]}
+{"round": 0, "reviewer": "claude/sonnet", "kind": "full", "pass": 2, "scores": {"hierarchy": 6, "typography": 6, "spacing": 6, "color": 6, "consistency": 6, "density": 6, "usability": 6, "polish": 6, "responsive": 4, "identity": 6}, "control": {"hierarchy": 6, "typography": 6, "spacing": 6, "color": 6, "consistency": 6, "density": 6, "usability": 6, "polish": 6, "responsive": 4, "identity": 6}, "pairs": [], "findings": [{"screen": "c01", "location": "Workspaces list rows", "severity": "structural", "problem": "Rows are not a table. Image, resources and 'Updated' float at different x-positions on each row, because the columns are sized to their content. Row actions also change by state: Open terminal, Stop and VS Code on running rows, View logs on failed, Start on stopped. Scanning a column is impossible.", "fix": "Use fixed grid columns: name, state, image, resources, updated, actions. Right-align the actions and give every row the same action slots."}, {"screen": "c01", "location": "Every running row: Open terminal button", "severity": "systemic", "problem": "The bright cyan filled button repeats on every running row, plus New workspace and Start. Accent-filled buttons dominate the page and compete with the state colours. The brand's own rule is one primary action per view.", "fix": "Make row actions neutral outline or ghost buttons. Keep the filled accent for New workspace only, or for the single primary action in a detail view."}, {"screen": "c01", "location": "State cells and filter chips", "severity": "structural", "problem": "State is a 12px monospace word next to a tiny dot. It is easy to miss and dims further in the dark theme. The waiting state (amber diamond) appears in c09 but not in the list. The filter chips (Running 2, Stopped 1) look like plain text, not controls.", "fix": "Add a fixed-width State column with a larger glyph plus label. Show the waiting state in the list. Draw the filters as real segmented chips with a selected state."}, {"screen": "c01", "location": "Page chrome, top-right toolbar and sidebar", "severity": "cosmetic", "problem": "Two unlabeled square icon buttons sit next to the CTA. The sidebar is flat and has no sections, and the content area is mostly empty. The screen reads as generic SaaS next to Beam and boxd, which have breadcrumbs, org context and resource meters.", "fix": "Label or tooltip the toolbar buttons. Group the nav into sections. Add useful context to the sidebar and header, such as a host or quota summary."}, {"screen": "c03", "location": "Overview details and Connect block", "severity": "structural", "problem": "The label/value tables have hairlines only and no scale for values. The Connect card is a box inside a box, and its code block uses a barely different fill. The tab bar (Access, Status, History, Snapshots...) has little weight, and the open tab is marked by only a thin underline. The state is shown only in small text under the title.", "fix": "Put a state badge beside the title. Use one surface level per container. Strengthen the active tab. Add a copy affordance to the command block."}, {"screen": "c03", "location": "Header actions: Open terminal / Stop / VS Code", "severity": "structural", "problem": "Stop and Change size are neutral outlines, so a destructive or disruptive action does not look different from the safe ones. VS Code is a bare text link that looks like a different component. There is no Delete or Snapshot action in the detail header.", "fix": "Make the three actions one consistent button group. Move Delete into a separated danger zone or a menu, in the destructive red."}, {"screen": "c05", "location": "Delete dialog", "severity": "cosmetic", "problem": "The dialog is clear and the red Delete is distinct. But Delete is enabled while the typed name does not match. The pink error text is small and low in contrast. Cancel and Delete are packed tightly, with Cancel placed before the red button. The scrim is light enough that the list behind it stays loud.", "fix": "Disable Delete until the name matches. Use a larger error message with an icon. Darken the scrim. Put the destructive action at a consistent position."}, {"screen": "c06", "location": "Delete dialog, light mode", "severity": "systemic", "problem": "The scrim turns the light page grey-blue and the dialog is plain white with a hairline. Pink and magenta error text on white is weak. The list behind it is still fully readable and visually competes.", "fix": "Use a stronger scrim and an elevated dialog shadow. Check the error-text contrast on white."}, {"screen": "c07", "location": "Narrow viewport workspaces list", "severity": "structural", "problem": "Narrow layout drops image, resources and updated time entirely. Running rows show only a huge Open terminal button, and the failed row has no action. Two oversized cyan buttons dominate. The icon buttons, search and New workspace are stacked with no navigation visible and no way to open the menu.", "fix": "Design a compact card or row with state, name, image and one overflow menu. Add a mobile top bar with a menu button. Keep only one accent button on the page."}, {"screen": "c08", "location": "Docs site: hero and code blocks", "severity": "structural", "problem": "The hero heading is a very large monospace face. At display size it is wide and loose, so it reads as a gimmick and wraps awkwardly. Code blocks, the output block and the Caution callout all use the same raised fill, and the table beneath has no borders, so the page is a stack of equally weighted boxes. The caution left stripe is yellow in dark and brown in light.", "fix": "Set the headline in the proportional face with tighter tracking and keep mono for ids and commands. Differentiate code, output and callout by border or tint. Give the table a defined header."}, {"screen": "c08", "location": "Docs site: eyebrow labels and shell", "severity": "cosmetic", "problem": "The mint monospaced uppercase labels (THE SITE, ON THIS PAGE, SPIN-STACK) are a different green from the success state, so the label colour clashes with the state semantics. The embedded site frame has a rounded border that the product does not use. The hero illustration is glossy and photographic against an otherwise flat UI.", "fix": "Put labels in the muted text colour or in the accent colour. Drop the frame rounding. Flatten the illustration or tone it to the palette."}, {"screen": "c09", "location": "Dashboard component: table", "severity": "systemic", "problem": "This table has real columns, with Name, State, Host and Last snapshot. It contradicts the actual app list in c01, which has no columns, and the nav labels differ (Work/Administer here, flat in c01). Names are underlined links in cyan, so the page is cluttered with underlines. This component shows the waiting state, but the app does not.", "fix": "Bring the app to this structure and use one list pattern. Use an unlined link style that shows on hover and state-coloured rows."}, {"screen": "c09", "location": "Delete dialog and toast placed inline", "severity": "cosmetic", "problem": "The dialog and the toast sit directly beneath the table as static boxes. Cancel comes after Delete here but before it in c05. The toast has a mint left stripe and a monospaced message, so it looks like an error block.", "fix": "Show components in context. Fix the order of Cancel and Delete everywhere."}, {"screen": "c11", "location": "Laboratory: palette", "severity": "systemic", "problem": "Surfaces page, surface, raised and overlay differ by only a few lightness points on a navy base, so nested cards blend into one navy. Muted text #9AA9C2 is the only secondary tone, and it is used for many small labels. Border #33435F is faint. The accent cyan, the label mint, the success green and the selected teal are four close hues in the green-blue range. Pink for failure and the saturated red for destructive are close but different.", "fix": "Widen the surface steps and strengthen the border. Reserve cyan for actions and links only. Choose one distinct hue per state. Check the muted-on-surface contrast for small text."}, {"screen": "c01", "location": "Type scale across the app", "severity": "systemic", "problem": "The app uses a small body size (about 11-12px) for nearly everything: metadata, buttons, tabs, labels. Row names at 12px bold are barely above the metadata, and the mono ids and images are at 12-13px in grey. The title is 20px. There is little hierarchy between sections.", "fix": "Raise body to 13-14px. Set names at 14px semi-bold and image ids at the same size in mono. Use three clear text tiers."}, {"screen": "c04", "location": "Light mode accent and surfaces", "severity": "cosmetic", "problem": "Light mode is competent but flat: a white page, a light-grey sidebar, and a pale blue-grey code fill with a solid dark-blue accent button. The selected nav item is nearly invisible. The identity is generic and it lacks a point of view.", "fix": "Strengthen the selected nav state with an accent bar. Give light mode its own surface ladder rather than a copy of the dark one."}]}
+{"round": 0, "reviewer": "codex/gpt", "kind": "full", "pass": 1, "scores": {"hierarchy": 7, "typography": 7, "spacing": 8, "color": 7, "consistency": 8, "density": 7, "usability": 7, "polish": 7, "responsive": 6, "identity": 8}, "control": {"hierarchy": 7, "typography": 7, "spacing": 8, "color": 7, "consistency": 8, "density": 7, "usability": 7, "polish": 7, "responsive": 6, "identity": 8}, "pairs": [], "findings": [{"screen": "c01", "location": "Workspace list rows and surrounding canvas", "severity": "structural", "problem": "The table occupies a shallow strip at the top while the rest of the desktop canvas is empty, making an operational inventory feel under-utilized and reducing scan momentum.", "fix": "Constrain the workspace area to a deliberate content width and add meaningful adjacent or below-table operational context, or increase row density and provide a clear empty lower-state boundary rather than an unbounded canvas."}, {"screen": "c01", "location": "Workspace list column layout", "severity": "structural", "problem": "Rows have no visible column labels; image, resource, recency, and actions must be inferred from alignment alone.", "fix": "Add a compact table header with muted labels and retain it while scanning longer lists."}, {"screen": "c01", "location": "Sidebar and main content boundary", "severity": "systemic", "problem": "The sidebar is visually strong but its active blue surface is close to the main dark surface, while thin borders carry most separation.", "fix": "Increase surface separation slightly between navigation, page, and selected navigation state; preserve the restrained palette but make hierarchy less dependent on hairlines."}, {"screen": "c02", "location": "Light-mode workspace list", "severity": "systemic", "problem": "Light mode inherits cool blue-gray borders and fills nearly everywhere, which makes the page feel washed out and gives secondary structure similar weight to content.", "fix": "Use a warmer or more neutral page surface, reserve blue-gray fills for controls and code, and reduce border prominence for ordinary row separation."}, {"screen": "c03", "location": "Details and Resources sections", "severity": "structural", "problem": "The two-column data groups lack a stronger section-level distinction from the large Connect panel, so the lower panel competes with primary workspace facts.", "fix": "Tighten the Connect panel or give it a subtler treatment; establish a clearer heading-to-data rhythm for the two operational summary columns."}, {"screen": "c03", "location": "Network and Saved values", "severity": "cosmetic", "problem": "Inline action text such as \u201cChange\u201d and the long Saved sentence read as a continuous data value, weakening scannability.", "fix": "Style inline actions distinctly and constrain/structure verbose status values so labels, facts, and actions remain separable."}, {"screen": "c05", "location": "Delete confirmation dialog", "severity": "structural", "problem": "The dark modal blends closely with the dark app surface, while the backdrop is extremely dark; the destructive decision lacks a sufficiently isolated focal plane.", "fix": "Raise the modal surface and shadow separation, and tune the overlay so the dialog is unmistakably foreground without obscuring all contextual evidence."}, {"screen": "c06", "location": "Delete confirmation dialog", "severity": "systemic", "problem": "Light and dark dialogs use materially different perceived elevation: the light dialog has a strong shadow and bright plane, while dark mode has weak plane separation.", "fix": "Define matched elevation tokens for both themes\u2014surface contrast, border, backdrop, and shadow\u2014so modal hierarchy is equivalent."}, {"screen": "c07", "location": "Narrow workspace list", "severity": "broken", "problem": "Mobile removes image, resource, updated time, Stop, VS Code, and View logs without exposing an overflow action or disclosure path; key workflows are lost.", "fix": "Use expandable rows or a per-row overflow menu containing the omitted metadata and actions, with a compact state-first summary."}, {"screen": "c07", "location": "Narrow-page header", "severity": "structural", "problem": "The action controls wrap into an ambiguous three-item cluster before the search and state filters, obscuring their purpose and weakening the scan order.", "fix": "Make New workspace the clear first-row primary action and place secondary global controls in a labeled overflow or a separate toolbar."}, {"screen": "c08", "location": "Documentation site shell and content panes", "severity": "systemic", "problem": "Multiple near-identical navy surfaces, borders, and boxed code/callout treatments create a dense enclosed feel; navigation, article, and contextual TOC do not separate enough by surface role.", "fix": "Reduce container borders, use fewer elevated surfaces, and establish clearer base/raised/code/callout roles while keeping the dark technical aesthetic."}, {"screen": "c09", "location": "Documentation article body", "severity": "cosmetic", "problem": "Several body and secondary-text tones are light enough to be readable but subdued against the expansive white canvas, especially in side navigation and explanatory copy.", "fix": "Increase secondary-text contrast and use less blue-gray tint for normal prose; reserve muted tones for genuinely secondary metadata."}, {"screen": "c10", "location": "Dashboard delete treatment", "severity": "structural", "problem": "The delete panel is embedded below the table rather than presented as a confirmation dialog and lacks the typed-name safeguard visible in the app flow.", "fix": "Use the same destructive confirmation pattern across product surfaces, including explicit target confirmation and a foreground modal or anchored confirmation state."}, {"screen": "c11", "location": "Narrow dashboard table", "severity": "structural", "problem": "The four-column table is forced into a narrow card, causing state and host labels to wrap awkwardly and damaging quick operational scanning.", "fix": "Transform each workspace into a stacked compact row on narrow screens, prioritizing name, state, host, recency, and an action menu."}, {"screen": "c12", "location": "Palette laboratory page", "severity": "cosmetic", "problem": "The palette names and swatches are clear, but the oversized rigid matrix leaves the semantic relationship between tokens and applied UI states mostly abstract.", "fix": "Add small paired component specimens for each surface/action/state token, especially light-mode applications and contrast-critical text combinations."}]}
+{"round": 0, "reviewer": "codex/gpt", "kind": "full", "pass": 2, "scores": {"hierarchy": 7, "typography": 6, "spacing": 7, "color": 6, "consistency": 7, "density": 7, "usability": 6, "polish": 6, "responsive": 7, "identity": 8}, "control": {"hierarchy": 7, "typography": 6, "spacing": 7, "color": 6, "consistency": 7, "density": 7, "usability": 6, "polish": 6, "responsive": 7, "identity": 8}, "pairs": [], "findings": [{"screen": "c01", "location": "Workspace-list rows and top controls", "severity": "structural", "problem": "The entire operational interface is rendered at a very small type size, including statuses, resource facts, timestamps, and button labels. This weakens scanability and makes the wide empty canvas feel underused.", "fix": "Increase the base UI/data text scale and row height modestly; retain compactness through clearer column widths and reduced unused page area rather than tiny text."}, {"screen": "c01", "location": "Workspace-list row actions", "severity": "structural", "problem": "Primary terminal actions, secondary stop actions, and the quiet VS Code link are tightly packed into the same visual band, while row metadata has little hierarchy.", "fix": "Establish a consistent action area: primary terminal button first, compact secondary actions, then integration link; use stronger alignment and separation from resource metadata."}, {"screen": "c03", "location": "Workspace detail header", "severity": "structural", "problem": "The detail page leaves a large gap between the workspace identity and its tabs, so the relationship between identity, operational controls, and section navigation is weaker than it should be.", "fix": "Treat the identity block, status, primary actions, and tabs as one compact header region with a shared alignment rhythm."}, {"screen": "c03", "location": "Details and Resources tables", "severity": "systemic", "problem": "The two data groups use faint, repeated horizontal rules and low-emphasis labels that make the information feel visually flat, especially in dark mode.", "fix": "Use fewer but more intentional dividers, slightly stronger label/value contrast, and a shared definition-list grid; reserve stronger borders for section boundaries."}, {"screen": "c05", "location": "Delete confirmation dialog", "severity": "broken", "problem": "The destructive confirmation is shown against an extremely subtle dark overlay, leaving the background almost as prominent as the dialog and reducing focus on the irreversible task.", "fix": "Use a substantially darker, neutral scrim and preserve a clearly elevated modal surface with a focused shadow and border."}, {"screen": "c05", "location": "Delete confirmation dialog", "severity": "structural", "problem": "The destructive action appears available despite a visible name mismatch; the confirmation control does not clearly communicate its disabled/error-dependent state.", "fix": "Disable Delete until the exact name matches, with a visibly disabled treatment; enable it only after valid input while retaining the inline validation copy."}, {"screen": "c06", "location": "Delete confirmation dialog in light mode", "severity": "systemic", "problem": "Light mode uses a gray-blue backdrop and cool shadow that make the dialog feel washed out rather than intentionally elevated; it differs materially from the dark-mode surface logic.", "fix": "Define modal elevation tokens for both themes: neutral scrim, dedicated raised surface, consistent border contrast, and restrained shadow."}, {"screen": "c07", "location": "Mobile workspace list", "severity": "structural", "problem": "Responsive behavior removes image, resource, timestamp, stop, VS Code, and log actions without offering a clear route to the omitted operational controls.", "fix": "Turn each row into a compact disclosure/card pattern: preserve status and primary action, and expose metadata plus secondary actions through a row menu or expanded detail."}, {"screen": "c07", "location": "Mobile page header", "severity": "cosmetic", "problem": "The two unlabeled icon-placeholder controls sit detached above the primary action and consume scarce vertical space.", "fix": "Group utility controls with the primary action in a compact toolbar, or move lower-priority controls into an overflow menu."}, {"screen": "c08", "location": "Documentation landing-page hero and body", "severity": "structural", "problem": "The marketing documentation shell is visually much more stylized and spacious than the product console, making the brand feel like two related but separate systems.", "fix": "Share a core type scale, surface/border tokens, action shapes, and navigation density between docs and product while preserving the documentation layout."}, {"screen": "c08", "location": "Documentation content frame", "severity": "cosmetic", "problem": "The outer frame, three-column body, internal section borders, code blocks, and caution block create many simultaneous rectangular boundaries.", "fix": "Reduce redundant container borders and let spacing establish most hierarchy; keep borders for navigation separation, code, and high-priority callouts."}, {"screen": "c10", "location": "Dashboard delete panel", "severity": "structural", "problem": "The confirmation appears as an inline card below the table rather than a clearly scoped modal or row-level workflow, which makes the affected workspace and action state ambiguous.", "fix": "Use a modal confirmation for destructive deletion, or attach an inline confirmation directly to the selected row with an explicit selected state."}, {"screen": "c11", "location": "Narrow dashboard table", "severity": "structural", "problem": "The four-column table is retained at a width too narrow for state and host values, causing awkward wrapping and unstable row rhythm.", "fix": "Transform the table into stacked workspace summaries on narrow screens, with host and snapshot facts as secondary lines and actions in a footer."}, {"screen": "c12", "location": "Brand-system palette page", "severity": "systemic", "problem": "Several palette swatches and the page presentation rely on similar blue-gray surfaces, while labels and explanatory copy are comparatively muted; the system does not clearly demonstrate robust semantic and elevation separation.", "fix": "Document and apply stricter contrast-tested tokens for page, surface, raised, overlay, border, muted text, and semantic states; show representative component pairings in both themes."}, {"screen": "c01", "location": "App navigation and workspace states", "severity": "systemic", "problem": "Navigation selection, primary actions, documentation links, hover-like blue surfaces, and cyan accent all occupy closely related blue/cyan territory, diluting the accent's operational meaning.", "fix": "Reserve the bright accent for primary actions and links, use a quieter selected-navigation token, and keep state colors exclusive to their semantic statuses."}]}
+{"round": 0, "reviewer": "agy/gemini-3.8-flash-high", "kind": "full", "pass": 1, "scores": {"color": 5, "consistency": 4, "density": 5, "hierarchy": 5, "identity": 5, "polish": 5, "responsive": 4, "spacing": 5, "typography": 5, "usability": 5}, "control": {"color": 5, "consistency": 4, "density": 5, "hierarchy": 5, "identity": 5, "polish": 5, "responsive": 4, "spacing": 5, "typography": 5, "usability": 5}, "pairs": [], "findings": [{"fix": "Demote row-level operational actions ('Open terminal', 'Start') to secondary outline or ghost buttons, reserving the solid #27BFE8 accent fill exclusively for the view's single primary CTA ('New workspace').", "location": "Workspaces list table rows (action buttons)", "problem": "Accent overuse and broken action hierarchy: Every running row displays a solid, bright cyan (#27BFE8) primary button for 'Open terminal', identical in styling and visual weight to the top-level 'New workspace' CTA. This floods the view with competing primary actions, violating the principle of one primary action per view.", "screen": "c01", "severity": "systemic"}, {"fix": "Replace row flexbox with a structured grid or table layout having fixed column widths and clear column headers (Name, Image, Specs, Updated, Actions) so metadata aligns vertically regardless of string length.", "location": "Workspaces list rows (data alignment)", "problem": "Lack of tabular grid alignment: Rows use unconstrained flex spacing without table column definitions or headers. When container image names vary in length (e.g., ghcr.io/acme/builder:2026.10 in row 3 vs 45-character image names in rows 1-2), the resource specifications and timestamps shift horizontally across columns, destroying vertical scannability.", "screen": "c01", "severity": "structural"}, {"fix": "Add an explicit neutral/muted token for stopped state (e.g. #64748B) to the brand system, and replace pink #F57AB4 with an accessible red token (e.g. #EF4444) for error states that aligns with the destructive action color.", "location": "States color tokens in Brand System", "problem": "Incomplete state palette and low-contrast error token: The brand system palette defines only success (#3DDC97), warning (#FFC857), and error (#F57AB4), completely omitting a token for stopped/inactive states (which appears ad-hoc in c01 and c10). Furthermore, using pink/magenta (#F57AB4) for errors yields poor contrast and breaks developer conventions where errors and destructive operations are red.", "screen": "c12", "severity": "systemic"}, {"fix": "Define a first-class semantic token map for light mode in the brand system, ensuring darker, high-contrast equivalents for status colors (e.g., #DC2626 for failed, #166534 for running) that satisfy WCAG AAA for body text and AA for labels.", "location": "Status labels and table text across light mode", "problem": "Light mode palette adaptation fails WCAG contrast: In light mode, status labels such as ci-job failed use magenta (#D12771) and stopped uses pale grey (#6B778C), while the palette laboratory (c12) only defines dark mode values without an inverted token mapping. Several text elements fail WCAG AA contrast against the pure white (#FFFFFF) background.", "screen": "c02", "severity": "systemic"}, {"fix": "Standardize heading typography across the entire suite: either commit to the monospace slab-serif for all primary titles to reinforce the technical infrastructure identity, or use a refined sans-serif (like Inter/Geist) consistently everywhere.", "location": "Heading typography and brand font divergence", "problem": "Brand typography schizophrenia across screens: Screens c01\u2013c06 render view headings ('Workspaces', 'api') and modal titles in a standard sans-serif typeface, whereas c08, c10, c11, and c12 render headings in a distinct slab-serif monospace font. This creates an inconsistent brand identity between app screens and the site/laboratory.", "screen": "c10", "severity": "systemic"}, {"fix": "Render all timestamp strings ('Updated 2m ago', etc.) in the design system's monospace font to ensure tabular numeric alignment and adherence to the infrastructure brand constraint.", "location": "Timestamps across workspaces rows", "problem": "Timestamps violate monospace direction constraint: Direction guidelines mandate that IDs, image names, and timestamps be set in a monospace face. While IDs and image names are monospace, timestamps ('Updated 2m ago', 'Updated yesterday', '2 min ago') are rendered in proportional sans-serif font.", "screen": "c01", "severity": "structural"}, {"fix": "Remove inline deletion cards from dashboard content containers; always invoke destructive confirmations as centered modal overlays with background scrims (as done in c05/c06).", "location": "Confirmation modal layout and placement", "problem": "Destructive confirmation card rendered inline within content card: In c10, 'Delete agent-1?' is rendered as an unmodalized card placed directly beneath the table inside the main card next to the toast notification, rather than as a focused modal dialog with an overlay backdrop scrim. This risks accidental clicks and confounds operational hierarchy.", "screen": "c10", "severity": "structural"}, {"fix": "Establish a strict systemic standard for dialog button placement (e.g. secondary Cancel on left, primary/destructive action on right) and enforce it identically across all dialog variants.", "location": "Action button ordering in confirmation modal", "problem": "Inverted button order between confirmation dialogs: In c05/c06, the dialog places [Cancel] on the left and [Delete] on the right. In c10 and c11, the order is reversed: [Delete] is on the left and [Cancel] is on the right. This inconsistency causes muscle-memory errors in critical destructive workflows.", "screen": "c10", "severity": "systemic"}, {"fix": "Standardize destructive VM deletion across all views to require typing the resource identifier to confirm deletion, preventing accidental destruction.", "location": "Destructive confirmation pattern discrepancy", "problem": "Bypassed confirmation typing safety pattern: Screen c05 enforces a safety safeguard requiring the user to type the workspace name ('agent-1') before enabling the Delete action. In c10/c11, this safety safeguard is completely omitted, presenting an unverified one-click destructive button for deleting an isolated VM.", "screen": "c10", "severity": "systemic"}, {"fix": "Add a secondary 'Copy' icon/button to the top right of the code snippet container with visual feedback when clicked.", "location": "'Connect' code block panel", "problem": "Missing copy affordance for terminal CLI commands: The Connect panel provides CLI commands ('spin ssh api', 'spin snapshot create api...'), but lacks a 'Copy' button or click-to-copy affordance. In developer infrastructure tools (compare ref-beam-apps.png), command snippets must be instantly copyable with a single click.", "screen": "c03", "severity": "structural"}, {"fix": "Replace multi-column tabular layout on mobile viewports (<640px) with stacked responsive cards or list rows with clear key-value hierarchy, preventing text hyphenation and badge wrapping.", "location": "Workspaces table on narrow viewport", "problem": "Severe table collapse and broken badge wrapping on mobile: In c11, the 4-column desktop table is rendered on a ~360px viewport without responsive reflow. Table headers collide, machine names break mid-word ('agent-' / '1'), status labels wrap to 3 lines ('waiting' / 'for a' / 'host'), and runner badges wrap awkwardly ('runner-' / 'a1').", "screen": "c11", "severity": "broken"}, {"fix": "Provide an overflow/action menu (e.g. kebab icon) on mobile rows containing all operational actions (Stop, View Logs, VS Code, Details), and preserve essential specs in a compact secondary line beneath the workspace name.", "location": "Workspaces list row actions on mobile", "problem": "Complete loss of essential operational controls on mobile: The responsive layout in c07 removes the Stop button, View logs button, and VS Code link, and drops all resource specs and image identifiers, leaving only an oversized Open terminal button. A mobile on-call engineer cannot stop runaway instances or diagnose failed jobs.", "screen": "c07", "severity": "broken"}, {"fix": "Style status filters as interactive filter chips/segmented pills with subtle borders and active/selected background states (similar to Beam's tab bar in ref-beam-apps.png).", "location": "Filter bar status counters ('Running 2', 'Stopped 1', 'Failed 1')", "problem": "Status filters lack interactive affordances: The status counters ('Running 2', 'Stopped 1', 'Failed 1') are styled as flat plain text with inline colored symbols, lacking pill containers, borders, or hover/selected affordances. Users cannot distinguish whether these are static summary metrics or clickable filter toggles.", "screen": "c01", "severity": "structural"}, {"fix": "Replace the glossy 3D illustration with a crisp, technical vector schematic (e.g. architecture diagram of sandboxes, snapshots, and diffs) that matches the serious infrastructure branding.", "location": "Hero section right illustration", "problem": "Clashing 3D visual asset: The hero graphic uses a glossy, skeuomorphic 3D blue layered puck/sphere with drop shadows that clashes with the flat, technical, monospace-driven developer aesthetic established across Beam, Boxd, and the Spinbox palette laboratory.", "screen": "c08", "severity": "cosmetic"}, {"fix": "Unify the navigation taxonomy across the entire product into a single consistent structure (adopting the grouped WORK/ADMINISTER structure from c10, as it better matches infrastructure platforms).", "location": "Left sidebar navigation structure", "problem": "Incompatible sidebar information architecture: c10 introduces a categorized sidebar (WORK vs ADMINISTER) with Hosts and Snapshots as primary navigation items, whereas c01 has a flat sidebar with Dashboard, Workspaces, Shared with me, Teams, and Settings. The product lacks a unified navigation architecture.", "screen": "c10", "severity": "systemic"}]}
+{"round": 0, "reviewer": "agy/gemini-3.8-flash-high", "kind": "full", "pass": 2, "scores": {"color": 5, "consistency": 5, "density": 5, "hierarchy": 5, "identity": 6, "polish": 6, "responsive": 4, "spacing": 6, "typography": 5, "usability": 5}, "control": {"color": 5, "consistency": 5, "density": 5, "hierarchy": 5, "identity": 6, "polish": 6, "responsive": 4, "spacing": 6, "typography": 5, "usability": 5}, "pairs": [], "findings": [{"fix": "Change row-level action buttons (Open terminal, Start) to secondary outline or ghost buttons, reserving the filled cyan accent exclusively for the primary view-level call-to-action (New workspace).", "location": "workspace row action buttons", "problem": "Every running row uses a filled bright cyan primary accent button ([Open terminal]) and the stopped row uses filled cyan ([Start]), directly competing with the top-level [New workspace] primary action and violating the brand rule of reserving the solid accent for the single primary action per view.", "screen": "c01", "severity": "systemic"}, {"fix": "Standardize row actions into a unified button group or apply consistent secondary outline button styling to 'VS Code' with an icon placeholder matching other row actions.", "location": "workspace row actions, rightmost item ('VS Code')", "problem": "'VS Code' is rendered as plain unstyled text without a button container, border, or icon placeholder, creating an inconsistent action affordance alongside bordered [Stop] and filled [Open terminal] buttons.", "screen": "c01", "severity": "structural"}, {"fix": "Disable the [Delete] button (muted background, disabled cursor, inactive state) until the input text exactly matches the target workspace name 'agent-1'.", "location": "delete confirmation modal footer actions", "problem": "The destructive [Delete] button is rendered in an active, fully enabled red state even though the workspace name confirmation input contains an invalid value ('agent') and displays a mismatch error.", "screen": "c05", "severity": "broken"}, {"fix": "Adjust the light mode error/failed token to a deeper crimson/ruby red (e.g., #B91C1C or #DC2626) that achieves at least 4.5:1 contrast against white and light surfaces.", "location": "status badges and validation text in light mode", "problem": "The error/failed state color (#F57AB4 pink) and validation text have insufficient contrast against white surfaces (approx. 2.3:1), failing WCAG AA requirements for readable text.", "screen": "c02", "severity": "broken"}, {"fix": "Unify error, failure, and destructive actions under a coherent red family (e.g., crimson/red-600) with dedicated variants for text, surfaces, and active borders.", "location": "semantic state tokens in palette laboratory", "problem": "The palette defines error/failed as pink (#F57AB4) while destructive actions use crimson red (#C2304F), splitting failure semantics across two unrelated hues and conflicting with established infrastructure conventions where failure and destructive danger share consistent red semantics.", "screen": "c12", "severity": "systemic"}, {"fix": "Stack workspace metadata beneath the name in a multi-line card/row layout and provide an overflow actions menu (...) so operational actions remain accessible on mobile.", "location": "narrow viewport workspace list rows", "problem": "The responsive mobile view strips out essential metadata (image repo, CPU/memory/disk specifications, relative update timestamps) and completely omits the Stop, VS Code, and View logs actions, making failed or running workspaces unmanageable on narrow screens.", "screen": "c07", "severity": "structural"}, {"fix": "Replace multi-column table layout on mobile with stacked definition cards, or enable horizontal overflow scrolling (overflow-x: auto) with white-space nowrap on monospace identifiers.", "location": "responsive dashboard table rows", "problem": "Table columns on narrow viewports lack min-width constraints and horizontal scrolling, causing severe mid-word line breaks and hyphenation across technical IDs and states (agent- / 1, runner- / a1, waiting / for a / host).", "screen": "c11", "severity": "broken"}, {"fix": "Standardize all display headings on the geometric sans-serif typeface used in c01-c04, restricting monospace strictly to code blocks, CLI commands, container image tags, and machine identifiers.", "location": "typography system across headings", "problem": "Headlines on documentation and dashboard screens (c08, c09, c10, c11) use a slab-serif typewriter monospace font, conflicting with the clean sans-serif typography used throughout the main app views (c01-c06).", "screen": "c08", "severity": "systemic"}, {"fix": "Consolidate metadata into a compact grid or unified specifications panel with inline resource metrics (similar to Boxd's inline resource bars) and remove full-width horizontal divider lines between every field.", "location": "workspace detail view layout", "problem": "Details and Resources are displayed in two sparse, widely separated columns with heavy horizontal rules under every single property, leaving over half of the view as dead space and providing lower density than infrastructure references.", "screen": "c03", "severity": "structural"}, {"fix": "Adopt a single canonical workspaces view pattern across the product: either a unified high-density data table with configurable columns and actions, or the list-row pattern, ensuring host, image, and specs are consistently represented.", "location": "workspaces view architecture", "problem": "Screen c10 introduces a standard table layout with column headers (Name, State, Host, Last snapshot) and a [Refresh] button, contradicting the card-row pattern in c01 which shows image tags, resource specs, and inline action buttons.", "screen": "c10", "severity": "systemic"}, {"fix": "Shift surface tokens from deep navy toward neutral or zinc dark tones (e.g., #09090B, #121214, #18181B), reserving blue/cyan purely for interactive focal points.", "location": "surface tokens and dark mode canvas", "problem": "All dark mode surfaces (#111827, #172035, #202C47) use heavily saturated blue-navy tones instead of the neutral, low-saturation charcoals seen in reference products like Beam and Boxd, creating visual fatigue and competing with syntax/code.", "screen": "c12", "severity": "systemic"}, {"fix": "Remove the outer bordered container enclosing the page; allow the documentation layout to span the full viewport with standard header, persistent sidebar navigation, and dedicated article content area.", "location": "outer documentation layout wrapper", "problem": "The documentation page wraps the header, hero marketing block, and documentation columns inside a single bordered card floating within an outer dark background, creating artificial nesting and cramped reading margins.", "screen": "c08", "severity": "structural"}, {"fix": "Apply a subtle tinted amber background fill (e.g., amber with 10% opacity) and an amber icon indicator to reinforce the warning semantics beyond the 2px left border.", "location": "documentation caution callout block", "problem": "The Caution callout uses an amber left border indicating caution, but retains the neutral dark blue-gray container background, weakening its semantic distinction from standard code blocks.", "screen": "c08", "severity": "cosmetic"}, {"fix": "Convert status counts into interactive filter pills/tabs with consistent badge styling, enabling users to click to filter the list by state.", "location": "filter bar status summary counts", "problem": "The inline status count list ('\u2022 Running 2 o Stopped 1 \u25a0 Failed 1') uses loose text with miscellaneous symbol characters (\u2022, o, \u25a0) rather than interactive filter chip affordances.", "screen": "c01", "severity": "cosmetic"}, {"fix": "Enclose Open terminal and VS Code into a structured action button group or dropdown menu of access methods, alongside an isolated secondary Stop button.", "location": "workspace detail top-right action bar", "problem": "The detail screen places VS Code as unstyled plain text adjacent to the bordered [Stop] and filled [Open terminal] buttons, without visual grouping or clear distinction between web terminal vs external IDE launcher.", "screen": "c03", "severity": "structural"}]}
+{"round": 1, "reviewer": "claude/sonnet", "kind": "full", "scores": {"hierarchy": 6, "typography": 7, "spacing": 6, "color": 6, "consistency": 7, "density": 6, "usability": 6, "polish": 6, "responsive": 4, "identity": 6}, "control": {"hierarchy": 6, "typography": 7, "spacing": 6, "color": 6, "consistency": 6, "density": 6, "usability": 5, "polish": 6, "responsive": 4, "identity": 7}, "pairs": [{"screen": "app-list-dark.desktop.dark", "winner": "candidate", "reason": "Neutral near-black surface with a blue primary keeps white button labels readable and leaves the state colours to carry meaning. B's cyan primary buttons have weak label contrast, and its navy surfaces blend together."}, {"screen": "app-list-light.desktop.dark", "winner": "baseline", "reason": "The deeper teal-blue primary gives stronger label contrast and ties into the cyan/blue logo. B's royal blue feels like generic SaaS, and its heavier grey-blue nav highlight competes with the content."}, {"screen": "app-detail-dark.desktop.dark", "winner": "candidate", "reason": "Surfaces are clearly separated, and the Connect panel and code block read as distinct layers. B's navy panel and code block nearly merge, and the cyan primary button is low-contrast with its label."}, {"screen": "app-detail-light.desktop.dark", "winner": "baseline", "reason": "The primary colour is more distinctive and has better contrast, and the pale-blue code block fits the brand. B's royal blue is more generic."}, {"screen": "app-dialog-dark.desktop.dark", "winner": "candidate", "reason": "The dialog stands out cleanly against the dimmed neutral background, and the red Delete button is clearly destructive. In B the navy dialog blends into the page, and the teal primaries behind it add a competing hue."}, {"screen": "app-dialog-light.desktop.dark", "winner": "baseline", "reason": "The destructive Delete button is clearly red and the primaries behind the scrim are subdued. The two are close, and A's accent is the more coherent with the brand."}, {"screen": "app-list-dark.mobile.dark", "winner": "baseline", "reason": "Version b's blue is clearly separate from the green running state and from failed pink. Version a's cyan on a navy ground sits closer to the status hues and blends with the navy surfaces."}, {"screen": "site-dark.desktop.dark", "winner": "candidate", "reason": "Version b's neutral near-black page with blue accent sits closer to the Beam and boxd references. Surfaces and borders separate more clearly. Version a's navy-on-navy layers blur together, and its teal labels and cyan accent look more like a generic SaaS theme."}, {"screen": "site-light.desktop.dark", "winner": "candidate", "reason": "Version b has a neutral light surface with a single blue accent, and its code blocks read as quiet panels. Version a tints everything blue-grey, and its teal-blue buttons and banner compete with each other."}, {"screen": "dashboard-dark.desktop.dark", "winner": "candidate", "reason": "The state colours (green, amber, pink, grey) are more distinct against neutral black than against navy. The blue primary button, the red Delete and the neutral Cancel form a clear hierarchy. The dialog and toast separate better from the page."}, {"screen": "dashboard-light.mobile.dark", "winner": "baseline", "reason": "Version a fits the whole table in the narrow width, including the Last snapshot column. Version b clips that column at the right edge. Version a's wrapped state text and hosts are untidy, but nothing is lost."}, {"screen": "palette-dark.desktop.dark", "winner": "candidate", "reason": "Version b's palette is neutral with one blue accent. Its accent-text and on-accent roles are separate and deliberate. Its state colours are readable and not neon. In version a, accent-text equals accent and the surface steps are close together."}], "findings": [{"screen": "app-list-dark.desktop.dark", "location": "Row layout (image column)", "severity": "structural", "problem": "The image column is centred and not aligned. Image strings start at different x positions on each row (495, 565, 562), and the action columns shift between rows. The list reads as ragged and can't be scanned in columns.", "fix": "Use a fixed grid with a column header row (Name, State, Image, Resources, Updated, Actions). Left-align every column and right-align the actions."}, {"screen": "app-list-dark.desktop.dark", "location": "State column", "severity": "systemic", "problem": "The state is small monospace text next to a tiny dot or square. It is weaker than the name, and 'stopped' is a hollow ring in muted grey. Failed pink-magenta text has low contrast on black.", "fix": "Make state a fixed-width badge or label with a distinct shape per state, a readable size and AA contrast. Give failed more weight."}, {"screen": "app-list-dark.desktop.dark", "location": "Row actions", "severity": "structural", "problem": "Open terminal is a filled blue primary on every running row. Several identical filled buttons repeat down the list and dilute the hierarchy. VS Code is a bare text link with no affordance. Stop sits at the same weight as Open.", "fix": "Make the primary action outline or tonal in the list and reserve filled blue for New workspace. Give VS Code a button affordance. Move destructive and stop actions into a row menu, styled as danger."}, {"screen": "app-list-dark.desktop.dark", "location": "Empty area below the list and the top-right icon buttons", "severity": "structural", "problem": "The page has only four rows and the rest is empty. The two top-right buttons carry no labels, so their meaning is unclear. Nothing shows resource totals, an activity feed or last snapshots.", "fix": "Add a summary strip (running count, total vCPU/RAM) and label the icon buttons or give them tooltips. Use a denser row with the ID visible."}, {"screen": "app-detail-dark.desktop.dark", "location": "Details table and Connect card", "severity": "systemic", "problem": "The label column sits in dim grey and the values are plain text. A bordered Connect card is nested inside a bordered page, and its code block is a third layer. The resources and details tables are the same weight as the card. The ID is the only monospace value of note and carries no copy affordance.", "fix": "Reduce nesting to two surface levels. Add copy buttons on the ID, image and commands. Lift the label contrast to AA."}, {"screen": "app-detail-dark.desktop.dark", "location": "Header actions", "severity": "structural", "problem": "Open terminal, Stop and VS Code are separate top-right controls with no destructive separation. Delete and Snapshot are absent from the header, so snapshot and diff have no entry point. The tab bar has 7 tabs of equal weight.", "fix": "Add Snapshot as a primary action next to Open terminal, and a danger-styled Delete in an overflow menu. Show a state badge in the header."}, {"screen": "app-list-light.desktop.dark", "location": "Light theme surfaces and nav selection", "severity": "systemic", "problem": "The light theme has pure white surfaces with hairline grey borders. The active nav pill is very pale, and the page has no tinted layering. Row and header hierarchy come from borders only.", "fix": "Add an off-white page background with white content surfaces, and give the selected nav item an accent left bar so the selection survives low contrast."}, {"screen": "app-dialog-dark.desktop.dark", "location": "Delete dialog", "severity": "cosmetic", "problem": "The dialog is good (type-to-confirm, red Delete), but the error text is magenta and small. The Delete button stays active while the name doesn't match. The body copy wraps with a single orphan word ('up.'). The scrim is dark and the dialog has no icon or other cue.", "fix": "Disable Delete until the name matches. Use a danger text colour at 14px or larger with an icon. Tighten the copy."}, {"screen": "app-detail-dark.desktop.dark", "location": "Overall responsiveness", "severity": "structural", "problem": "Only the desktop layout is shown. The list rows pack eight items into one line with no wrap, truncation or column drop. The 7-tab bar and the two-column tables have no narrow design.", "fix": "Design a narrow row (name and state on line one, image and resources on line two, actions in a menu). Make the tab bar scroll and stack the tables."}, {"screen": "c01", "location": "Docs site hero and code blocks", "severity": "structural", "problem": "The hero headline is a very large monospace face that is hard to read. The cyan brand colour is used for the eyebrows, links, buttons, nav highlight and the banner all at once, so the accent loses its meaning. The Caution block uses a yellow bar that appears nowhere else. The code block is the same navy as the page, so it barely separates.", "fix": "Set headlines in the proportional face and keep monospace for code and IDs. Limit the accent to links and one primary button. Give code blocks a darker inset surface. Define a callout colour per type."}, {"screen": "c01", "location": "Docs site shell", "severity": "cosmetic", "problem": "The site is boxed inside an outer frame with a stray 'THE SITE' label and extra margins. Sidebar items have no section grouping, and the 'On this page' panel is dim and small.", "fix": "Remove the nested frame. Group the sidebar by section and lift the contrast of the right-hand TOC."}, {"screen": "dashboard-light.mobile.dark", "location": "mobile workspaces table", "severity": "structural", "problem": "The four-column table is cut off at the right edge, so Last snapshot is clipped. There is no stacked row layout or visible scroll cue.", "fix": "Below about 480px, turn each row into a stacked item (name and state on the first line, host and snapshot as mono metadata beneath). Alternatively, wrap the table in an overflow container with an edge fade."}, {"screen": "dashboard-dark.desktop.dark", "location": "workspaces table", "severity": "structural", "problem": "The table has no per-row actions. Open terminal, Start, Stop and Delete are missing, so the operational actions the page exists for are not visible. Only a name link exists.", "fix": "Add a right-aligned actions column with a primary action per state and a quiet overflow menu. Style Delete in the destructive colour only inside the menu or the dialog."}, {"screen": "dashboard-dark.desktop.dark", "location": "Delete dialog and toast", "severity": "structural", "problem": "The dialog and the toast are drawn inline under the table with no scrim. They read as page content, and the toast floats beside the dialog.", "fix": "Render the dialog modally over a scrim. Pin the toast to a corner. Put the destructive button on the right and make the confirmation name the resource in mono."}, {"screen": "app-list-dark.mobile.dark", "location": "workspaces list on mobile", "severity": "structural", "problem": "Every running row carries a large solid blue Open terminal button. The primary-action colour is repeated four times and dominates the list. The ids, state and snapshot time that matter are small.", "fix": "Make row actions ghost or outline buttons. Keep one solid primary per view (New workspace). Add a mono id and image line to each row."}, {"screen": "app-list-dark.mobile.dark", "location": "top toolbar", "severity": "cosmetic", "problem": "Two empty square outlines sit beside New workspace with no label, and the filter chips are not visibly interactive.", "fix": "Give the icon buttons accessible labels and tooltips. Style the filter chips as toggles with a selected state."}, {"screen": "palette-dark.desktop.dark", "location": "surface tokens", "severity": "systemic", "problem": "The steps between page, surface, raised and overlay are about 4-6 lightness points. Cards and the sidebar blend into the page and rely on thin borders.", "fix": "Widen the surface steps, or make the border token consistently visible. Check each pairing against 1.2:1 or better."}, {"screen": "palette-dark.desktop.dark", "location": "states section", "severity": "systemic", "problem": "The grey of stopped, the waiting amber and the failed pink are close in lightness on dark. Mono 11-12px labels are small, so the shapes carry most of the distinction.", "fix": "Use larger state glyphs and 13px labels. Add a distinct stopped glyph shape and show the state pill on a surface to check contrast."}, {"screen": "site-dark.desktop.dark", "location": "hero and docs frame", "severity": "systemic", "problem": "A large mono headline and the whole docs frame sit inside a bordered box labelled THE SITE. The 3D logo illustration clashes with the flat, restrained system.", "fix": "Use a flatter or monochrome mark in the hero, or shrink it. Drop the nested frame outside the lab page."}, {"screen": "site-dark.desktop.dark", "location": "code blocks and table", "severity": "cosmetic", "problem": "Code blocks and the caution box share the same fill. The comparison table has no header weight and its rule lines are weak, so the content structure is flat.", "fix": "Give code a recessed surface and the caution a tinted warning background. Make the table header a mono small-caps row with a stronger bottom rule."}, {"screen": "dashboard-dark.desktop.dark", "location": "sidebar", "severity": "cosmetic", "problem": "The navigation has only a few items. Its selected state is a faint blue wash with no edge marker. The section labels WORK and ADMINISTER are tiny and low-contrast.", "fix": "Add a 2px accent edge to the selected item and raise the label contrast. Add counts such as Workspaces 4."}, {"screen": "dashboard-dark.desktop.dark", "location": "table cells", "severity": "cosmetic", "problem": "Name links are blue and underlined on every row, so the column is noisy. Host chips and plain timestamps are inconsistent: hosts are mono but timestamps are proportional.", "fix": "Show names as bold text, underlined on hover only. Set timestamps and ids in mono across the table."}, {"screen": "site-light.desktop.dark", "location": "light mode", "severity": "cosmetic", "problem": "The banner is a saturated blue-lavender strip. The grey code blocks and the page are close in tone, so the light theme feels flat next to the dark one.", "fix": "Use a neutral info banner with a left rule. Add a border to the code surfaces and raise the contrast of the muted text."}]}
+{"round": 1, "reviewer": "codex/gpt", "kind": "full", "scores": {"hierarchy": 7, "typography": 7, "spacing": 8, "color": 8, "consistency": 8, "density": 8, "usability": 7, "polish": 7, "responsive": 3, "identity": 8}, "control": {"hierarchy": 8, "typography": 8, "spacing": 8, "color": 8, "consistency": 7, "density": 7, "usability": 7, "polish": 8, "responsive": 3, "identity": 9}, "pairs": [{"screen": "app-list-dark.desktop.dark", "winner": "baseline", "reason": "The navy-and-cyan system feels more specific to Spinbox and gives operational actions a clearer, less generic presence."}, {"screen": "app-list-light.desktop.dark", "winner": "baseline", "reason": "The restrained cyan action color is more distinctive and less visually heavy than saturated blue on the light surface."}, {"screen": "app-detail-dark.desktop.dark", "winner": "baseline", "reason": "The navy content surface and cyan primary action create a more coherent infrastructure-console identity."}, {"screen": "app-detail-light.desktop.dark", "winner": "baseline", "reason": "Cyan provides clearer continuity with the dark mode and product mark while preserving the sparse data layout."}, {"screen": "app-dialog-dark.desktop.dark", "winner": "baseline", "reason": "The navy modal better belongs to the application shell and maintains a deliberate destructive-action hierarchy."}, {"screen": "app-dialog-light.desktop.dark", "winner": "tie", "reason": "The two light confirmation dialogs are visually indistinguishable at the supplied scale."}, {"screen": "app-list-dark.mobile.dark", "winner": "baseline", "reason": "The navy surface system gives the compact workspace list clearer structure and a more deliberate infrastructure-console identity."}, {"screen": "site-dark.desktop.dark", "winner": "baseline", "reason": "The dark documentation page has stronger brand cohesion, with cyan used consistently to guide attention without changing the information architecture."}, {"screen": "site-light.desktop.dark", "winner": "baseline", "reason": "The teal primary action and pale teal contextual surfaces create a clearer, more distinctive light-mode system."}, {"screen": "dashboard-dark.desktop.dark", "winner": "baseline", "reason": "The dashboard\u2019s navy surfaces, selected navigation treatment, and cyan primary action form a more coherent operational UI."}, {"screen": "dashboard-light.mobile.dark", "winner": "baseline", "reason": "It preserves all table columns at the narrow viewport; version b clips essential table content horizontally."}, {"screen": "palette-dark.desktop.dark", "winner": "baseline", "reason": "The palette is more internally coherent and gives Spinbox a distinctive cyan-on-navy identity suited to the product."}], "findings": [{"screen": "app-list-dark.desktop.dark", "location": "Workspace list rows", "severity": "structural", "problem": "Rows rely on alignment alone; there are no column labels, so image, resources, freshness, and actions take a moment to parse.", "fix": "Add a compact desktop table header or persistent field labels aligned to the existing columns."}, {"screen": "app-list-dark.desktop.dark", "location": "Workspace list action area", "severity": "structural", "problem": "Primary terminal actions repeat at full visual weight for every running row, making the list action-heavy before the user has evaluated the workspaces.", "fix": "Keep one strong row action, but place secondary actions such as Stop and VS Code in an overflow or quieter inline action group."}, {"screen": "app-list-light.desktop.dark", "location": "Workspace list header", "severity": "systemic", "problem": "The top-right unlabeled controls are indistinguishable from the supplied icon placeholders and have no visible purpose.", "fix": "Use visible labels, tooltips with recognizable non-placeholder icons, or remove controls that are not essential at list level."}, {"screen": "app-detail-dark.desktop.dark", "location": "Overview header and detail sections", "severity": "structural", "problem": "The large empty horizontal gap between the left identity block and right actions weakens the relationship between the workspace state and its controls.", "fix": "Use a bounded header grid that groups title/state with actions while preserving room for long image identifiers."}, {"screen": "app-detail-dark.desktop.dark", "location": "Connect code block", "severity": "cosmetic", "problem": "The code surface is close in value to the surrounding panel and competes weakly with the command text.", "fix": "Increase the code-panel tonal separation slightly while retaining its restrained border."}, {"screen": "app-detail-light.desktop.dark", "location": "Detail data rows", "severity": "systemic", "problem": "Labels and values have similar visual weight, making resource facts and metadata slower to scan in light mode.", "fix": "Reduce label contrast/weight modestly and reserve stronger text for values, IDs, and actionable links."}, {"screen": "app-dialog-dark.desktop.dark", "location": "Delete confirmation modal", "severity": "structural", "problem": "The Delete button is visually active despite an explicitly invalid confirmation value, which makes the destructive state ambiguous.", "fix": "Disable Delete until the exact name matches and show the enabled destructive state only after validation succeeds."}, {"screen": "app-dialog-dark.desktop.dark", "location": "Modal copy", "severity": "cosmetic", "problem": "The recovery-window sentence is grammatically unclear: \u201cuntil its days are up.\u201d", "fix": "State the retention period explicitly, for example \u201cYou can restore it from Trash for 30 days.\u201d"}, {"screen": "app-dialog-light.desktop.dark", "location": "Light-mode confirmation overlay", "severity": "systemic", "problem": "The gray backdrop preserves too much background contrast, so the destructive confirmation does not isolate the task as strongly as the dark-mode dialog.", "fix": "Use a slightly darker neutral overlay and ensure dialog elevation is carried by surface contrast and border rather than a broad shadow alone."}, {"screen": "c01", "location": "Global navigation", "severity": "structural", "problem": "The sidebar has substantial unused vertical space while Settings and account information are isolated at the bottom without a clear grouping hierarchy.", "fix": "Introduce explicit lower-nav grouping and compact the account/settings region into a consistent footer section."}, {"screen": "c02", "location": "Workspace overview, network field", "severity": "broken", "problem": "\u201coffline Change\u201d runs together, obscuring that Change is an action rather than part of the value.", "fix": "Separate the status value and action with spacing and use a distinct link treatment."}, {"screen": "c03", "location": "Documentation article body", "severity": "structural", "problem": "The three-column documentation frame leaves the article column relatively narrow while the right table of contents has little content.", "fix": "Allow the article column to expand when the right rail is sparse, or collapse the page TOC into the article header at this viewport."}, {"screen": "dashboard-light.mobile.dark", "location": "workspace table, narrow layout", "severity": "broken", "problem": "The preferred system does not have a robust narrow-table pattern: the alternate version visibly clips the last-snapshot column, while the selected version compresses status text into awkward multi-line fragments.", "fix": "At this breakpoint replace table rows with labeled stacked records, or retain only Name and State and move host/snapshot into row detail."}, {"screen": "dashboard-dark.desktop.dark", "location": "Delete agent-1 confirmation", "severity": "structural", "problem": "The destructive confirmation looks like another inline card beneath the table and competes with a nearby success toast; it lacks strong interaction priority for an irreversible operational action.", "fix": "Use a focused dialog with backdrop and explicit keyboard focus, or attach an inline danger panel directly to the selected agent row."}, {"screen": "site-dark.desktop.dark", "location": "documentation body, first explanatory paragraph", "severity": "broken", "problem": "An inline-code/control fragment is visibly clipped at the right edge after the Restore text.", "fix": "Remove the overflowing element or let the inline content wrap naturally within the content column."}, {"screen": "site-light.desktop.dark", "location": "documentation body, first explanatory paragraph", "severity": "broken", "problem": "The same clipped inline fragment appears in light mode, so the issue is systemic rather than theme-specific.", "fix": "Apply the same responsive inline-content sizing and wrapping rule in both themes."}, {"screen": "app-list-dark.mobile.dark", "location": "mobile workspace list", "severity": "structural", "problem": "Running entries expose only the Open terminal action, while stopped and failed entries lack equally visible contextual next actions or explanation.", "fix": "Provide a compact per-row action menu and state-appropriate primary action, such as Start for stopped and View failure for failed."}, {"screen": "palette-dark.desktop.dark", "location": "palette token presentation", "severity": "systemic", "problem": "The label token is shown as a highly saturated cyan block, making it read like an accent/action color instead of a quiet metadata role.", "fix": "Reserve cyan for interactive emphasis and use a subdued but AA-compliant blue-gray token for labels and secondary metadata."}, {"screen": "dashboard-dark.desktop.dark", "location": "workspace table actions", "severity": "structural", "problem": "The table is dense and readable, but its row affordance is mostly limited to linked names; operational actions are not discoverable at scan speed.", "fix": "Expose a compact trailing action menu per row with Open, snapshot, stop/start, and delete actions appropriate to state."}]}
+{"round": 1, "reviewer": "agy/gemini-3.8-flash-high", "kind": "full", "scores": {"color": 8, "consistency": 8, "density": 7, "hierarchy": 8, "identity": 8, "polish": 8, "responsive": 6, "spacing": 8, "typography": 7, "usability": 8}, "control": {"color": 5, "consistency": 6, "density": 6, "hierarchy": 6, "identity": 6, "polish": 6, "responsive": 5, "spacing": 7, "typography": 6, "usability": 6}, "pairs": [{"screen": "app-list-dark.desktop.dark", "winner": "candidate", "reason": "Neutral dark slate surfaces provide higher text contrast and professional restraint, avoiding version b's oversaturated cyan glow and blue color-cast that fatigues the eyes."}, {"screen": "app-list-light.desktop.dark", "winner": "candidate", "reason": "Royal blue accent offers superior contrast and clear visual separation from green running indicators, whereas version a's teal accent visually clashes and competes with running states."}, {"screen": "app-detail-dark.desktop.dark", "winner": "candidate", "reason": "Neutral slate background and crisp royal blue accent maximize legibility for dense technical specs and terminal commands without distracting cyan glare."}, {"screen": "app-detail-light.desktop.dark", "winner": "candidate", "reason": "Royal blue delivers robust WCAG-compliant contrast for text links and primary actions on light surfaces, whereas version a's teal text suffers from reduced legibility."}, {"screen": "app-dialog-dark.desktop.dark", "winner": "candidate", "reason": "Neutral modal surface and overlay ensure clear elevation and sharp contrast for the destructive red action and inline validation errors without navy tinting contamination."}, {"screen": "app-dialog-light.desktop.dark", "winner": "baseline", "reason": "Cohesive light palette where background actions recede predictably and the destructive confirmation action commands immediate, unambiguous visual attention."}], "findings": [{"fix": "Introduce a persistent tabular header row (Name, Status, Image, Specs, Updated, Actions) with subtle muted text, enabling sorting on compute specs and last updated time.", "location": "Workspace list header above row items", "problem": "The workspaces list lacks table column headers, making scanning dense infrastructure specs (CPU, memory, storage) and alignment ambiguous across variable-length rows.", "screen": "app-list-dark.desktop.dark", "severity": "structural"}, {"fix": "Convert status counters into interactive filter pills with clear badge borders and active toggle states to allow one-click filtering by state.", "location": "Status counter row next to search input", "problem": "Status counts (Running 2, Stopped 1, Failed 1) appear as passive text without interactive filter affordances.", "screen": "app-list-dark.desktop.dark", "severity": "structural"}, {"fix": "Standardize the action column layout with a fixed right-aligned slot for primary lifecycle action (Open terminal / Start / View logs) followed by a secondary action (Stop) and an overflow menu.", "location": "Row action button groups on workspace list items", "problem": "Inconsistent action button widths and shifting positions between running, stopped, and failed rows break vertical scanning and muscle memory.", "screen": "app-list-dark.desktop.dark", "severity": "systemic"}, {"fix": "Add an explicit header action group including a primary 'Create Snapshot' button and a destructive 'Delete' button with standard confirmation modal triggers.", "location": "Workspace detail top-right header", "problem": "The workspace detail header omits lifecycle actions for Snapshot and Delete, which are top user workflows for sandbox VM management.", "screen": "app-detail-dark.desktop.dark", "severity": "systemic"}, {"fix": "Compact the definition list into a structured two-column key-value grid with fixed-width label columns (e.g. 120px) to prevent excessive whitespace gaps.", "location": "Details and Resources metadata sections", "problem": "Excessive horizontal gap between keys and values across wide dividers forces horizontal eye-tracking across empty space.", "screen": "app-detail-dark.desktop.dark", "severity": "structural"}, {"fix": "Add a dedicated one-click 'Copy' button in the upper-right corner of the code block container with visual feedback on click.", "location": "Connect terminal snippet card", "problem": "The CLI connection snippet lacks a one-click copy affordance for terminal commands.", "screen": "app-detail-dark.desktop.dark", "severity": "structural"}, {"fix": "Add numerical count badges to relevant tabs (e.g. Snapshots (3)) and an alert indicator badge on Logs when a workspace failure occurs.", "location": "Workspace detail subnavigation tab bar", "problem": "Subnavigation tabs lack contextual badges or operational state indicators for fast status triage.", "screen": "app-detail-dark.desktop.dark", "severity": "structural"}, {"fix": "Enforce a strict monospace font stack across image registries and tags, with subdued coloring for registry prefixes to highlight container tags.", "location": "Workspace image reference display", "problem": "Container image paths and digests blend with standard sans-serif text, reducing scannability for DevOps engineers.", "screen": "app-list-light.desktop.dark", "severity": "systemic"}, {"fix": "Highlight the required confirmation string in a bold monospace badge inside the helper text and enable autofocus on the confirmation input.", "location": "Delete confirmation modal input field", "problem": "Confirmation prompt does not highlight the target workspace name within the instruction sentence, increasing cognitive load before typing.", "screen": "app-dialog-dark.desktop.dark", "severity": "cosmetic"}, {"fix": "Add an inline action link (e.g. 'View snapshot') and a dismiss button with standard spacing from the viewport edge.", "location": "Bottom-right status toast notification", "problem": "Toast notification lacks an action link to view the published snapshot and provides no dismiss affordance.", "screen": "app-list-dark.desktop.dark", "severity": "cosmetic"}]}
+{"round": 3, "reviewer": "claude/sonnet", "kind": "full", "scores": {"hierarchy": 7, "typography": 7, "spacing": 7, "color": 7, "consistency": 7, "density": 6, "usability": 7, "polish": 7, "responsive": 4, "identity": 6}, "control": {"hierarchy": 6, "typography": 7, "spacing": 7, "color": 5, "consistency": 6, "density": 6, "usability": 6, "polish": 6, "responsive": 4, "identity": 7}, "pairs": [{"screen": "app-list-dark.desktop.dark", "winner": "candidate", "reason": "The neutral near-black list shows one quiet action per row plus an overflow menu, so state and image IDs read first. The navy and cyan version repeats filled cyan Open terminal buttons in every row, and the accent competes with the state dots."}, {"screen": "app-list-light.desktop.dark", "winner": "candidate", "reason": "Light mode keeps the same calm structure: one tonal action per row and a row menu. The other version has a saturated blue fill on every row and three buttons across each row, which is heavier than the data."}, {"screen": "app-detail-dark.desktop.dark", "winner": "candidate", "reason": "The detail header has a clear action hierarchy: Stop, a single primary Open terminal, and an overflow menu. The neutral surfaces separate the nav, the tables and the Connect panel better than navy on navy."}, {"screen": "app-detail-light.desktop.dark", "winner": "candidate", "reason": "The header has one primary button, a secondary Stop and an overflow menu, with calm grey surfaces. The other version shows three equal-weight buttons, including VS Code as bare text, so the primary action is less clear."}, {"screen": "app-dialog-dark.desktop.dark", "winner": "candidate", "reason": "The neutral dark dialog is a distinct raised surface over a dimmed list. The red Delete is the only saturated element, so the destructive action reads clearly. The navy version blends the modal into the page."}, {"screen": "app-dialog-light.desktop.dark", "winner": "candidate", "reason": "The white dialog on a dimmed grey page has a clear tonal Cancel and a red Delete. The other version's slate backdrop with a blue-tinted page looks muddier and mixes the blue accent into the dimmed rows."}, {"screen": "app-list-dark.mobile.dark", "winner": "candidate", "reason": "Rows use quiet secondary buttons, a contextual 'View logs' action for the failed row and an overflow menu. Only 'New workspace' is filled, so the one primary action stands out. Version a fills every row with a saturated cyan button, which buries state and the primary action."}, {"screen": "site-dark.desktop.dark", "winner": "candidate", "reason": "The neutral near-black surfaces and the calmer blue accent are closer to the Beam and boxd consoles. The cyan on navy in version a is more saturated, and the surfaces blend into each other."}, {"screen": "site-light.desktop.dark", "winner": "candidate", "reason": "The neutral gray surfaces for code blocks and callouts are more restrained and keep the accent for links and the selected item. Version b tints everything blue, so the surfaces blend. The difference is small."}, {"screen": "dashboard-dark.desktop.dark", "winner": "candidate", "reason": "The neutral dark surface and the selected nav row without a cyan wash keep the accent restrained. The dialog puts Cancel first and the red Delete last, the conventional order, and its sans title matches the rest of the UI."}, {"screen": "dashboard-light.mobile.dark", "winner": "candidate", "reason": "The dialog title is sans-serif and the buttons are right-aligned with Cancel before the red Delete, as on desktop. The surfaces are neutral. In version a the destructive button comes first on the left, which makes a mis-tap more likely."}, {"screen": "palette-dark.desktop.dark", "winner": "candidate", "reason": "The palette is neutral and restrained. accent-text is a distinct lighter tone, and the labels are muted rather than neon cyan. The on-accent white-on-blue pairing also looks more deliberate than the identical accent and accent-text values in version b."}], "findings": [{"screen": "app-list-dark.desktop.dark", "location": "Workspaces rows, middle columns", "severity": "structural", "problem": "The image column floats with no fixed column. Its left edge shifts from row to row (v2026 rows start at about x=545, the builder row at 550, notes at 555), and the resource and updated columns shift with it. The grid looks ragged and is hard to scan.", "fix": "Use a real table or grid with fixed column tracks for name/state, image, resources, updated and action, and right-align the numeric columns."}, {"screen": "app-list-dark.desktop.dark", "location": "State chips (running/failed/stopped) in the rows", "severity": "systemic", "problem": "The state label is small monospace text at low contrast, with only a tiny dot or square marker. Failed pink and running green are dim on black, so state, which is the main fact in the list, is the weakest element in the row.", "fix": "Raise state text to AA-plus contrast, use a larger 8\u201310px glyph with a distinct shape per state, and consider a state column or a subtle row tint for failed."}, {"screen": "app-list-dark.desktop.dark", "location": "Row actions and overflow menu", "severity": "structural", "problem": "Stop and Delete are hidden in the \u2026 menu and VS Code is gone from the row. The only visible action is a neutral grey button, and nothing in the row signals a destructive path. The tertiary actions have no visible entry point.", "fix": "Keep Open terminal as the one primary action, expose VS Code in the menu with labels, and set destructive menu items off in a red-toned group at the bottom."}, {"screen": "app-detail-dark.desktop.dark", "location": "Header actions: Stop, Open terminal, \u2026", "severity": "structural", "problem": "Stop, an operational action that interrupts a running VM, sits first, next to the primary button, in the same neutral style as Change size. Open terminal is a mid-blue fill with modest contrast against the black page.", "fix": "Order the actions primary first, give Stop a distinct caution treatment or move it into the menu, and give the accent button a stronger fill and text contrast."}, {"screen": "app-detail-dark.desktop.dark", "location": "Details and Resources tables, Connect panel", "severity": "structural", "problem": "Detail rows have equal weight. ID, Image, Saved and URL labels, values and links are all one size, and the long Saved sentence breaks the key-value rhythm. The Resources table is a second equal-weight block and the page leaves its lower half empty. The detail has little operational context such as a snapshot or diff summary.", "fix": "Put state, uptime and last snapshot in a header strip, add copy buttons on IDs and the image, and place snapshots and diff above the fold with a compact timeline."}, {"screen": "app-detail-dark.desktop.dark", "location": "Page chrome", "severity": "systemic", "problem": "The product has no top bar or workspace context. Navigation, search, status and account all sit in the left rail, and there are no breadcrumbs or org name. Compared with boxd and Beam, global state and the environment are invisible.", "fix": "Add a slim top bar with account or org, the region/AWS account, and a health or version indicator; keep the rail for navigation only."}, {"screen": "app-dialog-dark.desktop.dark", "location": "Delete dialog", "severity": "structural", "problem": "Delete is red and enabled while the typed name does not match, with the error shown below. The destructive action gives no disabled state. The pink error text is small and low in contrast on the dark panel. The dialog does not repeat any identifying facts such as the workspace ID or snapshot count.", "fix": "Disable Delete until the name matches, make the error text AA contrast with an icon, and show the workspace ID and the size of what is lost."}, {"screen": "app-dialog-light.desktop.dark", "location": "Light-mode neutrals and buttons", "severity": "systemic", "problem": "In light mode the grey tonal buttons, grey code block and grey row-hover fills are close in value to the page, so the buttons look disabled. The pale selected nav item and the grey Cancel are nearly identical.", "fix": "Give buttons a defined 1px border and a darker fill than the surface, and use a clearer selected state in the nav, such as an accent bar."}, {"screen": "app-list-dark.desktop.dark", "location": "Filter bar: input and state counts", "severity": "cosmetic", "problem": "The Running, Stopped and Failed counts look like legend text, not filters. They have no active or hover affordance, and the search input has a faint border and weak contrast.", "fix": "Make them toggle chips with a pressed state and counts in tabular monospace, and give the input a visible border and a focus ring."}, {"screen": "app-list-dark.desktop.dark", "location": "Narrow layouts", "severity": "systemic", "problem": "Only the desktop viewport is shown. The 240px sidebar and the one-line rows (name, long image path, resources, updated, action) have no visible collapsed or stacked treatment, so narrow widths are undesigned.", "fix": "Define a rail that collapses to icons, and a row layout that stacks the image and resources under the name below about 900px, with truncation and copy for the image."}, {"screen": "app-list-dark.desktop.dark", "location": "Toast", "severity": "cosmetic", "problem": "The bottom-right toast mixes a small mono 'done' label with a sans/mono sentence ('Snapshot clean published') and has no dismiss control or link to the snapshot.", "fix": "Use one type style for the message, link to the snapshot or its diff, and add a dismiss button."}, {"screen": "app-detail-dark.desktop.dark", "location": "Overall palette and identity", "severity": "systemic", "problem": "Neutral black with a flat blue accent and small grey type is clean but close to generic dashboard styling. The brand's layered blue/cyan identity shows only in the logo, so the surfaces, borders and corners say little about Spinbox.", "fix": "Take a distinctive element from the brand, such as a layered-disc motif for snapshot or diff states, a signature accent tone, or tighter squared corners, and apply it to the selected nav, tabs and snapshot timeline."}, {"screen": "dashboard-dark.desktop.dark", "location": "Workspaces table, state column", "severity": "structural", "problem": "State is a small coloured dot or shape plus a monospace word. Nothing else in the row carries it: there are no row actions, and the failed and waiting rows look like the others. The table has no ID, image, size or uptime columns, so it is far less dense than boxd's machine rows. The Open and Start actions from the mobile list are missing.", "fix": "Add monospace ID, image and age columns and a right-aligned per-row primary action with an overflow menu. Give failed rows a subtle left bar or tint, so state is not carried by hue alone."}, {"screen": "dashboard-dark.desktop.dark", "location": "Delete confirmation dialog", "severity": "structural", "problem": "The dialog sits inline below the table, with no scrim and no focus isolation, so it reads as a content card. The copy says 'until its days are up' with no number. The workspace name is not set in monospace, and nothing makes the user confirm the target.", "fix": "Render it as a modal over a scrim, with a concrete retention period and the name in a mono chip. Keep Delete in the destructive red, placed last."}, {"screen": "dashboard-dark.desktop.dark", "location": "Left navigation", "severity": "systemic", "problem": "The selected item is only a faint background and the sidebar is the same colour as the content. Navigation, content and detail blur together. There is no user, org or search area like those in Beam and boxd.", "fix": "Give the sidebar its own surface and a 2px accent marker on the selected item. Add a footer with the user, org and status."}, {"screen": "dashboard-dark.desktop.dark", "location": "Toast and dialog in one row", "severity": "cosmetic", "problem": "The toast is placed beside the dialog in the document flow. It looks like a content block, and its left edge doesn't line up with anything.", "fix": "Fix the toast to a corner overlay with consistent elevation."}, {"screen": "app-list-dark.mobile.dark", "location": "Mobile workspaces list", "severity": "structural", "problem": "Two unlabeled square controls sit next to the primary button. There are no ID, image or time metadata under the names. The list leaves roughly half the screen empty. The state chips (Running 2, and so on) look like filters but give no sign that they are.", "fix": "Add a second line of monospace image and ID plus the last snapshot time to each row. Make the state chips obviously filter toggles with a selected style."}, {"screen": "dashboard-light.mobile.dark", "location": "Mobile table", "severity": "structural", "problem": "The table is squeezed rather than redesigned. Host values wrap to 'runner-/a1', 'agent-/1' breaks across two lines, and the state text wraps. The ID in the first column is unreadable.", "fix": "On narrow screens, switch to stacked rows with name and state on the first line and host and snapshot as a mono second line, or scroll horizontally with a sticky name column."}, {"screen": "site-dark.desktop.dark", "location": "Hero", "severity": "cosmetic", "problem": "The mark's ground shadow turns into a pale ring in the neutral dark theme and looks pasted on. The headline is a very large monospace in a wide measure. The hero takes about 400px of height before any docs content appears.", "fix": "Make the shadow transparent or tone it to the surface. Cap the headline size and measure, and tighten the vertical padding."}, {"screen": "site-dark.desktop.dark", "location": "Docs content, caution and table", "severity": "systemic", "problem": "The caution callout, the code blocks and the table sit on near-identical surfaces. The table has no container, and it touches the callout above it with no spacing. The warning is separated only by a thin amber bar.", "fix": "Give callouts their own tint and an icon or shape. Add a margin above the table and give it a header surface."}, {"screen": "palette-dark.desktop.dark", "location": "Palette, Actions and States", "severity": "systemic", "problem": "The warning, error and success hues are bright pastels (pink #F286C2 for failed) and are close in luminance. Failed pink reads as a soft, friendly colour rather than an error. The red destructive colour and the pink error colour differ, so failure and deletion use two different reds.", "fix": "Use a true red for error that shares a family with destructive. Reserve the state colours for chips with a shape and a label."}, {"screen": "palette-dark.desktop.dark", "location": "Surfaces", "severity": "systemic", "problem": "The page, surface, raised and overlay steps are only a few lightness points apart (#0E1013 to #262B32). In the app, nested areas such as cards, code and the dialog barely separate and rely on a hairline border.", "fix": "Widen the steps between the surface layers, or use consistently stronger borders between layers."}, {"screen": "site-light.desktop.dark", "location": "Light mode docs", "severity": "cosmetic", "problem": "The sidebar selected-item tint, the banner and the code blocks use similar pale blue-grey. The on-this-page links are low contrast against white. The mono eyebrow labels are dark blue and look different from the dark theme's tones.", "fix": "Darken the muted text to reach AA. Differentiate the banner from the code surfaces."}, {"screen": "app-list-dark.mobile.dark", "location": "Row actions", "severity": "cosmetic", "problem": "Action buttons differ in width per row ('Open terminal', 'View logs', 'Start'), so their left edges are ragged. The overflow dots have no visible hit area. Nothing shows where delete lives.", "fix": "Use a fixed action column width and a bordered 32px overflow button that holds Delete in red."}]}
+{"round": 3, "reviewer": "codex/gpt", "kind": "full", "scores": {"hierarchy": 7, "typography": 7, "spacing": 8, "color": 7, "consistency": 7, "density": 8, "usability": 7, "polish": 7, "responsive": 3, "identity": 7}, "control": {"hierarchy": 7, "typography": 7, "spacing": 7, "color": 7, "consistency": 6, "density": 7, "usability": 7, "polish": 7, "responsive": 3, "identity": 7}, "pairs": [{"screen": "app-list-dark.desktop.dark", "winner": "baseline", "reason": "The navy foundation and cyan operational actions give the list clearer product-specific hierarchy than the flatter near-black treatment."}, {"screen": "app-list-light.desktop.dark", "winner": "baseline", "reason": "Semantic action styling makes terminal access and start/stop controls far easier to scan in the dense light-mode table."}, {"screen": "app-detail-dark.desktop.dark", "winner": "candidate", "reason": "The neutral dark canvas separates the information groups and connect panel more effectively than blue-on-blue surfaces."}, {"screen": "app-detail-light.desktop.dark", "winner": "baseline", "reason": "Leading with the primary \u201cOpen terminal\u201d action establishes a clearer, safer action order."}, {"screen": "app-dialog-dark.desktop.dark", "winner": "candidate", "reason": "The neutral charcoal modal has better surface separation while retaining a strongly recognizable destructive action."}, {"screen": "app-dialog-light.desktop.dark", "winner": "baseline", "reason": "The outlined Cancel button preserves a clearer secondary-action hierarchy beside Delete."}, {"screen": "app-list-dark.mobile.dark", "winner": "candidate", "reason": "Neutral, consistently sized row actions preserve the status hierarchy and avoid making routine terminal access look like the primary page action."}, {"screen": "site-dark.desktop.dark", "winner": "baseline", "reason": "The navy surface system is more cohesive and deliberate across the documentation shell, panels, and code samples."}, {"screen": "site-light.desktop.dark", "winner": "tie", "reason": "The two light-mode documentation renderings are visually indistinguishable."}, {"screen": "dashboard-dark.desktop.dark", "winner": "baseline", "reason": "The selected navigation, confirmation surface, and success toast use the same restrained dark-blue system, giving the operational view stronger cohesion."}, {"screen": "dashboard-light.mobile.dark", "winner": "baseline", "reason": "The destructive confirmation gives Delete clear priority while retaining a clean, accessible mobile action layout."}, {"screen": "palette-dark.desktop.dark", "winner": "baseline", "reason": "Its palette establishes a coherent dark navy product environment with clearer relationships between page, surface, overlay, and interaction states."}], "findings": [{"screen": "app-list-dark.desktop.dark", "location": "Workspace-list row actions", "severity": "structural", "problem": "Running rows expose Open terminal, Stop, and VS Code as three similarly prominent adjacent actions, making quick scanning harder in the densest part of the product.", "fix": "Keep Open terminal as the sole visible primary action; move Stop and VS Code into a consistent overflow or compact secondary action pattern."}, {"screen": "app-list-dark.desktop.dark", "location": "Workspace-list header, right side", "severity": "structural", "problem": "Two unlabeled square controls appear before New workspace without visible meaning, creating ambiguity in the page\u2019s primary control area.", "fix": "Use recognizable icons with tooltips and active-state labels, or replace them with text controls when their meaning is important."}, {"screen": "app-list-light.desktop.dark", "location": "Light-mode workspace rows", "severity": "systemic", "problem": "The cyan filled operational buttons are much more visually dominant than row names and states, so the table reads as a field of actions rather than a workspace inventory.", "fix": "Reserve filled accent treatment for the page-level creation action; use quieter outlined or text actions at row level, retaining color for status."}, {"screen": "app-detail-dark.desktop.dark", "location": "Workspace-detail header actions", "severity": "structural", "problem": "Stop is visually presented as an ordinary gray secondary control with no caution signal despite being a disruptive operational action.", "fix": "Use a warning-toned outline or an explicit pause/stop semantic treatment while keeping terminal access as the primary action."}, {"screen": "app-detail-dark.desktop.dark", "location": "Detail and Resources sections", "severity": "cosmetic", "problem": "The two-column key/value layout has substantial unused horizontal space while long image identifiers remain visually dense.", "fix": "Constrain each definition list to a readable max width and give identifier values a dedicated flexible column with truncation/copy affordance."}, {"screen": "app-detail-light.desktop.dark", "location": "Light-mode connect code block", "severity": "systemic", "problem": "The pale blue-gray code surface is noticeably stronger than the surrounding neutral panels and does not map cleanly to the dark-mode code treatment.", "fix": "Define a shared code-surface token for both themes with a subtler neutral tint and distinct border, preserving monospace contrast."}, {"screen": "app-dialog-dark.desktop.dark", "location": "Delete confirmation dialog", "severity": "structural", "problem": "Delete remains fully enabled while the entered workspace name is visibly invalid, weakening the confirmation safeguard.", "fix": "Disable Delete until the exact name matches and provide the mismatch message as the blocking explanation."}, {"screen": "app-dialog-dark.desktop.dark", "location": "Delete confirmation dialog", "severity": "cosmetic", "problem": "The dialog\u2019s charcoal surface is close to the page canvas, so its containment relies heavily on a thin border.", "fix": "Increase modal elevation through a slightly lighter elevated surface and a softer, broader shadow while maintaining the restrained console palette."}, {"screen": "app-dialog-light.desktop.dark", "location": "Delete confirmation dialog", "severity": "cosmetic", "problem": "Cancel is filled gray in light mode whereas it is outlined in the preferred dark treatment, producing inconsistent secondary-button hierarchy across themes.", "fix": "Standardize Cancel as an outlined secondary action in both themes; reserve filled treatments for the destructive confirmation."}, {"screen": "app-list-dark.mobile.dark", "location": "Mobile workspace rows", "severity": "structural", "problem": "Rows omit column labels and place state, action, and overflow controls in a compressed horizontal pattern; scanning many workspaces will become ambiguous as metadata grows.", "fix": "Use a compact stacked row on narrow screens: workspace and state first, contextual metadata below, then actions in a dedicated trailing or expanded action region."}, {"screen": "app-list-dark.mobile.dark", "location": "Bottom success toast", "severity": "cosmetic", "problem": "The toast is visually isolated at the very bottom with substantial unused space above it, making it feel disconnected from the snapshot action it confirms.", "fix": "Anchor transient feedback nearer the affected action or use a standard viewport corner with consistent offsets."}, {"screen": "site-dark.desktop.dark", "location": "Documentation article body", "severity": "structural", "problem": "The central prose column is constrained while the right table-of-contents column has large unused space, creating an inefficient reading layout.", "fix": "Widen the article column modestly or make the right rail sticky and narrower so line lengths and available width are better balanced."}, {"screen": "site-dark.desktop.dark", "location": "Hero illustration and headline", "severity": "cosmetic", "problem": "The large illustration competes slightly with the already dominant multi-line monospace headline.", "fix": "Reduce illustration scale or lower its contrast so the product proposition remains the undisputed hero focal point."}, {"screen": "dashboard-dark.desktop.dark", "location": "Delete confirmation panel", "severity": "structural", "problem": "The confirmation is embedded beneath the table rather than blocking or clearly overlaying the destructive decision context, so it can be mistaken for another dashboard card.", "fix": "Present deletion in a modal/dialog layer with a focused title, clear target name, and keyboard focus management; retain the inline result only after confirmation."}, {"screen": "dashboard-dark.desktop.dark", "location": "Dashboard toolbar", "severity": "cosmetic", "problem": "Search, New workspace, and Refresh have similar visual weight despite distinctly different roles.", "fix": "Keep New workspace primary, soften Refresh to tertiary styling, and let the search field carry its input affordance without competing borders."}, {"screen": "dashboard-light.mobile.dark", "location": "Mobile confirmation panel", "severity": "structural", "problem": "The dialog uses a desktop-card treatment inside the content flow, which makes it easy to scroll past and leaves its relationship to the selected workspace unclear.", "fix": "Use a responsive modal or bottom sheet with an explicit selected-workspace reference and a persistent decision area."}, {"screen": "palette-dark.desktop.dark", "location": "Palette laboratory, Actions row", "severity": "systemic", "problem": "Accent and accent-text are the same cyan value, obscuring the distinction between an action fill and text placed on that fill.", "fix": "Define separate tokens: an accessible dark foreground for text/icons on cyan actions and a cyan token for links or action surfaces as appropriate."}, {"screen": "palette-dark.desktop.dark", "location": "Palette laboratory, Content row", "severity": "systemic", "problem": "The bright teal label token has a stronger visual pull than ordinary content hierarchy and risks being overused as decorative emphasis.", "fix": "Reserve the label token for small metadata/eyebrows and add a muted label variant for routine secondary metadata."}]}
+{"round": 3, "reviewer": "agy/gemini-3.8-flash-high", "kind": "full", "scores": {"color": 8, "consistency": 8, "density": 8, "hierarchy": 8, "identity": 8, "polish": 8, "responsive": 7, "spacing": 8, "typography": 8, "usability": 8}, "control": {"color": 5, "consistency": 5, "density": 6, "hierarchy": 5, "identity": 5, "polish": 5, "responsive": 5, "spacing": 6, "typography": 6, "usability": 6}, "pairs": [{"screen": "app-list-dark.desktop.dark", "winner": "candidate", "reason": "Neutral dark background and restrained secondary row actions with an overflow menu establish clear visual hierarchy, avoiding the visual noise of repetitive bright cyan buttons."}, {"screen": "app-list-light.desktop.dark", "winner": "candidate", "reason": "Disciplined hierarchy with an authoritative dark primary CTA and subdued row actions with overflow menus, avoiding repetitive saturated teal buttons across the list."}, {"screen": "app-detail-dark.desktop.dark", "winner": "candidate", "reason": "Neutral charcoal surface provides superior contrast and elevates 'Open terminal' as the primary action alongside an overflow menu for full lifecycle management."}, {"screen": "app-detail-light.desktop.dark", "winner": "candidate", "reason": "Clear action hierarchy prioritizing 'Open terminal' over secondary controls, complemented by an overflow menu for operational completeness."}, {"screen": "app-dialog-dark.desktop.dark", "winner": "candidate", "reason": "High-contrast neutral dark card elevates cleanly above the backdrop scrim without bright cyan background buttons distracting from the destructive confirmation."}, {"screen": "app-dialog-light.desktop.dark", "winner": "candidate", "reason": "Calm light-mode backdrop dimming and restrained table rows focus complete user attention on the destructive deletion confirmation."}, {"screen": "app-list-dark.mobile.dark", "winner": "candidate", "reason": "Version b provides contextual row actions including 'View logs' for failures and overflow menus for secondary operations with quiet surface styling, whereas version a omits failure actions and overuses loud cyan buttons."}, {"screen": "site-dark.desktop.dark", "winner": "candidate", "reason": "Version b employs a neutral dark slate background and restrained category kickers matching high-end developer consoles (Beam/boxd), whereas version a has an oversaturated navy cast with distracting neon cyan labels."}, {"screen": "site-light.desktop.dark", "winner": "tie", "reason": "Both versions present identical typography, spacing, surface colors, callouts, and table structure in light mode with no discernible differences."}, {"screen": "dashboard-dark.desktop.dark", "winner": "candidate", "reason": "Version b uses cohesive neutral dark surfaces (#0E1013), high-contrast button styling (#1B7DB4 with white text), and places Cancel before Delete, avoiding the harsh dark-on-cyan button and navy wash of version a."}, {"screen": "dashboard-light.mobile.dark", "winner": "candidate", "reason": "Version b follows standard defensive modal patterns by placing Cancel before Delete, maintaining cross-breakpoint consistency and preventing accidental workspace deletion."}, {"screen": "palette-dark.desktop.dark", "winner": "candidate", "reason": "Version a defines a disciplined neutral dark surface hierarchy (#0E1013 to #262B32) and professional blue accent (#1B7DB4) with white text, whereas version b uses oversaturated navy surfaces and neon cyan."}], "findings": [{"fix": "Disable the Delete button until the input value exactly matches the target workspace name.", "location": "Modal action buttons", "problem": "The destructive 'Delete' button remains visually active and enabled even when the typed workspace name fails validation.", "screen": "app-dialog-dark.desktop.dark", "severity": "broken"}, {"fix": "Add a subtle low-contrast table header row with column labels and sort triggers.", "location": "Workspaces table header area", "problem": "The table lacks column headers, leaving data columns (specs, images, timestamps) without structural alignment anchors across wide viewports.", "screen": "app-list-dark.desktop.dark", "severity": "structural"}, {"fix": "Convert status counts into interactive filter pills with explicit active, inactive, and 'All' states.", "location": "Status filter summary above table", "problem": "Status counts ('Running 2', 'Stopped 1', 'Failed 1') lack interactive filter affordances or an 'All' toggle option.", "screen": "app-list-dark.desktop.dark", "severity": "structural"}, {"fix": "Separate 'Stop' into a secondary action group or place it inside the overflow menu, keeping 'Open terminal' prominently isolated.", "location": "Workspace header action toolbar", "problem": "Destructive/interruptive 'Stop' button is placed directly adjacent to primary CTA 'Open terminal', risking accidental shutdown.", "screen": "app-detail-dark.desktop.dark", "severity": "structural"}, {"fix": "Render uncommitted disk writes with an amber warning badge and an inline 'Publish snapshot' action button.", "location": "Details card, 'Saved' metadata row", "problem": "Critical dirty disk state ('the last 40 s of writes, 17.7 MB, is not') is displayed as passive neutral text without warning cues or a snapshot action.", "screen": "app-detail-dark.desktop.dark", "severity": "structural"}, {"fix": "Move snapshot count to Details metadata or directly into the Snapshots tab header.", "location": "Resources card", "problem": "Listing 'Snapshots: 3' under hardware 'Resources' conflates compute provisioning (CPU, RAM, Disk) with point-in-time backup history.", "screen": "app-detail-dark.desktop.dark", "severity": "structural"}, {"fix": "Add a copy-to-clipboard button in the upper-right corner of the code block with visual feedback on copy.", "location": "Connect card code snippet container", "problem": "The CLI connection command block lacks a dedicated one-click copy button.", "screen": "app-detail-dark.desktop.dark", "severity": "systemic"}, {"fix": "Use a semantic status badge with an indicator dot (e.g. neutral/muted for offline, green for online) to indicate network state.", "location": "Details card, 'Network' row", "problem": "Network 'offline' state is rendered in unstyled plain text, failing to communicate network connectivity semantics.", "screen": "app-detail-dark.desktop.dark", "severity": "systemic"}, {"fix": "Add helper text or modal validation clarifying that instance resizing requires a stop/restart cycle.", "location": "Resources card header, 'Change size' button", "problem": "The 'Change size' button does not indicate whether resizing requires the VM to be stopped, risking unexpected disruption.", "screen": "app-detail-dark.desktop.dark", "severity": "systemic"}, {"fix": "Add a dismiss trigger and a direct link to view the snapshot in the Snapshots tab.", "location": "Bottom-right floating notification", "problem": "The snapshot completion toast ('\u2022 done Snapshot clean published') lacks a dismiss button and an actionable link to inspect the snapshot.", "screen": "app-list-dark.desktop.dark", "severity": "systemic"}, {"fix": "Clearly delineate global command palette search from local table filtering using distinct styling and keyboard affordances.", "location": "Sidebar navigation vs table toolbar", "problem": "Global search ('Search \u2318K') in the sidebar sits directly beside the local table filter ('Name or image'), creating ambiguous search scoping.", "screen": "app-list-dark.desktop.dark", "severity": "structural"}, {"fix": "Brighten the validation message color to high-contrast rose/amber to satisfy WCAG AAA on dark surfaces.", "location": "Confirmation modal input validation error", "problem": "The validation error text ('The name does not match agent-1.') has marginal contrast against the dark modal surface.", "screen": "app-dialog-dark.desktop.dark", "severity": "cosmetic"}, {"fix": "Reflow the mobile table into a single-column stacked card list layout (similar to s01-b) where machine name, state badge, host, and last snapshot occupy structured vertical rows rather than rigid table columns.", "location": "Workspaces data table columns (Name, State, Host)", "problem": "On narrow mobile viewports, the desktop 4-column data table is retained without reflowing or horizontal scrolling, causing table cells to compress and text to break awkwardly across hyphenated fragments (e.g. 'agent-' / '1', 'runner-' / 'a1', and 'waiting' / '\u25c6 for a' / 'host').", "screen": "dashboard-light.mobile.dark", "severity": "structural"}, {"fix": "Render destructive confirmation dialogs inside a centered modal overlay with a semi-transparent dark backdrop scrim that traps focus and clearly separates modal interaction from page content.", "location": "Confirmation dialog ('Delete agent-1?')", "problem": "The destructive confirmation dialog is rendered as an un-anchored inline card directly below the table rows rather than an overlay modal with a backdrop scrim, creating layout confusion and competing with table content.", "screen": "dashboard-dark.desktop.dark", "severity": "structural"}, {"fix": "Relocate toast notifications to a fixed global notification container anchored to the bottom-right corner of the viewport with a high z-index and auto-dismiss behavior.", "location": "Notification toast placement relative to confirmation dialog", "problem": "The notification toast ('\u25cf done Snapshot clean published') is rendered inline in the document body immediately adjacent to the confirmation dialog rather than within a dedicated, fixed-position notification viewport anchor.", "screen": "dashboard-dark.desktop.dark", "severity": "structural"}, {"fix": "Add a compact secondary metadata row underneath each workspace name in monospace font (e.g. 'runner-a1 \u00b7 2 min ago') before the action button group.", "location": "Workspaces list row items", "problem": "In mobile list view (s01-b), machine metadata such as assigned host (runner-a1), hardware specs, and last snapshot timestamp are completely missing, forcing users to navigate blindly without knowing machine context.", "screen": "app-list-dark.mobile.dark", "severity": "systemic"}, {"fix": "Reserve the monospaced typeface strictly for code blocks, terminal snippets, IDs, and runner names; use a refined, geometric or grotesque sans-serif with tighter tracking for display and page titles.", "location": "Hero display headline ('A virtual machine per workspace. A snapshot before, a diff after.')", "problem": "Using a wide slab/monospaced typewriter typeface for large display headlines creates uneven typographic color, wide tracking gaps, and awkward visual rhythm on marketing and documentation headers.", "screen": "site-dark.desktop.dark", "severity": "systemic"}, {"fix": "Add an 'Actions' column on desktop rows with quick-action buttons (Terminal/Start) and a three-dots menu for secondary actions (Snapshot, Diff, Delete), matching the affordances provided in the mobile view (s01-b).", "location": "Workspaces table header and row action area", "problem": "The desktop workspaces table lacks direct operational action triggers (such as Terminal, Start/Stop, Snapshot, or overflow '...') on the rows themselves; actions only appear when selecting or through contextual modals.", "screen": "dashboard-dark.desktop.dark", "severity": "systemic"}, {"fix": "Add a 3px semantic border accent (e.g., border-left: 3px solid #4CC38A) matching the status color of the notification.", "location": "Toast notification ('Snapshot clean published')", "problem": "On mobile, the toast notification lacks a semantic status color accent on its container border, relying solely on a small green dot icon which reduces at-a-glance state feedback.", "screen": "dashboard-light.mobile.dark", "severity": "cosmetic"}, {"fix": "Give the warning banner a distinct border and slightly warmer warning tint (#E3B04B at 10% opacity) or subtle amber left accent line to indicate experimental/prerelease status clearly.", "location": "Documentation site top banner ('Spinbox is experimental...')", "problem": "The experimental warning banner spans the full width with a low-contrast dark cyan fill that visually merges into the header rather than appearing as a distinct system callout.", "screen": "site-dark.desktop.dark", "severity": "cosmetic"}, {"fix": "Add a keyboard shortcut badge (e.g. '\u2318K') inside the search input and provide quick filter chips for common workspace states (Running, Stopped, Failed).", "location": "Search workspaces input bar", "problem": "The search bar lacks a keyboard shortcut affordance (e.g. '\u2318K' or '/'), search scope indicator, or quick filters, lagging behind developer console standards set by Beam and boxd.", "screen": "dashboard-dark.desktop.dark", "severity": "systemic"}, {"fix": "Align the palette tokens in s06 with the neutral dark architecture from s06-a: set surface-page to #0E1013, primary accent to #1B7DB4, on-accent to #FFFFFF, and remove saturated blue tints from neutral surfaces.", "location": "Palette laboratory (version b definition)", "problem": "In s06-b, the palette defines an oversaturated navy page (#111827) and neon cyan accent (#27BFE8 with dark #111827 text), conflicting directly with the neutral charcoal (#0E1013) and blue-with-white-text (#1B7DB4) system successfully applied across s01-b, s02-b, and s04-b.", "screen": "palette-dark.desktop.dark", "severity": "broken"}]}
+{"round": 4, "reviewer": "claude/sonnet", "kind": "full", "scores": {"hierarchy": 7, "typography": 7, "spacing": 7, "color": 7, "consistency": 7, "density": 6, "usability": 7, "polish": 7, "responsive": 3, "identity": 5}, "control": {"hierarchy": 6, "typography": 7, "spacing": 7, "color": 5, "consistency": 6, "density": 6, "usability": 6, "polish": 6, "responsive": 3, "identity": 7}, "pairs": [{"screen": "app-list-dark.desktop.dark", "winner": "candidate", "reason": "The neutral dark version has one quiet button per row with actions in an overflow menu, so state dots and monospace image names read first. The navy and cyan version puts three loud buttons on every row."}, {"screen": "app-list-light.desktop.dark", "winner": "candidate", "reason": "The light neutral version is calmer. It uses a single action plus a menu per row, and state colours stand out against grey. The other version fills every row with dark blue filled buttons."}, {"screen": "app-detail-dark.desktop.dark", "winner": "candidate", "reason": "Stop sits apart from the primary Open terminal button, and the neutral charcoal surfaces let the data and the one blue accent carry the hierarchy. The navy version with cyan is more generic and noisier."}, {"screen": "app-detail-light.desktop.dark", "winner": "candidate", "reason": "The neutral light version has a grey code block and a grey Stop button, and the single blue accent marks the primary action. The tinted blue code block and the heavy filled button compete with the data."}, {"screen": "app-dialog-dark.desktop.dark", "winner": "candidate", "reason": "The dialog surface is clearly lifted from the scrim, and the red Delete button reads as destructive against neutral Cancel. The navy version's dialog blends into the dimmed background."}, {"screen": "app-dialog-light.desktop.dark", "winner": "candidate", "reason": "A neutral scrim with a white dialog gives clean separation. The red Delete is distinct from the blue accent and the grey Cancel is clearly secondary. The navy-tinted scrim looks muddy."}, {"screen": "app-list-dark.mobile.dark", "winner": "candidate", "reason": "B gives every row a consistent action column, with View logs on the failed row and a \u22ef overflow menu. A leaves ci-job without an action. B's accent is calmer and its surface separates better."}, {"screen": "site-dark.desktop.dark", "winner": "candidate", "reason": "B's neutral near-black page, calmer blue accent and quieter selected state read as an infrastructure console, close to Beam and boxd. A's saturated navy and cyan feel like a generic SaaS site."}, {"screen": "site-light.desktop.dark", "winner": "tie", "reason": "The two light versions are nearly identical. A has bluer tinted code and callout surfaces and B has greyer ones. The accent and contrast are the same."}, {"screen": "dashboard-dark.desktop.dark", "winner": "baseline", "reason": "B's neutral surfaces and muted blue primary leave the state colours as the only strong hues. B also gives the toast a left status edge and sets the dialog title in monospace. A's cyan primary competes with the status colours."}, {"screen": "dashboard-light.mobile.dark", "winner": "baseline", "reason": "B separates accent (#1B7DB4) from accent-text (#6CC6EE) and uses a white on-accent, so contrast is deliberate. A reuses one cyan for fill and text. B's neutral surfaces step more clearly."}, {"screen": "palette-dark.desktop.dark", "winner": "tie", "reason": "The layouts are the same and the narrow table wraps identically. B has a monospace title and a stronger dialog shadow, but its Delete button touches Cancel. A's order and spacing are more conventional."}], "findings": [{"screen": "app-list-dark.desktop.dark", "location": "Workspace rows, image column", "severity": "structural", "problem": "The image column is centred or floats at an inconsistent x between rows (ci-job and notes are offset from api). Long GHCR names also dominate the row, so name and state get less weight than the image.", "fix": "Use a fixed grid with left-aligned columns: name+state, image truncated in the middle with the tag kept, specs, updated. Right-align only numeric columns."}, {"screen": "app-list-dark.desktop.dark", "location": "Row actions", "severity": "structural", "problem": "Stop and Delete are hidden in the \u22ef menu, so the destructive operation has no visible affordance and the row has no visible keyboard route to it. Stop is also missing from the list that the detail page offers.", "fix": "Keep the primary action visible. Put Stop, Snapshot and Delete in the menu with a danger-styled Delete separated by a divider. Add a visible label or tooltip on the \u22ef trigger."}, {"screen": "app-list-dark.desktop.dark", "location": "State legend and filters", "severity": "systemic", "problem": "The filter chips use dot, ring and square shapes at very small size. Counts are set in tiny monospace, and the chips do not look clickable. The stopped ring is low contrast.", "fix": "Make the chips real toggle buttons with a border and selected state, and use larger tabular counts. Raise the contrast of the stopped glyph and text."}, {"screen": "app-detail-dark.desktop.dark", "location": "Header action cluster", "severity": "structural", "problem": "Open terminal sits on the right next to Stop and an unlabeled \u22ef. VS Code, a top workflow, has dropped out of the detail header. Stop is a neutral button and does not signal that it changes state.", "fix": "Show Open terminal and VS Code as the primary group. Put Stop in its own operational group, and move Delete into a clearly destructive section."}, {"screen": "app-detail-dark.desktop.dark", "location": "Overview body", "severity": "structural", "problem": "The Details and Resources tables look alike and are separated only by hairlines. The empty lower half of the page has no snapshot or diff summary, even though that is the product's core feature.", "fix": "Add a snapshots panel with a diff link and a state timeline. Tint or inset the code block as a CLI surface and add a copy button."}, {"screen": "app-detail-dark.desktop.dark", "location": "Status text", "severity": "cosmetic", "problem": "The 'running' label is tiny green monospace on charcoal, with a 12px dot. The ID and image line under the title is dim.", "fix": "Use a larger status pill with a fixed glyph and text. Raise the ID line to the secondary text token for AAA contrast."}, {"screen": "app-dialog-dark.desktop.dark", "location": "Dialog surface and fields", "severity": "systemic", "problem": "The scrim is only slightly darker than the page, and the dialog is barely lifted. The danger message is small pink text on charcoal, and the red Delete is enabled while the name field is invalid.", "fix": "Darken the scrim and add an elevation border. Disable Delete until the name matches, show the error with an icon, and make the field border red."}, {"screen": "app-dialog-light.desktop.dark", "location": "Light dialog", "severity": "cosmetic", "problem": "The error text is dim magenta at about 12px and the dialog has no destructive framing. The red Delete is the same visual weight as the primary blue.", "fix": "Add a danger icon and heading. Darken the error colour to AA+ and keep red reserved for destructive actions only."}, {"screen": "app-list-light.desktop.dark", "location": "Light surface and sidebar", "severity": "systemic", "problem": "The light theme has near-white surfaces with hairline borders, and the selected sidebar item is a pale grey block. Page, sidebar, rows and cards barely separate.", "fix": "Add a slightly tinted page background against white content surfaces. Give the active nav item an accent edge or stronger fill."}, {"screen": "c03", "location": "Docs site hero and shell", "severity": "systemic", "problem": "The docs site is navy with a saturated cyan accent, a glossy 3D logo and a huge monospace headline, and it is wrapped in a boxed frame with a 'THE SITE' label. It reads as a different brand from the neutral app.", "fix": "Unify palette and accent with the app tokens. Use a proportional display face for the headline and drop the frame around the page."}, {"screen": "c01", "location": "Navy cyan app variant", "severity": "systemic", "problem": "Cyan-filled buttons repeat on every row, so the accent loses meaning and is no longer a signal. The navy surfaces are also close in value to the borders.", "fix": "Reserve the accent for one primary action per view and render the rest as neutral secondary buttons."}, {"screen": "all", "location": "Narrow layouts", "severity": "structural", "problem": "No narrow or mobile layout is shown. The fixed 240px sidebar and wide row grid have no visible collapse, and rows with long image names would overflow.", "fix": "Design a collapsed icon rail or drawer below 900px. Reflow rows into stacked cards with name and state on top and the image on its own line."}, {"screen": "app-list-dark.mobile.dark", "location": "workspace list, narrow layout", "severity": "structural", "problem": "Each row shows only a name, a state and one button. There is no image, host or time, so it is far less dense than boxd's machine row.", "fix": "Add a second line per row with a monospace id, image, host and last-seen time. Keep the \u22ef menu for secondary actions."}, {"screen": "dashboard-dark.desktop.dark", "location": "delete confirmation dialog", "severity": "structural", "problem": "The dialog is an inline card under the table with no scrim. Delete is placed first and touches Cancel, nothing shows what is destroyed, and the body text is low-contrast.", "fix": "Make it a modal on a scrim. Put Cancel on the left and the destructive button on the right with a gap, and name the workspace id in monospace. Use the main text colour for the body."}, {"screen": "dashboard-dark.desktop.dark", "location": "workspaces table", "severity": "structural", "problem": "Every name is an underlined link, so the first column is noisy. The table has no actions column, no row hover or selection state, and the waiting diamond is tiny.", "fix": "Underline names on hover only. Add a right-hand actions column and a row hover state, and enlarge the state glyphs."}, {"screen": "palette-dark.desktop.dark", "location": "mobile table", "severity": "structural", "problem": "The desktop table is shrunk instead of redesigned. Host chips break mid-word (runner-/a1), the waiting state wraps onto three lines and the header wraps.", "fix": "Collapse each row into a stacked card or list with name, state and monospace metadata, and never wrap ids or host chips."}, {"screen": "site-dark.desktop.dark", "location": "site shell and hero", "severity": "systemic", "problem": "The site sits inside a rounded lab frame labelled 'THE SITE'. The hero and its heavy monospace headline are very large, so the docs content starts far down the page.", "fix": "Remove the lab frame in product use. Reduce the hero height and headline size so the docs content shows above the fold."}, {"screen": "site-dark.desktop.dark", "location": "sidebar and On this page", "severity": "cosmetic", "problem": "The sidebar is a flat list with no groups. The 'On this page' items are small and low-contrast.", "fix": "Group the sidebar under section labels and raise the muted text contrast to at least 4.5:1."}, {"screen": "site-light.desktop.dark", "location": "light mode code blocks and callout", "severity": "systemic", "problem": "The code blocks and the Caution callout share nearly the same grey-blue fill, and the callout is marked only by a thin brown edge. The table follows with no gap.", "fix": "Give the callout its own warning tint and a label or icon, and add spacing before the table."}, {"screen": "dashboard-light.mobile.dark", "location": "palette lab surfaces", "severity": "systemic", "problem": "The page, surface and raised steps are close in value, so cards and the page blend in dark mode.", "fix": "Widen the lightness steps between page, surface and raised, and print contrast ratios on the swatches."}, {"screen": "dashboard-dark.desktop.dark", "location": "toast", "severity": "cosmetic", "problem": "The toast floats beside the dialog with no anchor, and it mixes monospace and sans inside one sentence.", "fix": "Pin toasts to a viewport corner. Set the message in one face and make only ids monospace."}, {"screen": "app-list-dark.mobile.dark", "location": "top actions row", "severity": "cosmetic", "problem": "Two unlabelled square icon buttons sit next to New workspace, and the narrow layout has no navigation or breadcrumb.", "fix": "Add tooltips or text labels to the icon buttons and a compact menu button to the narrow header."}, {"screen": "site-dark.desktop.dark", "location": "buttons", "severity": "cosmetic", "problem": "The site uses pill buttons and the dashboard uses squarer ones, so the corner radius is inconsistent between site and app.", "fix": "Pick one button radius token and use it in both."}]}
+{"round": 4, "reviewer": "codex/gpt", "kind": "full", "scores": {"hierarchy": 8, "typography": 7, "spacing": 8, "color": 8, "consistency": 8, "density": 8, "usability": 8, "polish": 8, "responsive": 4, "identity": 8}, "control": {"hierarchy": 7, "typography": 7, "spacing": 7, "color": 7, "consistency": 7, "density": 7, "usability": 7, "polish": 7, "responsive": 4, "identity": 8}, "pairs": [{"screen": "app-list-dark.desktop.dark", "winner": "baseline", "reason": "Its navy console surface and restrained cyan primary action establish a more distinctive, operationally focused system than the flatter charcoal treatment."}, {"screen": "app-list-light.desktop.dark", "winner": "baseline", "reason": "The blue action treatment makes start/terminal controls clearer and brings the light mode into a more coherent relationship with the dark console."}, {"screen": "app-detail-dark.desktop.dark", "winner": "candidate", "reason": "The action hierarchy is stronger: Stop is safely secondary, Open terminal is primary, and the overflow action removes low-value clutter."}, {"screen": "app-detail-light.desktop.dark", "winner": "candidate", "reason": "It preserves the clearer operational action ordering from the dark detail screen while retaining a clean light-mode surface."}, {"screen": "app-dialog-dark.desktop.dark", "winner": "baseline", "reason": "The navy dialog surface better belongs to the dark application and retains a clear destructive red Delete action."}, {"screen": "app-dialog-light.desktop.dark", "winner": "candidate", "reason": "The filled neutral Cancel button provides a more deliberate secondary-action affordance beside Delete."}, {"screen": "app-list-dark.mobile.dark", "winner": "candidate", "reason": "Operational actions are consistently sized and secondary, rather than dominating every row with saturated cyan."}, {"screen": "site-dark.desktop.dark", "winner": "candidate", "reason": "The neutral dark surfaces give the documentation a more serious console character and clearer surface hierarchy."}, {"screen": "site-light.desktop.dark", "winner": "tie", "reason": "The two light-mode renders are visually indistinguishable at the supplied resolution."}, {"screen": "dashboard-dark.desktop.dark", "winner": "baseline", "reason": "Selected navigation, destructive confirmation, and success feedback use clear, semantic emphasis."}, {"screen": "dashboard-light.mobile.dark", "winner": "baseline", "reason": "The mobile confirmation presents the destructive action with unambiguous priority and retains a compact, legible layout."}, {"screen": "palette-dark.desktop.dark", "winner": "candidate", "reason": "Its neutral dark palette, high-contrast action text, and restrained blue accent form the stronger system."}], "findings": [{"screen": "app-list-dark.desktop.dark", "location": "Workspace-list header, to the left of New workspace", "severity": "cosmetic", "problem": "Two unlabeled square controls compete with the primary creation action and have no visible grouping or explanatory context.", "fix": "Place these controls in a compact labeled toolbar or overflow menu, leaving New workspace as the only standalone header action."}, {"screen": "app-list-dark.desktop.dark", "location": "Workspace rows, right-side action area", "severity": "structural", "problem": "Rows use a mixture of full actions, state controls, a text link, and an absent overflow action, so the action model is uneven across workspace states.", "fix": "Standardize a row action slot: primary contextual action, state action where applicable, then a consistent overflow menu for secondary actions."}, {"screen": "app-list-light.desktop.dark", "location": "Workspace-list rows in light mode", "severity": "systemic", "problem": "The pale page, rules, and secondary text are close in value, weakening row boundaries and scanability compared with the dark mode.", "fix": "Increase separation between canvas, navigation selection, dividers, and muted text while preserving the sparse table-like layout."}, {"screen": "app-detail-dark.desktop.dark", "location": "Detail header actions", "severity": "structural", "problem": "Stop appears before Open terminal even though opening the running workspace is the dominant task; this reverses the primary-action reading order used elsewhere.", "fix": "Keep Open terminal first and visually primary, Stop second and neutral, then put lower-frequency actions in overflow."}, {"screen": "app-detail-dark.desktop.dark", "location": "Details and Resources sections", "severity": "cosmetic", "problem": "The two-column metadata layout leaves a large empty lower-right region before the Connect panel, making the information block feel less intentionally composed.", "fix": "Use the available right column for relevant contextual status or reduce the fixed two-column section height so Connect follows the metadata more tightly."}, {"screen": "app-detail-light.desktop.dark", "location": "Connect command block", "severity": "systemic", "problem": "The code panel is nearly the same light gray as secondary controls, reducing its terminal/code identity and visual separation from surrounding information.", "fix": "Use a distinct, higher-contrast code surface and preserve monospace text with a subtle border."}, {"screen": "app-dialog-dark.desktop.dark", "location": "Delete confirmation dialog", "severity": "structural", "problem": "Delete remains fully prominent while the entered workspace name visibly fails validation, creating avoidable destructive-action ambiguity.", "fix": "Disable Delete until the exact name matches, then enable it; retain the inline mismatch message."}, {"screen": "app-dialog-dark.desktop.dark", "location": "Dialog copy", "severity": "cosmetic", "problem": "\u201cIt goes to the trash with its disk; you can undelete it until its days are up\u201d is imprecise for an infrastructure product where retention behavior matters.", "fix": "State the concrete retention period and whether snapshots, disks, and network resources are retained or removed."}, {"screen": "app-dialog-light.desktop.dark", "location": "Delete confirmation backdrop", "severity": "cosmetic", "problem": "The gray overlay substantially mutes the page but does not give the destructive dialog a distinct warning context beyond its red button.", "fix": "Keep the neutral dimming overlay, but add a concise destructive-status cue in the dialog header or a restrained danger border/accent."}, {"screen": "app-list-dark.desktop.dark", "location": "Workspace states and summary counts", "severity": "systemic", "problem": "State color is consistent within the screen, but stopped is visually much weaker than running and failed, while the compact labels carry most of the differentiation.", "fix": "Increase non-color differentiation consistently with clearer state icons/badges and ensure muted stopped text meets the same contrast standard in both themes."}, {"screen": "app-list-dark.mobile.dark", "location": "Workspace-row overflow controls", "severity": "structural", "problem": "Every row exposes an unlabeled ellipsis control, but its menu purpose and available actions are not discoverable from the list.", "fix": "Use a recognizable kebab affordance with an accessible label and tooltip; reserve it for less-common actions while keeping state-appropriate primary actions visible."}, {"screen": "app-list-dark.mobile.dark", "location": "Failed workspace row", "severity": "structural", "problem": "The failed row has only \u201cView logs,\u201d whereas running and stopped rows expose immediate state-changing actions; recovery remains unclear from this primary operational view.", "fix": "Include the most likely recovery action, such as Retry or Restart, alongside View logs when that action is available."}, {"screen": "site-dark.desktop.dark", "location": "Documentation article, end of introductory paragraph", "severity": "broken", "problem": "A small clipped rectangular element appears directly after \u201cdisk back;\u201d, reading as a rendering or overflow defect.", "fix": "Remove the clipped artifact or render its intended control/content fully with a clear accessible label."}, {"screen": "site-dark.desktop.dark", "location": "Documentation body and right rail", "severity": "structural", "problem": "The main article is comparatively narrow while the right \u201cOn this page\u201d rail has very little content, producing substantial unused space in a dense technical document.", "fix": "Collapse the right rail when it has only a few anchors, or allocate more width to the reading and code column."}, {"screen": "dashboard-dark.desktop.dark", "location": "Delete confirmation", "severity": "structural", "problem": "The confirmation is embedded below the table rather than presented as a modal or clearly localized row state, making it easy to lose the target context in a larger workspace list.", "fix": "Use a modal/dialog with the workspace name and irreversible-effects summary, or anchor an inline confirmation directly to the selected row."}, {"screen": "dashboard-light.mobile.dark", "location": "Mobile workspace table", "severity": "structural", "problem": "The four-column table compresses host IDs and wraps the waiting status into multiple lines, weakening fast status scanning.", "fix": "At this breakpoint switch each workspace to a compact stacked row: name/state first, then host and snapshot metadata."}, {"screen": "palette-dark.desktop.dark", "location": "Palette token cards", "severity": "systemic", "problem": "Token swatches show names and hex values but do not communicate intended foreground/background pairings or contrast status, despite the stated AA requirement.", "fix": "Add approved text-pairing examples and a visible WCAG contrast result to each relevant surface and action token."}, {"screen": "palette-dark.desktop.dark", "location": "Typography controls and palette examples", "severity": "cosmetic", "problem": "The heavy monospace treatment is used broadly for display headings and labels, making hierarchy feel somewhat mechanical rather than selectively emphasizing identifiers and operational data.", "fix": "Keep monospace for IDs, commands, timestamps, and token names; use a complementary highly legible UI face for navigation and explanatory prose."}]}
+{"round": 6, "reviewer": "claude/sonnet", "kind": "full", "scores": {"hierarchy": 7, "typography": 6, "spacing": 7, "color": 7, "consistency": 7, "density": 6, "usability": 6, "polish": 7, "responsive": 4, "identity": 6}, "control": {"hierarchy": 7, "typography": 6, "spacing": 7, "color": 7, "consistency": 7, "density": 6, "usability": 6, "polish": 7, "responsive": 4, "identity": 6}, "pairs": [{"screen": "app-list-dark.desktop.dark", "winner": "candidate", "reason": "Accent is kept for the one primary action. Row actions are quiet secondary buttons with a \u22ef overflow, and state words are larger and legible. The other version puts a loud cyan button on every row."}, {"screen": "app-list-light.desktop.dark", "winner": "candidate", "reason": "The light list is calmer. Only New workspace is filled, and the rows read as data. The other version repeats solid blue buttons down every row."}, {"screen": "app-detail-dark.desktop.dark", "winner": "candidate", "reason": "Header actions have a clear hierarchy: Stop as a secondary button, a single filled Open terminal, and \u22ef for the rest. The other version has a bright cyan block, an outlined Stop and a bare VS Code link, which are loosely ranked."}, {"screen": "app-detail-light.desktop.dark", "winner": "candidate", "reason": "Same clear action hierarchy as the dark detail page, and the code block is a neutral surface. The other version's blue-tinted code block and doubled blue buttons are heavier."}, {"screen": "app-dialog-dark.desktop.dark", "winner": "candidate", "reason": "The scrim and the dialog surface separate clearly. The destructive red Delete is the only saturated element, with the rest of the page quiet. The other version's cyan buttons still glow behind the dialog."}, {"screen": "app-dialog-light.desktop.dark", "winner": "candidate", "reason": "Cancel is a neutral secondary button and Delete is a brighter red. The backdrop rows stay quiet, so the destructive action stands out."}, {"screen": "app-list-dark.mobile.dark", "winner": "candidate", "reason": "One primary action. Row actions are quiet, ci-job gets a contextual 'View logs', and each row has an overflow menu. The other version paints every row action in loud cyan, so nothing stands out as primary."}, {"screen": "site-dark.desktop.dark", "winner": "candidate", "reason": "The sans headline and the near-black neutral ground read better and sit calmer around the mark. The other version's oversized mono hero headline and uppercase mono labels slow reading, and the navy ground blends with its raised surfaces."}, {"screen": "site-light.desktop.dark", "winner": "candidate", "reason": "The light docs page is calm and readable, with a sans heading and a strong, restrained accent. The other version's mono hero and pill buttons feel stylised."}, {"screen": "dashboard-dark.desktop.dark", "winner": "candidate", "reason": "The dialog has Cancel on the left and the red Delete on the right. The state column is more legible, and Refresh is a quiet secondary action. The other version puts the destructive button first, and its 'Refresh' button competes with the primary."}, {"screen": "dashboard-light.mobile.dark", "winner": "candidate", "reason": "On narrow screens the dialog keeps Cancel and Delete clearly distinguished, and the state text is larger. The other version shrinks the mono state text and the wrapped 'waiting for a host' is tiny."}, {"screen": "palette-dark.desktop.dark", "winner": "candidate", "reason": "The palette is restrained, with distinct steps between surfaces, and accent and accent-text are separate tokens. In the other version accent-text equals accent, the surfaces are blue-tinted and close together, and cyan is used for labels."}], "findings": [{"screen": "app-list-dark.desktop.dark", "location": "Workspace rows, state column", "severity": "systemic", "problem": "State is a small dot or square plus a lowercase mono word. The stopped state is a hollow ring in muted grey. Nothing in the row shows how long a workspace has been running or any resource load. The list has less operational information than boxd, which shows CPU, memory and disk meters.", "fix": "Make the state a fixed-width badge with a distinct shape per state (filled, hollow, square) at 12\u201313px with a tinted background. Add compact usage meters or uptime in mono."}, {"screen": "app-list-dark.desktop.dark", "location": "Image column", "severity": "structural", "problem": "The image column is centered, not left-aligned. The text start shifts from row to row (545 vs 550 vs 555px). The long ghcr path is the loudest text in each row and pushes the name and state aside.", "fix": "Use fixed grid columns. Left-align the image, truncate the registry prefix with an ellipsis, and mute it so the name and state lead."}, {"screen": "app-list-dark.desktop.dark", "location": "Toolbar under the page title", "severity": "structural", "problem": "The filter counts (Running 2, Stopped 1, Failed 1) are plain text next to the search field. They don't read as clickable filters and have no selected state.", "fix": "Turn them into a segmented filter with a visible selected state and pressed styling, with the count in mono."}, {"screen": "app-detail-dark.desktop.dark", "location": "Header actions", "severity": "structural", "problem": "Stop sits beside Open terminal as an equal-weight button. VS Code, a top workflow, is hidden. Destructive actions such as delete are only in \u22ef with no visual distinction.", "fix": "Add VS Code as a secondary button. Move Stop and Delete into a grouped menu, with Delete separated and in the danger colour."}, {"screen": "app-detail-dark.desktop.dark", "location": "Details and Resources tables, Connect card", "severity": "systemic", "problem": "Details and Resources are two separate tables of near-identical weight, with a third boxed card below. The card, its code block and the page ground have nearly the same value, so surfaces blend. The code has no copy affordance.", "fix": "Use one step per surface level (ground, panel, inset code). Put key facts in a single header strip. Add a copy button to the code block."}, {"screen": "app-detail-dark.desktop.dark", "location": "Tab bar", "severity": "cosmetic", "problem": "Seven tabs sit at 12px with a thin underline. Snapshots and History, the core snapshot and diff workflow, get no more weight than Ports.", "fix": "Show counts on tabs (Snapshots 3) and increase the size and active contrast."}, {"screen": "app-dialog-dark.desktop.dark", "location": "Delete dialog", "severity": "structural", "problem": "The dialog is distinguished only by a slightly lighter navy and the scrim is faint. The consequence text is grey body copy. The Delete button is enabled while the typed name does not match.", "fix": "Darken the scrim. Name the consequence in the foreground colour. Disable Delete until the name matches. Show the name to type in mono, with a danger-tinted header or rule."}, {"screen": "app-list-dark.desktop.dark", "location": "Toast bottom right", "severity": "cosmetic", "problem": "The toast mixes mono and sans words (done, Snapshot, clean, published) and has no icon or dismiss control. The text is hard to parse, and the toast has low contrast against the ground.", "fix": "Use one face. Add a state icon and a close control, and raise the border contrast."}, {"screen": "app-list-dark.desktop.dark", "location": "Sidebar", "severity": "structural", "problem": "The sidebar has empty space between the five items and Settings, and no counts, org or environment indicator. Navigation is generic: Dashboard, Shared with me, Teams. There is no snapshots or resources view, and nothing that says 'in your AWS account'.", "fix": "Group nav into sections. Show the workspace count and the account/region context in the sidebar footer or header."}, {"screen": "app-list-light.desktop.dark", "location": "Light mode", "severity": "systemic", "problem": "In the light theme, the muted grey metadata (CPU/GB, Updated) and the grey image path are low-weight on white and probably near the AA limit. The pale blue active nav item is barely different from the ground.", "fix": "Darken the secondary text tokens to at least 7:1 for body-size mono. Strengthen the nav selected state with a left bar."}, {"screen": "app-list-dark.desktop.dark", "location": "Whole app, narrow widths", "severity": "broken", "problem": "The rows hold six columns plus an action, and no narrow or collapsed layout is shown. The sidebar is fixed at 240px, and the long image names will overflow or wrap.", "fix": "Define a stacked row under about 900px that keeps name, state and one action. Collapse the sidebar to a drawer and drop the image and resource columns into a second line."}, {"screen": "c01", "location": "Documentation site hero", "severity": "systemic", "problem": "The hero headline is set in a very large mono face at tight width and reads as heavy and wide. The eyebrow labels use cyan caps in mono. The whole site sits in a bordered, rounded frame on the navy ground, so it looks like a mockup, not a page. The docs table has no header contrast.", "fix": "Set the headline in the UI sans or a lighter weight with tighter tracking. Drop the outer frame and let the docs span the page. Strengthen the table header and use the same accent for the eyebrow and active nav."}, {"screen": "c02", "location": "Code and diff blocks on the docs page", "severity": "cosmetic", "problem": "The blocks have the same panel value as the page and caution callout, and the diff output (A/M/D) has no colour semantics. Inline code in the paragraph has a different border and fill from the blocks.", "fix": "Colour the A/M/D markers with the semantic added/modified/deleted tones, plus a letter. Unify inline and block code styling."}, {"screen": "dashboard-dark.desktop.dark", "location": "sidebar selected item 'Workspaces'", "severity": "systemic", "problem": "The selected row is a faint navy fill on a near-black ground, so the current location barely registers. The accent isn't used for navigation state.", "fix": "Add a 2px accent bar or a stronger accent-selected fill with accent-text, and keep the same treatment on the docs navigation."}, {"screen": "dashboard-dark.desktop.dark", "location": "workspaces table", "severity": "structural", "problem": "There is no row actions column, no ID or image column and no copyable mono IDs. Only the host is monospace, and the table looks like a plain list instead of an operational console like boxd.", "fix": "Add image, ID and age columns in mono, plus per-row open/stop/start and an overflow menu. Show a state count summary and tighter row density."}, {"screen": "dashboard-dark.desktop.dark", "location": "delete dialog and toast", "severity": "structural", "problem": "The dialog and the toast are placed inline under the table with no scrim, and the toast has no semantic border. The dialog doesn't name the workspace's consequence in mono.", "fix": "Render the dialog as a modal on a scrim, with the ID in mono. Pin the toast to a corner."}, {"screen": "app-list-dark.mobile.dark", "location": "top toolbar", "severity": "cosmetic", "problem": "Two placeholder squares sit beside 'New workspace', so the toolbar is unclear. Row action buttons are as heavy as the primary and name-less overflow dots have no label.", "fix": "Make row actions ghost or outline buttons, keep the filled button only for the primary, and add labels or tooltips to the icon buttons."}, {"screen": "dashboard-light.mobile.dark", "location": "table on narrow width", "severity": "structural", "problem": "On narrow screens the table wraps names and hosts ('agent-/1', 'runner-/a1', 'waiting for a host' in three lines), so IDs break. There is no card or stacked-row treatment.", "fix": "Below about 480px, switch to stacked rows with the name and state on line one and host and snapshot as a mono meta line. Keep identifiers on one line."}, {"screen": "site-dark.desktop.dark", "location": "hero", "severity": "systemic", "problem": "The mark is saturated cyan and blue on a neutral near-black page, and the page has almost no accent besides the primary button. The page reads as generic and the brand is carried only by the logo.", "fix": "Use the mark's cyan as the one accent for links, selected navigation, focus rings and code highlights. Give the hero a defined ground or border."}, {"screen": "palette-dark.desktop.dark", "location": "surfaces", "severity": "systemic", "problem": "The surface steps page, surface and raised (#0A0E16, #111723, #19212F) are very close, and the lines between them are low-contrast. Cards and the code blocks blend into the ground.", "fix": "Widen the lightness steps, or draw structure with border tokens. Check non-text boundary contrast of at least 3:1 for inputs."}, {"screen": "palette-dark.desktop.dark", "location": "muted and label tokens", "severity": "cosmetic", "problem": "The 'label' colour (#8FB3C6) is a separate blue-grey that is close to muted. It adds a third secondary text tone without a clear semantic role.", "fix": "Merge it into muted, or make it the accent tint."}, {"screen": "dashboard-dark.desktop.dark", "location": "state column", "severity": "cosmetic", "problem": "The state glyph shapes (circle, diamond, square) are tiny and the stopped state is a plain grey dot. Stopped and running are hard to tell apart at a glance in a long list.", "fix": "Enlarge the glyphs to at least 10px, use a hollow ring for stopped, and keep the glyphs identical across list, detail and toast."}, {"screen": "site-light.desktop.dark", "location": "docs page", "severity": "structural", "problem": "The page carries a huge hero above the docs and a code block with no copy control. Tables have no row hover or zebra, and the 'Caution' callout's amber stripe carries the only semantic signal, with no icon.", "fix": "Shrink the hero on docs pages and add a copy button to code blocks. Give callouts an icon and a label."}, {"screen": "site-dark.desktop.dark", "location": "banner", "severity": "cosmetic", "problem": "The experimental banner is a teal full-width strip in the same hue as the accent, so a warning looks like a brand decoration.", "fix": "Use the warning token with an icon, or a neutral surface with an amber border."}]}
+{"round": 6, "reviewer": "codex/gpt", "kind": "full", "scores": {"hierarchy": 8, "typography": 7, "spacing": 8, "color": 7, "consistency": 8, "density": 9, "usability": 8, "polish": 8, "responsive": 3, "identity": 7}, "control": {"hierarchy": 7, "typography": 7, "spacing": 7, "color": 7, "consistency": 6, "density": 8, "usability": 7, "polish": 7, "responsive": 3, "identity": 7}, "pairs": [{"screen": "app-list-dark.desktop.dark", "winner": "candidate", "reason": "B uses a quieter action hierarchy and compact overflow affordance, making the workspace rows read as operational inventory rather than a row of competing buttons."}, {"screen": "app-list-light.desktop.dark", "winner": "candidate", "reason": "B carries the same restrained, console-like action treatment into light mode, with better visual economy."}, {"screen": "app-detail-dark.desktop.dark", "winner": "candidate", "reason": "B gives the primary terminal action clear prominence while placing the destructive stop action in a restrained secondary position."}, {"screen": "app-detail-light.desktop.dark", "winner": "baseline", "reason": "B has the more coherent operational action hierarchy; its blue primary action remains legible without making every control feel equally important."}, {"screen": "app-dialog-dark.desktop.dark", "winner": "baseline", "reason": "B\u2019s modal surface, border, and button treatment feel more integrated with the dark product shell while preserving the destructive action\u2019s red emphasis."}, {"screen": "app-dialog-light.desktop.dark", "winner": "candidate", "reason": "B\u2019s subdued secondary Cancel treatment creates a clearer destructive-action hierarchy than the equally outlined pair in A."}, {"screen": "app-list-dark.mobile.dark", "winner": "candidate", "reason": "Its restrained action treatment keeps the workspace states and row actions legible without turning routine controls into large accent blocks."}, {"screen": "site-dark.desktop.dark", "winner": "candidate", "reason": "The proportional display type gives the documentation hero substantially clearer hierarchy and more natural reading than the oversized monospace headline."}, {"screen": "site-light.desktop.dark", "winner": "candidate", "reason": "The proportional headline is more readable and polished while preserving the technical character through smaller monospace labels and code."}, {"screen": "dashboard-dark.desktop.dark", "winner": "baseline", "reason": "It establishes a much clearer action hierarchy: the primary action, destructive confirmation, selected navigation, and toast all communicate their roles immediately."}, {"screen": "dashboard-light.mobile.dark", "winner": "baseline", "reason": "Its responsive layout preserves clear button affordances and a deliberate destructive-action order instead of making the confirmation feel cramped."}, {"screen": "palette-dark.desktop.dark", "winner": "candidate", "reason": "Its deeper, more neutral surfaces and restrained blue accent create a more credible infrastructure-console system with clearer semantic separation."}], "findings": [{"screen": "app-list-dark.desktop.dark", "location": "Workspace table rows", "severity": "structural", "problem": "The dense rows expose no column labels, so image, resource allocation, and update time must be inferred from position alone.", "fix": "Add a compact table header or visually persistent field labels, retaining the current dense row height."}, {"screen": "app-list-dark.desktop.dark", "location": "Workspace row actions", "severity": "structural", "problem": "Moving Stop and VS Code entirely behind overflow makes frequent operating actions less discoverable, particularly for a running workspace.", "fix": "Keep Open terminal primary; retain a visible, compact secondary Stop or a clearly labeled split/action menu with state-aware options."}, {"screen": "app-list-dark.desktop.dark", "location": "Top-right page controls", "severity": "cosmetic", "problem": "Two unlabeled square placeholder controls sit beside New workspace without a visible grouping or purpose.", "fix": "Group them in a compact utility toolbar with visible iconography and tooltips in production."}, {"screen": "app-detail-dark.desktop.dark", "location": "Workspace detail header", "severity": "structural", "problem": "The status line and machine image occupy a single long line, giving the immutable image identifier undue prominence relative to operational state.", "fix": "Keep status adjacent to the title and move the image reference into Details or a muted secondary metadata line."}, {"screen": "app-detail-dark.desktop.dark", "location": "Details and Resources panels", "severity": "systemic", "problem": "The two columns have generous unused width while long image IDs create an uneven visual rhythm.", "fix": "Use explicit label/value grid columns with a constrained readable content width and allow technical values to truncate with copy/reveal affordances."}, {"screen": "app-detail-dark.desktop.dark", "location": "Connect command block", "severity": "cosmetic", "problem": "The command block lacks a visible copy affordance despite being a primary task for infrastructure users.", "fix": "Add a compact Copy button or icon at the code block\u2019s upper right, with copied-state feedback."}, {"screen": "app-dialog-dark.desktop.dark", "location": "Delete confirmation modal", "severity": "structural", "problem": "Delete remains visually active even though the entered confirmation name does not match, increasing the chance of misunderstanding the current form state.", "fix": "Disable Delete until the exact workspace name is entered; retain the inline mismatch message and explain the enabled condition."}, {"screen": "app-dialog-dark.desktop.dark", "location": "Delete confirmation modal", "severity": "cosmetic", "problem": "The deletion consequence is phrased softly and the retention period is not quantified.", "fix": "State the recovery window explicitly, for example: \u201cMoved to Trash; recoverable for 30 days,\u201d if that is the actual policy."}, {"screen": "app-list-light.desktop.dark", "location": "Light-mode workspace list", "severity": "systemic", "problem": "Fine blue-gray borders and metadata text are close in value to the white surface, weakening scanability for dense operational data.", "fix": "Increase contrast of dividers and secondary text while keeping the blue accent reserved for links and primary actions."}, {"screen": "app-detail-light.desktop.dark", "location": "Light-mode resource area", "severity": "cosmetic", "problem": "The Change size action floats above the resource values without a strong relationship to the section heading.", "fix": "Align Change size on the Resources heading baseline and use consistent heading/action spacing across detail sections."}, {"screen": "app-list-dark.desktop.dark", "location": "Toast notification", "severity": "cosmetic", "problem": "The completion toast is visually isolated at the bottom edge and does not identify the workspace or offer a relevant follow-up action.", "fix": "Include the affected workspace name and a contextual action such as View snapshot, while preserving the concise success state."}, {"screen": "app-detail-dark.desktop.dark", "location": "Global shell", "severity": "systemic", "problem": "No narrow-layout behavior is demonstrated; the fixed sidebar, wide technical values, and multi-action header would be difficult to preserve on smaller screens.", "fix": "Define a responsive mode with collapsible navigation, stacked detail sections, truncation/copy patterns for IDs, and an action menu."}, {"screen": "app-list-dark.mobile.dark", "location": "Workspace rows", "severity": "structural", "problem": "The failed workspace has no visible row-level recovery action, unlike running and stopped rows, forcing users to infer that the overflow menu contains the next step.", "fix": "Add a compact explicit \u201cView logs\u201d or \u201cRetry\u201d action for failed rows, while retaining the overflow menu for secondary actions."}, {"screen": "dashboard-dark.desktop.dark", "location": "Delete confirmation dialog", "severity": "structural", "problem": "The destructive confirmation is embedded as a persistent panel beneath the table, making it easy to read as ordinary page content rather than an interruptive, high-consequence decision.", "fix": "Present deletion in a focused modal or clearly elevated inline confirmation state tied to the selected row, with focus management and an explicit workspace identifier."}, {"screen": "dashboard-dark.desktop.dark", "location": "Delete confirmation actions", "severity": "structural", "problem": "The dialog does not visibly require or show the exact target name beyond its heading, which is weak protection for an irreversible infrastructure operation.", "fix": "Include the workspace name in the warning body and require a typed name confirmation for disk-deleting actions."}, {"screen": "dashboard-dark.desktop.dark", "location": "Workspace table", "severity": "cosmetic", "problem": "The table has a large amount of unused horizontal space while the action area is absent, so primary workflow controls are separated from the objects they affect.", "fix": "Reserve a right-aligned Actions column for Open, Snapshot, Stop/Start, and overflow controls at desktop widths."}, {"screen": "dashboard-light.mobile.dark", "location": "Narrow delete confirmation", "severity": "structural", "problem": "The confirmation card competes with the table for vertical attention and pushes the status toast directly underneath it, creating a crowded lower viewport.", "fix": "Use a modal sheet or replace the selected row with an inline confirmation; keep transient toasts anchored independently from task content."}, {"screen": "palette-dark.desktop.dark", "location": "Palette laboratory, action swatches", "severity": "systemic", "problem": "The \u201caccent-text\u201d token is a light blue fill rather than a clearly named foreground token, so its purpose is ambiguous and risks incorrect use on variable surfaces.", "fix": "Rename it by role and document its intended foreground/background pairings; add contrast examples for accent, hover, selected, and disabled states."}, {"screen": "palette-dark.desktop.dark", "location": "Palette laboratory, token cards", "severity": "cosmetic", "problem": "Several token samples use similar dark card bodies, making the differences among page, surface, raised, and overlay harder to perceive at a glance.", "fix": "Show each surface token in a realistic nested composition and label its elevation/use case alongside the hexadecimal value."}]}
diff --git a/ui-hill-climb/product.md b/ui-hill-climb/product.md
new file mode 100644
index 0000000..a404551
--- /dev/null
+++ b/ui-hill-climb/product.md
@@ -0,0 +1,18 @@
+Product: Spinbox, a self-hosted platform (in the customer's own AWS account) for running isolated
+sandboxes, development workspaces and AI agents, each a virtual machine with snapshots and diffs.
+Users are infrastructure engineers, platform engineers and security-conscious developers. Top
+workflows: see their workspaces and their state, open one (terminal, VS Code), stop/start/delete,
+snapshot and diff, read the docs. What you are judging is the BRAND SYSTEM - colours, surfaces,
+borders, corners, type and how the accent is used - as it draws the app (screens of the
+workspaces list, a workspace's detail, a confirmation dialog), the documentation website, a
+laboratory page of the palette. The small square outlines next to navigation items and in buttons
+are icon placeholders in every version: ignore them. Words, rows and names are fixtures.
+
+Direction constraints:
+- Information-dense, readable, well-structured views. Not every fact is a card.
+- Clear distinction between navigation, content, contextual detail and operational actions; destructive actions look destructive.
+- IDs, image names, timestamps in a monospace face.
+- Restrained colour with semantic meaning: one colour per state (running, waiting, stopped, failed), the same everywhere; a state never by hue alone.
+- Light and dark mode both first-class; text contrast WCAG AA at least, body text ideally AAA.
+- Not a generic SaaS dashboard: purpose-built for a serious infrastructure product.
+References (ref-*.png) are developer/infrastructure consoles that are the bar for a 10: Beam (dark console) and boxd.
diff --git a/ui-hill-climb/prompt-pairs.md b/ui-hill-climb/prompt-pairs.md
new file mode 100644
index 0000000..c188690
--- /dev/null
+++ b/ui-hill-climb/prompt-pairs.md
@@ -0,0 +1,44 @@
+You are reviewing the UI of a real product. You cannot and must not modify any file.
+
+Product: Spinbox, a self-hosted platform (in the customer's own AWS account) for running isolated
+sandboxes, development workspaces and AI agents, each a virtual machine with snapshots and diffs.
+Users are infrastructure engineers, platform engineers and security-conscious developers. Top
+workflows: see their workspaces and their state, open one (terminal, VS Code), stop/start/delete,
+snapshot and diff, read the docs. What you are judging is the BRAND SYSTEM - colours, surfaces,
+borders, corners, type and how the accent is used - as it draws the app (screens of the
+workspaces list, a workspace's detail, a confirmation dialog), the documentation website, a
+laboratory page of the palette. The small square outlines next to navigation items and in buttons
+are icon placeholders in every version: ignore them. Words, rows and names are fixtures.
+
+Direction constraints:
+- Information-dense, readable, well-structured views. Not every fact is a card.
+- Clear distinction between navigation, content, contextual detail and operational actions; destructive actions look destructive.
+- IDs, image names, timestamps in a monospace face.
+- Restrained colour with semantic meaning: one colour per state (running, waiting, stopped, failed), the same everywhere; a state never by hue alone.
+- Light and dark mode both first-class; text contrast WCAG AA at least, body text ideally AAA.
+- Not a generic SaaS dashboard: purpose-built for a serious infrastructure product.
+References (ref-*.png) are developer/infrastructure consoles that are the bar for a 10: Beam (dark console) and boxd.
+
+Attached:
+- Reference products in the same category (ref-*.png): the bar for a 10.
+- Pairs of the same screen in two versions (sNN-a.png, sNN-b.png), in no particular order.
+- Single screens (cNN.png) to score on their own, if any.
+
+Tasks:
+1. For each pair, say which version is better for this product and audience, or "tie", and the
+ single most important reason.
+2. For the version you preferred overall, score each rubric category 1-10 ("scores"); and score the
+ cNN screens together on the same rubric ("control"). If there are no cNN screens, repeat
+ "scores" in "control".
+Rubric, each 1-10 where 10 is the best reference product in the set, not perfection:
+hierarchy (what matters is seen first), typography (scale, weights, legibility of data and ids),
+spacing (consistent scale, alignment), color (semantic roles, contrast, restraint; tones that are
+too saturated or surfaces that blend count against it), consistency, density, usability
+(navigation, affordances, state feedback), polish (edges, corners, borders, details),
+responsive (narrow layouts designed), identity (purpose-built for this product and audience).
+3. List concrete findings about the version you preferred: screen id, location, the problem, and
+ the fix. Prefer structural and systemic problems over cosmetic ones. No generic advice. At most
+ 12 findings.
+
+Judge only what is visible. Do not assume either version is newer. Answer with JSON matching the
+attached schema and nothing else.
diff --git a/ui-hill-climb/references/README.md b/ui-hill-climb/references/README.md
new file mode 100644
index 0000000..dfc4c8f
--- /dev/null
+++ b/ui-hill-climb/references/README.md
@@ -0,0 +1,9 @@
+# References
+
+- **Beam (beam.cloud), console, dark** — near-neutral warm-black ground (≈#1a1817) with surfaces one
+ small step lighter, hairline borders that separate without shouting, 8px corners, one soft blue
+ used only for the primary action and links. Take: neutral surfaces and quiet borders; the accent as
+ a scarce resource; settings as rows inside one rounded group.
+- **boxd, dashboard, dark** — cool near-black, 12px cards with a 1px low-contrast edge, mono
+ uppercase eyebrow labels, compact chips for state, meters in mono. Take: rounded containers for
+ groups, small mono metadata, status as a tiny chip rather than coloured text.
diff --git a/ui-hill-climb/review.schema.json b/ui-hill-climb/review.schema.json
new file mode 100644
index 0000000..bd0b30c
--- /dev/null
+++ b/ui-hill-climb/review.schema.json
@@ -0,0 +1,58 @@
+{
+ "title": "ui-hill-climb review",
+ "type": "object",
+ "additionalProperties": false,
+ "required": ["pairs", "scores", "control", "findings"],
+ "$defs": {
+ "score": { "type": "integer", "minimum": 1, "maximum": 10 },
+ "scores": {
+ "type": "object",
+ "additionalProperties": false,
+ "required": ["hierarchy", "typography", "spacing", "color", "consistency", "density", "usability", "polish", "responsive", "identity"],
+ "properties": {
+ "hierarchy": { "$ref": "#/$defs/score" },
+ "typography": { "$ref": "#/$defs/score" },
+ "spacing": { "$ref": "#/$defs/score" },
+ "color": { "$ref": "#/$defs/score" },
+ "consistency": { "$ref": "#/$defs/score" },
+ "density": { "$ref": "#/$defs/score" },
+ "usability": { "$ref": "#/$defs/score" },
+ "polish": { "$ref": "#/$defs/score" },
+ "responsive": { "$ref": "#/$defs/score" },
+ "identity": { "$ref": "#/$defs/score" }
+ }
+ }
+ },
+ "properties": {
+ "pairs": {
+ "type": "array",
+ "items": {
+ "type": "object",
+ "additionalProperties": false,
+ "required": ["screen", "winner", "reason"],
+ "properties": {
+ "screen": { "type": "string", "description": "Pair id, e.g. s01" },
+ "winner": { "type": "string", "enum": ["a", "b", "tie"] },
+ "reason": { "type": "string" }
+ }
+ }
+ },
+ "scores": { "$ref": "#/$defs/scores", "description": "Scores of the version preferred overall" },
+ "control": { "$ref": "#/$defs/scores", "description": "Scores of the single cNN screens" },
+ "findings": {
+ "type": "array",
+ "items": {
+ "type": "object",
+ "additionalProperties": false,
+ "required": ["screen", "location", "severity", "problem", "fix"],
+ "properties": {
+ "screen": { "type": "string" },
+ "location": { "type": "string" },
+ "severity": { "type": "string", "enum": ["broken", "structural", "systemic", "cosmetic"] },
+ "problem": { "type": "string" },
+ "fix": { "type": "string" }
+ }
+ }
+ }
+ }
+}
diff --git a/ui-hill-climb/rubric.md b/ui-hill-climb/rubric.md
new file mode 100644
index 0000000..7fff8f9
--- /dev/null
+++ b/ui-hill-climb/rubric.md
@@ -0,0 +1,6 @@
+Rubric, each 1-10 where 10 is the best reference product in the set, not perfection:
+hierarchy (what matters is seen first), typography (scale, weights, legibility of data and ids),
+spacing (consistent scale, alignment), color (semantic roles, contrast, restraint; tones that are
+too saturated or surfaces that blend count against it), consistency, density, usability
+(navigation, affordances, state feedback), polish (edges, corners, borders, details),
+responsive (narrow layouts designed), identity (purpose-built for this product and audience).
diff --git a/ui-hill-climb/screens.json b/ui-hill-climb/screens.json
new file mode 100644
index 0000000..c98621e
--- /dev/null
+++ b/ui-hill-climb/screens.json
@@ -0,0 +1,275 @@
+{
+ "viewports": {
+ "desktop": [
+ 1440,
+ 900
+ ],
+ "mobile": [
+ 390,
+ 844
+ ]
+ },
+ "themes": [
+ "dark"
+ ],
+ "screens": [
+ {
+ "id": "app-list-dark",
+ "path": "/ui-hill-climb/app.html?mode=dark&view=list",
+ "viewports": [
+ "desktop",
+ "mobile"
+ ],
+ "steps": [
+ {
+ "wait": 200
+ }
+ ],
+ "fullPage": false
+ },
+ {
+ "id": "app-list-light",
+ "path": "/ui-hill-climb/app.html?mode=light&view=list",
+ "viewports": [
+ "desktop",
+ "mobile"
+ ],
+ "steps": [
+ {
+ "wait": 200
+ }
+ ],
+ "fullPage": false
+ },
+ {
+ "id": "app-detail-dark",
+ "path": "/ui-hill-climb/app.html?mode=dark&view=detail",
+ "viewports": [
+ "desktop",
+ "mobile"
+ ],
+ "steps": [
+ {
+ "wait": 200
+ }
+ ],
+ "fullPage": false
+ },
+ {
+ "id": "app-detail-light",
+ "path": "/ui-hill-climb/app.html?mode=light&view=detail",
+ "viewports": [
+ "desktop",
+ "mobile"
+ ],
+ "steps": [
+ {
+ "wait": 200
+ }
+ ],
+ "fullPage": false
+ },
+ {
+ "id": "app-dialog-dark",
+ "path": "/ui-hill-climb/app.html?mode=dark&view=dialog",
+ "viewports": [
+ "desktop"
+ ],
+ "steps": [
+ {
+ "wait": 200
+ }
+ ],
+ "fullPage": false
+ },
+ {
+ "id": "app-dialog-light",
+ "path": "/ui-hill-climb/app.html?mode=light&view=dialog",
+ "viewports": [
+ "desktop"
+ ],
+ "steps": [
+ {
+ "wait": 200
+ }
+ ],
+ "fullPage": false
+ },
+ {
+ "id": "site-dark",
+ "path": "/index.html?mode=dark",
+ "viewports": [
+ "desktop",
+ "mobile"
+ ],
+ "steps": [
+ {
+ "eval": "document.fonts.ready"
+ },
+ {
+ "eval": "for (const el of document.querySelectorAll('#page > *, .masthead, .index')) if (el.id !== 'site') el.style.display='none'"
+ },
+ {
+ "wait": 200
+ }
+ ]
+ },
+ {
+ "id": "site-light",
+ "path": "/index.html?mode=light",
+ "viewports": [
+ "desktop",
+ "mobile"
+ ],
+ "steps": [
+ {
+ "eval": "for (const el of document.querySelectorAll('#page > *, .masthead, .index')) if (el.id !== 'site') el.style.display='none'"
+ },
+ {
+ "wait": 200
+ }
+ ]
+ },
+ {
+ "id": "dashboard-dark",
+ "path": "/index.html?mode=dark",
+ "viewports": [
+ "desktop",
+ "mobile"
+ ],
+ "steps": [
+ {
+ "eval": "for (const el of document.querySelectorAll('#page > *, .masthead, .index')) if (el.id !== 'dashboard') el.style.display='none'"
+ },
+ {
+ "wait": 200
+ }
+ ]
+ },
+ {
+ "id": "dashboard-light",
+ "path": "/index.html?mode=light",
+ "viewports": [
+ "desktop",
+ "mobile"
+ ],
+ "steps": [
+ {
+ "eval": "for (const el of document.querySelectorAll('#page > *, .masthead, .index')) if (el.id !== 'dashboard') el.style.display='none'"
+ },
+ {
+ "wait": 200
+ }
+ ]
+ },
+ {
+ "id": "palette-dark",
+ "path": "/index.html?mode=dark",
+ "viewports": [
+ "desktop"
+ ],
+ "steps": [
+ {
+ "eval": "for (const el of document.querySelectorAll('#page > *, .index')) if (el.id !== 'palette-section') el.style.display='none'"
+ },
+ {
+ "wait": 300
+ }
+ ]
+ },
+ {
+ "id": "palette-light",
+ "path": "/index.html?mode=light",
+ "viewports": [
+ "desktop"
+ ],
+ "steps": [
+ {
+ "eval": "for (const el of document.querySelectorAll('#page > *, .index')) if (el.id !== 'palette-section') el.style.display='none'"
+ },
+ {
+ "wait": 300
+ }
+ ]
+ },
+ {
+ "id": "mark-dark",
+ "path": "/index.html?mode=dark",
+ "viewports": [
+ "desktop"
+ ],
+ "steps": [
+ {
+ "eval": "for (const el of document.querySelectorAll('#page > *, .masthead, .index')) if (el.id !== 'mark') el.style.display='none'"
+ },
+ {
+ "wait": 200
+ }
+ ]
+ },
+ {
+ "id": "buttons-dark",
+ "path": "/index.html?mode=dark",
+ "viewports": [
+ "desktop",
+ "mobile"
+ ],
+ "steps": [
+ {
+ "eval": "for (const el of document.querySelectorAll('#page > *, .index')) if (el.id !== 'buttons') el.style.display='none'"
+ },
+ {
+ "wait": 300
+ }
+ ]
+ },
+ {
+ "id": "buttons-light",
+ "path": "/index.html?mode=light",
+ "viewports": [
+ "desktop",
+ "mobile"
+ ],
+ "steps": [
+ {
+ "eval": "for (const el of document.querySelectorAll('#page > *, .index')) if (el.id !== 'buttons') el.style.display='none'"
+ },
+ {
+ "wait": 300
+ }
+ ]
+ },
+ {
+ "id": "components-dark",
+ "path": "/index.html?mode=dark",
+ "viewports": [
+ "desktop",
+ "mobile"
+ ],
+ "steps": [
+ {
+ "eval": "for (const el of document.querySelectorAll('#page > *, .index, .masthead')) if (!['states','fields','tables'].includes(el.id)) el.style.display='none'"
+ },
+ {
+ "wait": 300
+ }
+ ]
+ },
+ {
+ "id": "components-light",
+ "path": "/index.html?mode=light",
+ "viewports": [
+ "desktop",
+ "mobile"
+ ],
+ "steps": [
+ {
+ "eval": "for (const el of document.querySelectorAll('#page > *, .index, .masthead')) if (!['states','fields','tables'].includes(el.id)) el.style.display='none'"
+ },
+ {
+ "wait": 300
+ }
+ ]
+ }
+ ]
+}
\ No newline at end of file