pub const UI_CSS: &str = " /* DESIGN.md tokens \u{2014} dark is the primary/default theme (DD5); light follows the OS via\n prefers-color-scheme (#31), no manual toggle. Fonts are the metrics-compatible\n fallbacks; the latin-subset Monaspace/General Sans woff2 land in a later slice with\n no layout shift. */\n :root {\n color-scheme: dark;\n --bg: #0a0a0b;\n --surface: #121214;\n --raised: #17171b;\n --hair: #26262b;\n --text: #e6e6e9;\n --muted: #8a8a93;\n --faint: #55555c;\n --amber: #ff7a1a;\n --warning: #f5a623;\n --error: #ff5c5c;\n --error-bg: #ff5c5c14;\n --success: #46d39a;\n --success-bg: #46d39a14;\n --mono: ui-monospace, \"SF Mono\", SFMono-Regular, Menlo, monospace;\n --sans: ui-sans-serif, system-ui, -apple-system, sans-serif;\n }\n\n /* Light palette (DESIGN.md \"Light mode\"). --raised coincides with --surface (both read\n as the elevated/white layer against the --bg canvas \u{2014} the dark scale\'s bg<surface<raised\n progression has no headroom above white). --faint has no explicit light spec; it\'s\n decorative-only (DD4), so it\'s picked for a low, ~faint-in-dark contrast against --bg\n rather than lifted from the doc. */\n @media (prefers-color-scheme: light) {\n :root {\n color-scheme: light;\n --bg: #f7f7f5;\n --surface: #ffffff;\n --raised: #ffffff;\n --hair: #e2e2dd;\n --text: #16161a;\n --muted: #6b6b72;\n --faint: #a3a39c;\n --amber: #e0570b;\n --error: #c7382f;\n --error-bg: #c7382f14;\n --success: #1e9e6a;\n --success-bg: #1e9e6a14;\n }\n }\n\n * {\n box-sizing: border-box;\n margin: 0;\n padding: 0;\n }\n\n body {\n height: 100vh; /* exact viewport height so the CANVAS scrolls, never the whole page */\n display: flex;\n flex-direction: column;\n overflow: hidden;\n background: var(--bg);\n color: var(--text);\n font-family: var(--sans);\n font-size: 0.875rem;\n line-height: 1.5;\n -webkit-font-smoothing: antialiased;\n }\n\n /* The two-pane body: canvas flexes, attribution holds a fixed 320px on the right. Fills\n the height below the header so the pane\'s border-left runs the full column. */\n .body {\n display: flex;\n flex: 1;\n min-height: 0;\n }\n\n /* --- The app shell: never a blank page before wasm mounts (D20). The boot node is\n removed by the csr entry once the app is alive; if it survives, boot failed and\n the error state below is revealed in its place. --- */\n .boot {\n display: flex;\n flex-direction: column;\n align-items: center;\n justify-content: center;\n gap: 0.5rem;\n min-height: 100vh;\n padding: 2rem;\n text-align: center;\n color: var(--muted);\n }\n\n .boot-glyph {\n font-family: var(--mono);\n font-size: 1.5rem;\n color: var(--faint);\n }\n\n .boot p {\n max-width: 34rem;\n }\n\n .boot-hint {\n color: var(--muted);\n font-size: 0.8125rem;\n }\n\n .boot code {\n font-family: var(--mono);\n color: var(--text);\n }\n\n .boot--error .boot-glyph {\n color: var(--warning);\n }\n\n [hidden] {\n display: none !important;\n }\n\n @media (prefers-reduced-motion: no-preference) {\n .boot-glyph {\n animation: boot-pulse 1.6s ease-in-out infinite;\n }\n }\n\n @keyframes boot-pulse {\n 0%,\n 100% {\n opacity: 0.55;\n }\n 50% {\n opacity: 1;\n }\n }\n\n /* --- Header bar. Mono, compact, quiet: the verdict is the protagonist through the\n `text` token and its position, not color. Zero amber in the header frame. --- */\n .hdr {\n display: flex;\n align-items: baseline;\n gap: 1rem;\n padding: 0.5rem 0.875rem;\n border-bottom: 1px solid var(--hair);\n background: var(--surface);\n font-family: var(--mono);\n font-size: 0.75rem;\n }\n\n .logo {\n color: var(--text);\n font-weight: 700;\n letter-spacing: -0.01em;\n }\n\n .logo-glyph {\n color: var(--muted); /* never amber \u{2014} amber is spent only in the canvas */\n }\n\n .pair {\n color: var(--text);\n }\n\n .pair b {\n font-weight: 600;\n }\n\n .role {\n color: var(--muted);\n }\n\n .vs {\n color: var(--faint);\n }\n\n .verdict {\n color: var(--text);\n font-variant-numeric: tabular-nums;\n text-decoration: none;\n }\n\n .verdict--fork:hover {\n text-decoration: underline;\n text-underline-offset: 3px;\n }\n\n .verdict:focus-visible {\n outline: 1px solid var(--hair);\n outline-offset: 2px;\n border-radius: 2px;\n }\n\n .meta {\n margin-left: auto;\n color: var(--muted);\n font-variant-numeric: tabular-nums;\n }\n\n /* --- The alignment canvas: side-by-side runs on the shared spine. Sameness recedes\n (muted), divergence glows (amber on the fork + every downstream row). Amber is\n spent here and nowhere else. DOM rows carry selectable text; the SVG overlay\n draws the timeline rail, the amber divergent-path segment, and the fork node,\n both keyed to the 30px row pitch so they line up. --- */\n .canvas {\n flex: 1;\n min-width: 0; /* let the flex child shrink so its own overflow engages */\n min-height: 12rem;\n overflow: auto; /* the canvas is the scroll container \u{2014} header + pane stay fixed */\n }\n\n /* Fills the canvas viewport (dotted field), with the bounded rows left-anchored on top.\n When the rows are wider than the canvas the canvas alone scrolls (DD4 canvas-only\n horizontal scroll) \u{2014} the header and attribution pane never move. */\n .track {\n position: relative;\n min-height: 100%;\n background-image: radial-gradient(circle, var(--hair) 1px, transparent 1px);\n background-size: 22px 22px;\n }\n\n .spine {\n position: absolute;\n top: 0;\n left: 0;\n pointer-events: none;\n }\n .spine-rail {\n stroke: var(--faint); /* the quiet shared timeline */\n stroke-width: 1;\n }\n .spine-path {\n stroke: var(--amber); /* the divergent path made literal; the ignition beat animates it */\n stroke-width: 2;\n }\n .fork-node {\n fill: var(--amber);\n }\n\n .rows {\n list-style: none;\n width: fit-content; /* hug the two bounded runs; the fork band never spans dead space */\n margin-left: 28px; /* clear the 28px SVG spine strip */\n padding: 18px 0; /* 18px top pad matches the geometry\'s TOP_PAD */\n }\n\n /* Fixed side columns, not 1fr: the two runs sit adjacent as one comparison instead of\n stretching to opposite edges, and the layout anchors left \u{2014} leaving the right open for\n the attribution pane (slice 2). Same template on every row keeps the columns aligned. */\n .row {\n display: grid;\n grid-template-columns: 104px 300px 300px auto;\n align-items: center;\n column-gap: 18px;\n height: 30px; /* == ROW_H; the SVG overlay is computed from the same pitch */\n padding: 0 14px;\n font-family: var(--mono);\n font-size: 0.75rem;\n color: var(--muted);\n font-variant-numeric: tabular-nums;\n cursor: pointer;\n scroll-margin-top: 96px; /* auto-scroll lands the fork in the upper third, not the edge */\n }\n /* Hover tint (never on the selected row) and a clear focus-visible ring. The ring is a\n box-shadow, not an outline, so it never overrides the fork\'s dashed amber cue. */\n .row:hover:not(.row--selected) {\n background: var(--surface);\n }\n .row:focus-visible {\n outline: none;\n box-shadow: inset 0 0 0 2px var(--muted);\n }\n\n .gutter {\n display: flex;\n align-items: baseline;\n gap: 8px;\n }\n .cue {\n width: 0.9em;\n display: inline-block;\n text-align: center;\n color: var(--faint);\n }\n .idx {\n color: var(--muted);\n }\n\n .cell {\n display: flex;\n align-items: baseline;\n gap: 8px;\n min-width: 0;\n }\n .kind {\n width: 40px;\n color: var(--faint);\n }\n .name {\n width: 104px;\n color: var(--muted);\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n }\n .sum {\n flex: 1;\n min-width: 0;\n color: var(--muted);\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n }\n /* The honest cut mark (issue #17) and, when the slot carries a resolvable address, a real\n click-to-expand affordance (issue #30) \u{2014} same element, same class, so idle appearance\n never changes: only interaction reveals whether there\'s anything behind it to fetch.\n Never amber (DESIGN.md) \u{2014} this is not divergence, and must not compete with the fork\'s\n one scarce accent. */\n .slot-trunc {\n border: none;\n background: none;\n padding: 0;\n margin: 0;\n font: inherit;\n color: var(--muted);\n }\n button.slot-trunc {\n cursor: pointer;\n transition: color 150ms ease-out;\n }\n button.slot-trunc:hover {\n color: var(--text);\n text-decoration: underline dotted;\n }\n button.slot-trunc:focus-visible {\n color: var(--text);\n text-decoration: underline dotted;\n outline: 2px solid var(--muted);\n outline-offset: 1px;\n }\n button.slot-trunc:disabled {\n cursor: default;\n opacity: 0.6;\n text-decoration: none;\n }\n @media (prefers-reduced-motion: reduce) {\n button.slot-trunc {\n transition: none;\n }\n }\n\n /* Divergence glows: the fork and every downstream row in one uniform amber (no intensity\n grading \u{2014} one scarce accent). The dashed outline is the fork\'s redundant non-color cue;\n `outline` never shifts layout, so the row stays 30px and the SVG stays aligned. */\n .row--fork {\n color: var(--amber);\n background: rgba(255, 122, 26, 0.05);\n border-radius: 4px;\n outline: 1px dashed var(--amber);\n outline-offset: -1px;\n }\n .row--down {\n color: var(--amber);\n }\n .row--fork .idx,\n .row--fork .kind,\n .row--fork .name,\n .row--fork .sum,\n .row--fork .cue,\n .row--down .idx,\n .row--down .kind,\n .row--down .name,\n .row--down .sum,\n .row--down .cue {\n color: var(--amber);\n }\n .tag {\n justify-self: end;\n white-space: nowrap; /* the [FORK \u{b7} conf] tag stays on the fork\'s single 30px row */\n color: var(--amber);\n font-variant-numeric: tabular-nums;\n }\n\n /* Selection is the neutral frame: raised surface + a hairline ring (inset box-shadow, so\n no layout shift). Never amber (DD2) \u{2014} the default-selected fork keeps its amber role via\n `row--fork` and only gains the raised surface + hairline on top. Placed after the role\n rules so its raised background wins over the fork\'s amber tint. */\n .row--selected {\n background: var(--raised);\n box-shadow: inset 0 0 0 1px var(--hair);\n }\n\n /* --- Motion: the ONE expressive beat (DESIGN.md \u{a7}Motion). On load the amber ignites at\n the fork node and flows down the divergent path \u{2014} the fork node pops, the amber\n spine segment draws downward from the fork, and the divergent rows kindle to full\n amber. Medium 250\u{2013}400ms, ease-out (enter easing); the beat spans 0\u{2013}380ms. Each\n animation\'s `to` state IS the static markup, so with `prefers-reduced-motion:\n reduce` this whole block is off and the lit end state simply shows with no motion.\n The glow is the saturated stroke \u{2014} no blur halo. Fires on load; slice 3a already\n scrolls the fork into view first, so the beat plays where the eye is. --- */\n @media (prefers-reduced-motion: no-preference) {\n .fork-node {\n transform-box: fill-box;\n transform-origin: center;\n animation: fork-ignite 200ms ease-out both;\n }\n .spine-path {\n transform-box: fill-box;\n transform-origin: top; /* the fork end: the segment runs fork_y \u{2192} y_last */\n animation: path-flow 300ms ease-out 80ms both;\n }\n .row--fork,\n .row--down {\n animation: row-kindle 300ms ease-out 80ms both;\n }\n }\n\n @keyframes fork-ignite {\n from {\n opacity: 0;\n transform: scale(0.2);\n }\n to {\n opacity: 1;\n transform: scale(1);\n }\n }\n @keyframes path-flow {\n from {\n transform: scaleY(0);\n }\n to {\n transform: scaleY(1);\n }\n }\n @keyframes row-kindle {\n from {\n opacity: 0.4;\n }\n to {\n opacity: 1;\n }\n }\n\n /* --- Attribution pane: the diff\'s one answer in DR5 order. No amber, no red/green \u{2014} a\n statement ABOUT the divergence, not the divergence itself. --- */\n .attr {\n flex: 0 0 320px;\n border-left: 1px solid var(--hair);\n background: var(--surface);\n padding: 16px 18px;\n font-family: var(--mono);\n font-size: 0.75rem;\n overflow-y: auto;\n }\n .attr-title {\n margin-bottom: 14px;\n font-size: 0.625rem;\n font-weight: 600;\n text-transform: uppercase;\n letter-spacing: 0.08em;\n color: var(--muted);\n }\n .attr-list {\n display: flex;\n flex-direction: column;\n gap: 10px;\n }\n .attr-row {\n display: flex;\n align-items: baseline;\n justify-content: space-between;\n gap: 14px;\n }\n .attr-row dt {\n color: var(--muted);\n }\n .attr-row dd {\n color: var(--text);\n text-align: right;\n font-variant-numeric: tabular-nums;\n }\n .attr-empty {\n color: var(--muted);\n line-height: 1.6;\n }\n\n /* A division of the SAME pane (issue #40), not a new one \u{2014} a hairline divider plus a\n heading-level step-down (h3, still styled as .attr-title) carry the hierarchy, so\n this reads as \"more of the same voice,\" never a competing header. */\n .attr-section {\n margin-top: 20px;\n padding-top: 16px;\n border-top: 1px solid var(--hair);\n }\n .attr-section .attr-title {\n margin-bottom: 10px;\n }\n\n /* --- Content-diff pane: the selected pair\'s field-level -/+ evidence (issue #27). The\n ONLY red/green in any surface (DESIGN.md containment) \u{2014} removed red, added green,\n kept deliberately distinct from the amber fork. Lives inside the attribution aside,\n below the answer; a card per field, monospace, values wrapping so long payloads stay\n readable and copyable (never a canvas draw). --- */\n .content-diff,\n .content-diff-empty {\n margin-top: 18px;\n padding-top: 14px;\n border-top: 1px solid var(--hair);\n }\n .content-diff {\n display: flex;\n flex-direction: column;\n gap: 12px;\n }\n .content-diff-empty {\n color: var(--muted);\n line-height: 1.6;\n }\n .content-diff-field {\n display: flex;\n flex-direction: column;\n gap: 3px;\n }\n .content-diff-path {\n color: var(--muted);\n font-size: 0.6875rem;\n }\n .content-diff-del,\n .content-diff-add {\n display: flex;\n gap: 6px;\n padding: 2px 6px;\n border-radius: 3px;\n white-space: pre-wrap;\n overflow-wrap: anywhere;\n }\n .content-diff-del {\n color: var(--error);\n background: var(--error-bg);\n }\n .content-diff-add {\n color: var(--success);\n background: var(--success-bg);\n }\n .content-diff-sign {\n flex: 0 0 auto;\n font-variant-numeric: tabular-nums;\n opacity: 0.85;\n }\n .content-diff-val {\n flex: 1;\n min-width: 0;\n }\n /* The copy affordance (issue #27 evidence-out). Quiet neutral at rest \u{2014} buttons are never\n amber (DESIGN.md), and it must not compete with the card\'s scarce red/green, so the\n confirmed state stays neutral too. Sits top-right of the card; hover = surface tint;\n focus-visible = hairline ring, matching the canvas rows. */\n .content-diff-copy {\n align-self: flex-end;\n margin-bottom: -4px;\n padding: 2px 8px;\n border: 1px solid var(--hair);\n border-radius: 4px;\n background: var(--surface);\n color: var(--muted);\n font-family: var(--mono);\n font-size: 0.6875rem;\n cursor: pointer;\n transition:\n color 150ms ease-out,\n background 150ms ease-out,\n border-color 150ms ease-out;\n }\n .content-diff-copy:hover {\n background: var(--raised);\n color: var(--text);\n border-color: var(--muted);\n }\n .content-diff-copy:focus-visible {\n outline: 2px solid var(--muted);\n outline-offset: 1px;\n }\n @media (prefers-reduced-motion: reduce) {\n .content-diff-copy {\n transition: none;\n }\n }\n\n /* --- Disconnect banner: the server that fed this view stopped (D-decision 2026-07-12).\n A slim strip docked at the bottom edge, `warning` \u{2014} never amber: a system-status\n message is not a divergence. Fixed, so it annunciates without reflowing the canvas;\n no spinner \u{2014} the state is terminal until the user restarts + reloads. --- */\n .banner {\n position: fixed;\n left: 0;\n right: 0;\n bottom: 0;\n padding: 0.5rem 0.875rem;\n border-top: 1px solid var(--warning);\n background: rgba(245, 166, 35, 0.08); /* faint warning wash over the surface */\n font-family: var(--mono);\n font-size: 0.75rem;\n color: var(--warning);\n }\n\n .banner code {\n color: var(--text); /* the restart command reads as copy-ready, brighter than the label */\n }\n";Expand description
The design system’s stylesheet — the single source both painters dress from.
It lives beside the view-model rather than in ui/ for a packaging reason: ui/ is a
separate workspace, so crates/amberfork/src/html_export.rs could only reach it with an
include_str! that escapes its own crate root, which builds in the repo and then fails
cargo package (the tarball carries no path outside the crate). Owning it here makes it a
normal dependency for the static export, while ui/index.html inlines the same bytes at
build time via trunk — one stylesheet, no hand-copied second copy to drift.