evalstate's picture
download
raw
66.5 kB
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Rendering Checker Pipeline — Module Explainer</title>
<style data-birch-system>/*
Birch HTML Design System
Opinionated single-option stylesheet for reliable LLM-generated artifacts.
Model:
1. Design tokens as CSS variables.
2. Layout primitives as classes.
3. Semantic components as classes with data-* variants.
*/
/* ==================== 1. TOKENS ==================== */
:root {
color-scheme: light;
/* Palette */
--color-ivory: #FAF9F5;
--color-slate: #141413;
--color-clay: #D97757;
--color-clay-dark: #B85C3E;
--color-oat: #E3DACC;
--color-olive: #788C5D;
--color-rust: #B04A3F;
--color-sky: #6A8CAF;
--color-white: #FFFFFF;
--color-gray-50: #F7F5EE;
--color-gray-100: #F0EEE6;
--color-gray-150: #E8E4DA;
--color-gray-200: #DED9CD;
--color-gray-300: #D1CFC5;
--color-gray-500: #87867F;
--color-gray-700: #3D3D3A;
--color-gray-800: #242421;
/* Semantic colors */
--bg: var(--color-ivory);
--surface: var(--color-white);
--surface-tint: var(--color-gray-100);
--surface-warm: var(--color-oat);
--text: var(--color-slate);
--text-muted: var(--color-gray-500);
--text-soft: var(--color-gray-700);
--muted: var(--text-muted);
--border-color: var(--color-gray-300);
--accent: var(--color-clay);
--accent-strong: var(--color-clay-dark);
--success: var(--color-olive);
--warning: #C78E3F;
--danger: var(--color-rust);
--info: var(--color-sky);
/* Typography */
--font-serif: ui-serif, Georgia, "Times New Roman", serif;
--font-sans: system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
--font-mono: ui-monospace, "SF Mono", Menlo, Consolas, monospace;
--text-xs: 11px;
--text-sm: 12px;
--text-md: 14px;
--text-base: 15px;
--text-lg: 17px;
--text-xl: 21px;
--text-2xl: 26px;
--text-3xl: 36px;
--text-4xl: 44px;
--leading-tight: 1.15;
--leading-title: 1.25;
--leading-body: 1.55;
--leading-loose: 1.7;
/* Spacing */
--space-1: 4px;
--space-2: 8px;
--space-3: 12px;
--space-4: 16px;
--space-5: 24px;
--space-6: 32px;
--space-7: 48px;
--space-8: 64px;
--space-9: 96px;
/* Shape */
--radius-xs: 4px;
--radius-sm: 8px;
--radius-md: 12px;
--radius-lg: 16px;
--radius-xl: 20px;
--radius-pill: 999px;
--border: 1.5px solid var(--border-color);
--border-thin: 1px solid var(--border-color);
--shadow-sm: 0 1px 2px rgba(20, 20, 19, 0.06);
--shadow-md: 0 4px 14px rgba(20, 20, 19, 0.08);
--shadow-lg: 0 12px 28px rgba(20, 20, 19, 0.12);
/* Motion */
--ease: cubic-bezier(0.2, 0, 0, 1);
--ease-spring: cubic-bezier(0.34, 1.56, 0.64, 1);
--dur-fast: 120ms;
--dur: 180ms;
--hover-y: -2px;
/* Page sizing */
--page-max: 1100px;
--page-pad: 28px;
--page-pad-block: 48px;
/* Legacy aliases used by source examples */
--ivory: var(--color-ivory);
--slate: var(--color-slate);
--clay: var(--color-clay);
--clay-d: var(--color-clay-dark);
--oat: var(--color-oat);
--olive: var(--color-olive);
--rust: var(--color-rust);
--sky: var(--color-sky);
--white: var(--color-white);
--gray-50: var(--color-gray-50);
--gray-100: var(--color-gray-100);
--gray-150: var(--color-gray-150);
--gray-200: var(--color-gray-200);
--gray-300: var(--color-gray-300);
--gray-500: var(--color-gray-500);
--gray-700: var(--color-gray-700);
--gray-800: var(--color-gray-800);
--serif: var(--font-serif);
--sans: var(--font-sans);
--mono: var(--font-mono);
}
/* ==================== BASE ==================== */
*,
*::before,
*::after {
box-sizing: border-box;
}
html {
background: var(--bg);
color: var(--text);
font-family: var(--font-sans);
-webkit-text-size-adjust: 100%;
}
body {
margin: 0;
background: var(--bg);
color: var(--text);
font-family: var(--font-sans);
font-size: var(--text-base);
line-height: var(--leading-body);
-webkit-font-smoothing: antialiased;
text-rendering: optimizeLegibility;
}
img,
svg {
max-width: 100%;
}
a {
color: inherit;
text-decoration-color: rgba(217, 119, 87, 0.45);
text-underline-offset: 3px;
}
a:hover {
color: var(--accent);
}
h1,
h2,
h3,
p {
margin-block: 0;
}
h1,
h2,
h3 {
font-family: var(--font-serif);
font-weight: 500;
letter-spacing: -0.01em;
line-height: var(--leading-title);
}
h1 {
font-size: clamp(32px, 5vw, var(--text-4xl));
}
h2 {
font-size: var(--text-2xl);
}
h3 {
font-size: var(--text-xl);
}
code,
kbd,
pre {
font-family: var(--font-mono);
}
code {
font-size: 0.92em;
white-space: normal;
word-break: normal;
}
h1 code,
h2 code,
h3 code {
overflow-wrap: anywhere;
word-break: break-word;
}
button,
input,
textarea,
select {
font: inherit;
}
button {
cursor: pointer;
}
::selection {
background: rgba(217, 119, 87, 0.24);
}
/* ==================== 2. LAYOUT PRIMITIVES ==================== */
.page {
width: min(100% - (var(--page-pad) * 2), var(--page-max));
margin-inline: auto;
padding-block: var(--page-pad-block) var(--space-9);
}
.page[data-size="narrow"] {
--page-max: 820px;
}
.page[data-size="wide"] {
--page-max: 1280px;
}
.page[data-size="full"] {
--page-max: 1440px;
}
.section {
margin-block: var(--section-gap, var(--space-5));
}
.section[data-gap="lg"] {
--section-gap: var(--space-6);
}
.section[data-gap="xl"] {
--section-gap: var(--space-7);
}
.section:first-child {
margin-block-start: 0;
}
.stack > .section {
margin-block: 0;
}
.rule {
border: 0;
border-top: var(--border-thin);
margin: var(--space-3) 0 var(--space-5);
}
.stack {
display: flex;
flex-direction: column;
gap: var(--stack-gap, var(--space-4));
}
.stack[data-gap="xs"] { --stack-gap: var(--space-2); }
.stack[data-gap="sm"] { --stack-gap: var(--space-3); }
.stack[data-gap="md"] { --stack-gap: var(--space-4); }
.stack[data-gap="lg"] { --stack-gap: var(--space-6); }
.stack[data-gap="xl"] { --stack-gap: var(--space-8); }
.stack > :is(.button, .btn, .chip, .badge) {
align-self: flex-start;
}
.cluster {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: var(--cluster-gap, var(--space-3));
}
.cluster[data-align="start"] { align-items: flex-start; }
.cluster[data-align="end"] { align-items: flex-end; }
.cluster[data-justify="between"] { justify-content: space-between; }
.cluster[data-justify="end"] { justify-content: flex-end; }
.grid {
display: grid;
gap: var(--grid-gap, var(--space-5));
}
.grid[data-cols="2"] {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
.grid[data-cols="3"] {
grid-template-columns: repeat(3, minmax(0, 1fr));
}
.grid[data-cols="4"] {
grid-template-columns: repeat(4, minmax(0, 1fr));
}
.auto-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(min(100%, var(--grid-min, 240px)), 1fr));
gap: var(--grid-gap, var(--space-5));
}
.auto-grid > * {
min-width: 0;
}
.split {
display: grid;
grid-template-columns: minmax(0, 1fr) minmax(280px, var(--split-side, 360px));
gap: var(--split-gap, var(--space-6));
align-items: start;
}
.split > * {
min-width: 0;
}
.section-head {
min-width: 0;
}
.section-rail {
display: grid;
grid-template-columns: minmax(0, 1fr) minmax(280px, var(--rail-width, 360px));
column-gap: var(--rail-gap, var(--space-6));
row-gap: var(--space-5);
align-items: start;
}
.section-rail > .section-head {
grid-column: 1 / -1;
}
.section-rail > .reference-panel,
.section-rail > aside {
align-self: start;
}
.sidebar-layout {
display: grid;
grid-template-columns: var(--sidebar-width, 240px) minmax(0, 1fr);
gap: var(--sidebar-gap, var(--space-7));
align-items: start;
}
.sidebar-layout > :first-child,
.sticky {
position: sticky;
top: var(--sticky-top, var(--space-5));
}
.bleed {
margin-inline: calc(var(--page-pad) * -1);
}
.scroll-x {
overflow-x: auto;
overscroll-behavior-inline: contain;
}
.center {
display: grid;
place-items: center;
}
.spacer {
flex: 1;
}
@media (max-width: 860px) {
:root {
--page-pad: 20px;
--page-pad-block: 40px;
}
.grid[data-cols],
.split,
.section-rail,
.sidebar-layout {
grid-template-columns: 1fr;
}
.sidebar-layout > :first-child,
.sticky {
position: static;
}
}
/* ==================== TYPOGRAPHY UTILITIES ==================== */
.eyebrow {
display: inline-flex;
align-items: center;
gap: var(--space-2);
color: var(--accent);
font-family: var(--font-mono);
font-size: var(--text-xs);
font-weight: 700;
letter-spacing: 0.08em;
text-transform: uppercase;
}
.eyebrow::before {
content: "";
width: 24px;
height: 1.5px;
background: currentColor;
}
.lede {
max-width: 720px;
color: var(--text-soft);
font-size: var(--text-lg);
line-height: 1.6;
}
.muted {
color: var(--text-muted);
}
.caption {
color: var(--text-muted);
font-family: var(--font-mono);
font-size: var(--text-sm);
}
.stat-value {
display: block;
color: var(--text);
font-family: var(--font-serif);
font-size: clamp(30px, 3.2vw, 42px);
font-variant-numeric: tabular-nums;
font-weight: 550;
letter-spacing: -0.025em;
line-height: 1;
}
.stat-card {
gap: var(--space-2);
}
.stat-card .caption {
letter-spacing: 0.08em;
text-transform: uppercase;
}
.stat-card .muted {
max-width: 34ch;
font-size: var(--text-sm);
}
.mono {
font-family: var(--font-mono);
}
/* ==================== 3. SEMANTIC COMPONENTS ==================== */
.panel,
.card {
background: var(--surface);
border: var(--border);
border-radius: var(--radius-md);
}
.panel {
padding: var(--panel-pad, var(--space-5));
}
.card {
padding: var(--card-pad, var(--space-5));
transition:
transform var(--dur-fast) var(--ease),
border-color var(--dur-fast) var(--ease),
box-shadow var(--dur-fast) var(--ease),
background-color var(--dur-fast) var(--ease);
}
.card[data-hover="lift"]:hover,
.card[data-clickable]:hover {
transform: translateY(var(--hover-y));
border-color: rgba(217, 119, 87, 0.55);
box-shadow: var(--shadow-md);
}
.card[data-clickable] {
cursor: pointer;
}
.card[data-variant="flat"] {
background: transparent;
border-color: transparent;
box-shadow: none;
}
.card[data-variant="outlined"] {
background: var(--surface);
border: var(--border);
}
.card[data-variant="elevated"] {
border-color: transparent;
box-shadow: var(--shadow-md);
}
.card[data-variant="filled"],
.card[data-tone="oat"] {
background: var(--surface-warm);
}
.card[data-tone],
.panel[data-tone],
.callout[data-tone] {
background: var(--surface);
}
.card[data-tone="danger"],
.panel[data-tone="danger"],
.callout[data-tone="danger"] {
border-color: rgba(176, 74, 63, 0.36);
}
.card[data-tone="warning"],
.panel[data-tone="warning"],
.callout[data-tone="warning"] {
border-color: rgba(199, 142, 63, 0.38);
}
.card[data-tone="success"],
.panel[data-tone="success"],
.callout[data-tone="success"] {
border-color: rgba(120, 140, 93, 0.38);
}
.card[data-tone="info"],
.panel[data-tone="info"],
.callout[data-tone="info"] {
border-color: rgba(106, 140, 175, 0.36);
}
.card[data-accent] {
position: relative;
overflow: hidden;
}
.card[data-accent]::before {
content: "";
position: absolute;
inset: 0 auto 0 0;
width: 5px;
background: var(--accent);
}
.card-head {
display: flex;
align-items: center;
gap: var(--space-3);
min-width: 0;
max-width: 100%;
}
.card-head > :first-child {
flex: 0 0 auto;
}
.card-titles {
flex: 1 1 0;
min-width: 0;
max-width: 100%;
}
.card-title {
max-width: 100%;
margin: 0;
font-family: var(--font-serif);
font-size: var(--text-xl);
font-weight: 550;
line-height: var(--leading-title);
overflow-wrap: anywhere;
}
.card-sub {
margin: 2px 0 0;
color: var(--text-muted);
font-size: var(--text-sm);
overflow-wrap: anywhere;
}
.toolbar {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: var(--space-4);
padding: var(--space-4) var(--space-5);
background: var(--surface);
border: var(--border);
border-radius: var(--radius-md);
}
.toolbar[data-sticky] {
position: sticky;
top: 0;
z-index: 10;
}
.chip,
.badge {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 6px;
border-radius: var(--radius-pill);
white-space: nowrap;
font-family: var(--font-mono);
font-size: var(--text-xs);
font-weight: 650;
line-height: 1;
}
.chip {
min-height: 24px;
padding: 0 10px;
color: var(--text-soft);
background: var(--surface-tint);
border: 1px solid transparent;
}
.badge {
min-height: 22px;
padding: 0 9px;
color: var(--text-muted);
background: var(--surface);
border: var(--border-thin);
text-transform: uppercase;
letter-spacing: 0.06em;
}
.chip[data-tone="clay"],
.badge[data-tone="clay"] {
color: var(--accent);
background: rgba(217, 119, 87, 0.14);
}
.chip[data-tone="olive"],
.badge[data-tone="olive"],
.chip[data-tone="success"],
.badge[data-tone="success"] {
color: var(--success);
background: rgba(120, 140, 93, 0.15);
}
.chip[data-tone="warning"],
.badge[data-tone="warning"] {
color: var(--warning);
background: rgba(199, 142, 63, 0.16);
}
.chip[data-tone="rust"],
.badge[data-tone="rust"],
.chip[data-tone="danger"],
.badge[data-tone="danger"] {
color: var(--danger);
background: rgba(176, 74, 63, 0.13);
}
.chip[data-tone="sky"],
.badge[data-tone="sky"],
.chip[data-tone="info"],
.badge[data-tone="info"] {
color: var(--info);
background: rgba(106, 140, 175, 0.14);
}
.finding {
border-left: 6px solid var(--border-color);
}
.finding[data-severity="blocker"],
.finding[data-severity="critical"],
.finding[data-severity="danger"],
.finding[data-tone="danger"] {
border-left-color: var(--danger);
}
.finding[data-severity="high"],
.finding[data-severity="warning"],
.finding[data-tone="warning"] {
border-left-color: var(--warning);
}
.finding[data-severity="medium"],
.finding[data-severity="info"],
.finding[data-tone="info"] {
border-left-color: var(--info);
}
.finding[data-severity="low"],
.finding[data-severity="success"],
.finding[data-tone="success"] {
border-left-color: var(--success);
}
.button,
.btn {
appearance: none;
display: inline-flex;
align-items: center;
justify-content: center;
min-height: 36px;
padding: 0 14px;
border: 1.5px solid transparent;
border-radius: var(--radius-sm);
font-family: var(--font-sans);
font-size: var(--text-md);
font-weight: 650;
line-height: 1;
text-decoration: none;
transition:
transform var(--dur-fast) var(--ease),
background-color var(--dur-fast) var(--ease),
border-color var(--dur-fast) var(--ease),
box-shadow var(--dur-fast) var(--ease);
}
.button:hover,
.btn:hover {
transform: translateY(-1px);
}
.button[data-variant="primary"],
.btn-primary {
color: var(--color-white);
background: var(--color-slate);
}
.button[data-variant="primary"]:hover,
.btn-primary:hover {
background: var(--color-gray-700);
}
.button[data-variant="secondary"],
.btn-secondary {
color: var(--text);
background: var(--surface-tint);
border-color: var(--border-color);
}
.button[data-variant="ghost"],
.btn-ghost {
color: var(--text-soft);
background: transparent;
}
.button[data-variant="ghost"]:hover,
.btn-ghost:hover {
color: var(--text);
background: var(--surface-tint);
}
.button[data-variant="danger"],
.btn-danger {
color: var(--color-white);
background: var(--danger);
}
.input {
width: 100%;
min-height: 38px;
padding: 0 12px;
color: var(--text);
background: var(--surface);
border: var(--border);
border-radius: var(--radius-sm);
outline: none;
}
.input:focus {
border-color: var(--accent);
box-shadow: 0 0 0 3px rgba(217, 119, 87, 0.16);
}
.avatar {
display: inline-flex;
align-items: center;
justify-content: center;
width: var(--avatar-size, 38px);
height: var(--avatar-size, 38px);
border-radius: 50%;
color: var(--text-soft);
background: var(--surface-warm);
font-family: var(--font-mono);
font-size: var(--text-sm);
font-weight: 700;
letter-spacing: 0.02em;
flex: 0 0 auto;
}
.code-block {
margin: 0;
padding: var(--space-4) var(--space-5);
overflow-x: auto;
color: #E8E6DC;
background: var(--color-slate);
border-radius: var(--radius-md);
font-family: var(--font-mono);
font-size: var(--text-sm);
line-height: var(--leading-loose);
}
.code-block[data-kind="command"],
.code-block[data-wrap="true"],
.command-block {
max-width: 100%;
min-width: 0;
box-sizing: border-box;
white-space: pre-wrap;
overflow-wrap: anywhere;
word-break: break-word;
overflow-x: hidden;
}
.code-block[data-wrap="true"] code,
.code-block[data-kind="command"] code {
display: block;
max-width: 100%;
white-space: pre-wrap;
overflow-wrap: anywhere;
word-break: break-word;
}
.copyable {
position: relative;
}
.copyable > .code-block {
padding-inline-end: 88px;
}
.copy-button {
appearance: none;
position: absolute;
inset-block-start: var(--space-2);
inset-inline-end: var(--space-2);
display: inline-flex;
align-items: center;
justify-content: center;
min-height: 26px;
padding: 0 9px;
border: 1px solid rgba(250, 249, 245, 0.18);
border-radius: var(--radius-sm);
color: var(--color-gray-100);
background: rgba(255, 255, 255, 0.08);
font-family: var(--font-mono);
font-size: var(--text-xs);
font-weight: 700;
letter-spacing: 0.04em;
text-transform: uppercase;
transition:
background-color var(--dur-fast) var(--ease),
border-color var(--dur-fast) var(--ease),
transform var(--dur-fast) var(--ease);
}
.copy-button:hover {
transform: translateY(-1px);
border-color: rgba(250, 249, 245, 0.35);
background: rgba(255, 255, 255, 0.14);
}
.copy-button[data-copied="true"] {
color: var(--color-white);
background: rgba(120, 140, 93, 0.55);
border-color: rgba(120, 140, 93, 0.75);
}
.reference-panel {
--panel-pad: var(--space-5);
}
.callout {
padding: var(--space-5);
color: var(--text-soft);
background: var(--surface-tint);
border: var(--border);
border-inline-start: 4px solid var(--accent);
border-radius: var(--radius-md);
}
.callout[data-tone="success"] {
border-inline-start-color: var(--success);
background: rgba(120, 140, 93, 0.10);
}
.callout[data-tone="warning"] {
border-inline-start-color: var(--warning);
background: rgba(199, 142, 63, 0.11);
}
.callout[data-tone="danger"] {
border-inline-start-color: var(--danger);
background: rgba(176, 74, 63, 0.10);
}
.callout[data-tone="info"] {
border-inline-start-color: var(--info);
background: rgba(106, 140, 175, 0.11);
}
.callout-label {
display: block;
margin-bottom: var(--space-2);
color: var(--text-muted);
font-family: var(--font-mono);
font-size: var(--text-xs);
font-weight: 700;
letter-spacing: 0.09em;
text-transform: uppercase;
}
.callout > :last-child {
margin-bottom: 0;
}
.checklist {
list-style: none;
margin: 0;
padding: 0;
display: grid;
gap: var(--space-3);
}
.checklist li {
display: block !important;
position: relative !important;
min-width: 0;
padding-inline-start: calc(18px + var(--space-3));
overflow-wrap: anywhere;
}
.checklist li::before {
content: "✓";
position: absolute !important;
inset-block-start: 2px;
inset-inline-start: 0;
display: inline-flex;
align-items: center;
justify-content: center;
width: 18px;
height: 18px;
border-radius: 50%;
color: var(--success);
background: rgba(120, 140, 93, 0.12);
font-family: var(--font-mono);
font-size: 12px;
font-weight: 700;
line-height: 1;
}
.checklist code {
white-space: normal;
overflow-wrap: anywhere;
word-break: break-word;
}
.plain-list {
margin: 0;
padding-inline-start: 1.15em;
color: var(--text-soft);
}
.plain-list li {
overflow-wrap: anywhere;
}
.plain-list li + li {
margin-top: var(--space-2);
}
.plain-list li::marker {
color: var(--text-muted);
}
.insight-list,
.takeaway-list {
list-style: none;
margin: 0;
padding: 0;
display: grid;
gap: var(--space-3);
}
.insight-list li,
.takeaway-list li {
position: relative;
display: block;
padding-inline-start: calc(10px + var(--space-3));
color: var(--text-soft);
}
.insight-list li::before,
.takeaway-list li::before {
content: "";
position: absolute;
inset-block-start: 0.65em;
inset-inline-start: 0;
width: 7px;
height: 7px;
border-radius: 50%;
background: var(--accent);
}
.insight-list li > *,
.takeaway-list li > * {
min-width: 0;
}
.insight-list[data-tone="success"] li::before,
.takeaway-list[data-tone="success"] li::before {
background: var(--success);
}
.insight-list[data-tone="info"] li::before,
.takeaway-list[data-tone="info"] li::before {
background: var(--info);
}
.metric-list {
--metric-label: 132px;
--metric-value: 64px;
display: grid;
gap: var(--space-3);
}
.metric-row {
display: grid;
grid-template-columns: var(--metric-label) minmax(120px, 1fr) var(--metric-value);
gap: var(--space-3);
align-items: center;
}
.metric-row > :first-child {
min-width: 0;
}
.metric-row > :last-child {
justify-self: end;
font-family: var(--font-mono);
font-size: var(--text-sm);
}
.meter {
height: 9px;
overflow: hidden;
border-radius: var(--radius-pill);
background: var(--surface-tint);
}
.meter span {
display: block;
width: var(--value);
height: 100%;
background: var(--tone, var(--accent));
}
.flow-list {
list-style: none;
margin: 0;
padding: 0;
display: grid;
gap: var(--space-3);
}
.flow-step {
position: relative;
display: grid;
grid-template-columns: 42px minmax(0, 1fr);
gap: var(--space-3);
align-items: start;
padding: var(--space-4);
background: var(--surface);
border: var(--border);
border-radius: var(--radius-md);
}
.flow-step > .flow-num {
grid-column: 1;
}
.flow-step > :not(.flow-num) {
grid-column: 2;
min-width: 0;
}
.flow-step::after {
content: "";
position: absolute;
inset-block-start: calc(100% + 1px);
inset-inline-start: 36px;
width: 1.5px;
height: var(--space-3);
background: var(--border-color);
}
.flow-step:last-child::after {
display: none;
}
.flow-num {
display: inline-flex;
align-items: center;
justify-content: center;
width: 34px;
height: 34px;
border-radius: var(--radius-pill);
color: var(--text);
background: var(--surface-warm);
box-shadow: inset 0 0 0 1px rgba(20, 20, 19, 0.10);
font-family: var(--font-mono);
font-size: var(--text-sm);
font-weight: 700;
}
.flow-title {
margin: 0 0 var(--space-1);
font-family: var(--font-serif);
font-size: var(--text-xl);
font-weight: 550;
line-height: var(--leading-title);
}
.flow-detail {
margin: 0;
color: var(--text-soft);
}
.flow-branch {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: var(--space-3);
}
.flow-branch .flow-step::after {
display: none;
}
.flow-step[data-tone="success"] {
border-color: rgba(120, 140, 93, 0.38);
background: rgba(120, 140, 93, 0.08);
}
.flow-step[data-tone="warning"] {
border-color: rgba(199, 142, 63, 0.42);
background: rgba(199, 142, 63, 0.09);
}
@media (max-width: 620px) {
.metric-row {
grid-template-columns: 1fr max-content;
}
.metric-row .meter {
grid-column: 1 / -1;
order: 3;
}
.flow-branch {
grid-template-columns: 1fr;
}
}
/* ---------- Charts and numeric data ---------- */
.chart-panel {
max-width: 100%;
min-width: 0;
overflow: hidden;
}
.chart-panel > * {
min-width: 0;
}
.chart-panel svg,
svg.chart-svg {
display: block;
width: 100%;
height: auto;
}
.chart-caption {
color: var(--text-muted);
font-size: var(--text-sm);
}
.numeric-table-wrap {
overflow-x: auto;
background: var(--surface);
border: var(--border);
border-radius: var(--radius-md);
}
.numeric-table {
width: 100%;
min-width: 720px;
border-collapse: collapse;
}
.numeric-table th,
.numeric-table td {
padding: var(--space-3) var(--space-4);
border-bottom: 1px solid var(--color-gray-200);
vertical-align: middle;
}
.numeric-table th {
color: var(--text-muted);
background: var(--surface-tint);
font-family: var(--font-mono);
font-size: var(--text-xs);
font-weight: 700;
letter-spacing: 0.08em;
text-align: left;
text-transform: uppercase;
}
.numeric-table tr:last-child td {
border-bottom: 0;
}
.numeric-table .metric,
.numeric-table .num {
text-align: right;
font-variant-numeric: tabular-nums;
white-space: nowrap;
}
.numeric-table .entity,
.numeric-table .label-cell {
font-weight: 650;
}
.numeric-table .note {
color: var(--text-soft);
}
.numeric-table code {
white-space: normal;
overflow-wrap: anywhere;
word-break: break-word;
}
.numeric-table .subtle {
display: block;
margin-top: 2px;
color: var(--text-muted);
font-family: var(--font-mono);
font-size: var(--text-xs);
font-weight: 400;
}
@media (max-width: 620px) {
.numeric-table {
min-width: 0;
}
.numeric-table thead {
display: none;
}
.numeric-table tbody,
.numeric-table tr,
.numeric-table td {
display: block;
}
.numeric-table tr {
padding: var(--space-3) var(--space-4);
border-bottom: 1px solid var(--color-gray-200);
}
.numeric-table tr:last-child {
border-bottom: 0;
}
.numeric-table td {
padding: var(--space-1) 0;
border-bottom: 0;
}
.numeric-table td[data-label] {
display: flex;
justify-content: space-between;
gap: var(--space-4);
}
.numeric-table td[data-label]::before {
content: attr(data-label);
color: var(--text-muted);
font-family: var(--font-mono);
font-size: var(--text-xs);
text-transform: uppercase;
}
}
/* ---------- Diff ---------- */
.diff {
overflow-x: auto;
color: #E8E6DC;
background: var(--color-slate);
border-radius: var(--radius-md);
font-family: var(--font-mono);
font-size: 12.5px;
line-height: var(--leading-loose);
}
.diff-row {
display: grid;
grid-template-columns: 48px 18px minmax(max-content, 1fr);
align-items: baseline;
min-width: max-content;
padding-inline-end: var(--space-4);
white-space: pre;
}
.diff-row .ln {
padding-inline-end: 14px;
color: var(--color-gray-500);
text-align: right;
user-select: none;
}
.diff-row .mark {
color: var(--color-gray-500);
text-align: center;
}
.diff-row .code {
color: #E8E6DC;
}
.diff-row.ctx .code,
.diff-row[data-kind="ctx"] .code {
color: #B8B6AC;
}
.diff-row.add,
.diff-row[data-kind="add"] {
background: rgba(120, 140, 93, 0.24);
box-shadow: inset 3px 0 0 rgba(120, 140, 93, 0.78);
}
.diff-row.add .mark,
.diff-row[data-kind="add"] .mark {
color: #B9D394;
font-weight: 800;
}
.diff-row.del,
.diff-row[data-kind="del"] {
background: rgba(176, 74, 63, 0.24);
box-shadow: inset 3px 0 0 rgba(176, 74, 63, 0.82);
}
.diff-row.del .mark,
.diff-row[data-kind="del"] .mark {
color: #F19A8D;
font-weight: 800;
}
.diff-row.hunk,
.diff-row[data-kind="hunk"] {
background: rgba(255, 255, 255, 0.045);
}
.diff-row.hunk .code,
.diff-row[data-kind="hunk"] .code {
color: var(--color-gray-500);
}
.diff[data-wrap="true"] {
overflow-x: hidden;
}
.diff[data-wrap="true"] .diff-row {
grid-template-columns: 42px 18px minmax(0, 1fr);
min-width: 0;
white-space: normal;
}
.diff[data-wrap="true"] .code {
white-space: pre-wrap;
overflow-wrap: anywhere;
}
/* ---------- Timeline ---------- */
.timeline {
position: relative;
display: grid;
gap: 0;
padding-inline-start: var(--space-6);
}
.timeline:is(ol, ul) {
margin: 0;
list-style: none;
}
.timeline::before {
content: "";
position: absolute;
inset-block: 4px 4px;
inset-inline-start: 9px;
width: 1.5px;
background: var(--border-color);
}
.timeline-item,
.tl-entry {
position: relative;
display: grid;
grid-template-columns: minmax(84px, max-content) minmax(0, 1fr);
gap: var(--space-4);
padding-block: 0 var(--space-5);
}
.timeline-item:last-child,
.tl-entry:last-child {
padding-block-end: 0;
}
.timeline-item::before,
.tl-entry::before {
content: "";
position: absolute;
inset-block-start: 4px;
inset-inline-start: calc(4.25px - var(--space-6));
width: 11px;
height: 11px;
border: 2px solid var(--surface);
border-radius: 50%;
background: var(--accent);
box-shadow: 0 0 0 1.5px var(--accent);
}
.timeline-item:not(:has(> .timeline-time, > .tl-time)),
.tl-entry:not(:has(> .timeline-time, > .tl-time)) {
display: block;
}
.timeline-time,
.tl-time {
color: var(--text-muted);
font-family: var(--font-mono);
font-size: var(--text-sm);
white-space: nowrap;
}
.timeline-body,
.tl-body {
min-width: 0;
color: var(--text-soft);
}
.timeline-body strong,
.tl-body strong {
color: var(--text);
}
@media (max-width: 620px) {
.timeline-item,
.tl-entry {
grid-template-columns: 1fr;
gap: var(--space-1);
}
}
/* ---------- Flowchart ---------- */
.flowchart {
display: block;
width: 100%;
height: auto;
}
svg.flow,
svg.flowchart {
display: block;
width: 100%;
height: auto;
}
.flow text,
.flowchart text {
fill: var(--text);
font-family: var(--font-sans);
font-size: 12px;
}
.flow .sub,
.flowchart .sub {
fill: var(--text-muted);
font-size: 10px;
}
.flow-edge,
.edge {
fill: none;
stroke: var(--text-muted);
stroke-width: 1.5;
}
.flow-edge[data-kind="yes"],
.edge.yes {
stroke: var(--success);
}
.flow-edge[data-kind="no"],
.edge.no {
stroke: var(--danger);
stroke-dasharray: 4 4;
}
/* SVG flow nodes */
.flow-node,
.flow .node {
cursor: pointer;
transition: transform var(--dur-fast) var(--ease);
}
.flow-node:hover,
.flow .node:hover {
transform: translateY(-1px);
}
.flow-node rect,
.flow .node rect {
fill: var(--surface);
stroke: var(--border-color);
stroke-width: 1.5;
rx: 8;
}
.flow-node path,
.flow .node.gate path {
fill: var(--surface);
stroke: var(--border-color);
stroke-width: 1.5;
}
.flow-node[data-shape="term"] rect,
.flow .node.term rect {
fill: var(--surface-tint);
rx: 22;
}
.flow-node[data-kind="ok"] rect,
.flow .node.ok rect {
fill: rgba(120, 140, 93, 0.12);
stroke: var(--success);
}
.flow-node[data-kind="bad"] rect,
.flow .node.bad rect {
fill: rgba(176, 74, 63, 0.10);
stroke: var(--danger);
}
.flow-node.active rect,
.flow-node.active path,
.flow .node.active rect,
.flow .node.active path {
stroke: var(--accent);
stroke-width: 2;
}
/* HTML flow nodes, for non-SVG diagrams */
div.flow-node {
padding: var(--space-3) var(--space-4);
background: var(--surface);
border: var(--border);
border-radius: var(--radius-sm);
transition:
transform var(--dur-fast) var(--ease),
border-color var(--dur-fast) var(--ease),
box-shadow var(--dur-fast) var(--ease);
}
div.flow-node:hover {
transform: translateY(var(--hover-y));
border-color: rgba(217, 119, 87, 0.55);
box-shadow: var(--shadow-sm);
}
div.flow-node[data-shape="term"] {
border-radius: var(--radius-pill);
background: var(--surface-tint);
}
div.flow-node[data-kind="ok"] {
border-color: var(--success);
background: rgba(120, 140, 93, 0.12);
}
div.flow-node[data-kind="bad"] {
border-color: var(--danger);
background: rgba(176, 74, 63, 0.10);
}
@media (prefers-reduced-motion: reduce) {
*,
*::before,
*::after {
scroll-behavior: auto !important;
transition-duration: 0.01ms !important;
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
}
}
/* Responsive safety net for generated artifacts. */
p,
li {
overflow-wrap: anywhere;
}
@media (max-width: 860px) {
.split,
.section-rail,
.sidebar-layout {
grid-template-columns: minmax(0, 1fr) !important;
}
.split > *,
.section-rail > *,
.sidebar-layout > *,
.chart-panel {
width: 100%;
max-width: 100%;
min-width: 0;
}
}</style>
<style>
.pipeline-svg { width: 100%; max-width: 360px; display: block; margin: 0 auto; height: auto; }
.numeric-table code { white-space: normal; overflow-wrap: anywhere; word-break: break-word; }
</style>
</head>
<body>
<main class="page stack" data-gap="lg">
<header class="stack" data-gap="sm">
<div class="eyebrow">Module explainer</div>
<h1>Rendering Checker Pipeline</h1>
<p class="lede">How <code>scripts/check_birch_renderings.py</code> validates Birch HTML artifacts — from static HTML parsing through optional Chrome screenshots and browser geometry audits to structured findings reports. Written for new contributors who need to modify or extend the checker without breaking existing eval runs.</p>
<div class="cluster">
<span class="chip">check_birch_renderings.py</span>
<span class="chip" data-tone="olive">birch_mpl.py</span>
<span class="chip">run_eval.py</span>
</div>
</header>
<!-- ── SECTION 1: Mental model ──────────────────────────── -->
<section class="section stack" data-gap="md">
<div class="section-head">
<div>
<span class="eyebrow">Mental model</span>
<h2>A three-layer deterministic quality gate</h2>
</div>
</div>
<div class="card stack" data-variant="flat">
<p>Picture the checker as a funnel with three concentric layers. The <strong>static layer</strong> runs pure Python HTML parsers — no browser required — to validate Birch component contracts, embedded CSS validity, CSS-variable usage, and forbidden patterns (gradients, patch markers, SVG fill=none violations). This runs on any machine.</p>
<p>The <strong>visual layer</strong> (Chrome required) captures headless screenshots at the requested viewport. Pillow then samples every fourth pixel and measures how many fall within tolerance of Birch's canonical 12-colour palette (<code>ivory</code>, <code>slate</code>, <code>clay</code>, <code>oat</code>, <code>olive</code>, <code>rust</code>, <code>sky</code>, and five grays). Palette-close fraction below 40 % is a warning; non-background fraction below 2.5 % is a fail (blank render); blackish fraction above 8 % is a warning (unstyled SVG or code blocks).</p>
<p>The <strong>geometry layer</strong> (Chrome required) injects roughly 300 lines of audit JavaScript into a temporary copy of the artifact, loads it through <code>--dump-dom</code>, and reads layout measurements from the <code>data-birch-audit</code> attribute it writes to <code>document.body</code>. It catches overflow, metric-bar misalignment, stat-card squeeze, pathological text wrapping, and code-block underfill.</p>
</div>
<div class="auto-grid" style="--grid-min: 240px">
<div class="card stack" data-variant="outlined">
<h3>Artifact mode</h3>
<p>Pass <code>--artifact path</code> to check a single generated output with no reference comparison. Use this for all eval outputs. Repeatable: pass the flag multiple times for multiple artifacts.</p>
<p class="caption">Source: <code>check_artifact()</code></p>
</div>
<div class="card stack" data-variant="outlined">
<h3>Pair mode</h3>
<p>Pass <code>--pair original:candidate</code> to compare a reference page against a Birch candidate. Adds structural delta findings (word coverage, section-count drift, Birch class delta). Use only when a meaningful reference exists.</p>
<p class="caption">Source: <code>compare_pair()</code></p>
</div>
<div class="card stack" data-variant="outlined">
<h3>Exit codes</h3>
<p>Exits 1 if any <code>fail</code> findings exist. Pass <code>--fail-on-warn</code> to also exit 1 on warnings. The two modes are mutually exclusive — passing both flags raises <code>SystemExit</code> in <code>main()</code>.</p>
<p class="caption">Source: <code>main()</code> — last block</p>
</div>
</div>
</section>
<!-- ── SECTION 2: Runtime path ──────────────────────────── -->
<section class="section stack" data-gap="lg">
<div class="section-head">
<div>
<span class="eyebrow">Runtime path</span>
<h2>From input HTML to reports</h2>
</div>
</div>
<div class="panel chart-panel stack" data-gap="sm">
<svg class="pipeline-svg" viewBox="0 0 300 395"
xmlns="http://www.w3.org/2000/svg"
role="img"
aria-label="Pipeline flow diagram: input HTML feeds page_stats which feeds contract_findings. Then optionally capture plus pixel metrics, then optionally geometry_audit via injected JavaScript. Finally JSON and Markdown reports are written.">
<defs>
<marker id="arr" markerWidth="7" markerHeight="7" refX="5" refY="3.5"
orient="auto" markerUnits="userSpaceOnUse">
<path d="M0,0 L0,7 L7,3.5 z" fill="#D1CFC5"/>
</marker>
</defs>
<!-- Node 1: Input HTML -->
<rect x="30" y="8" width="240" height="38" rx="5"
fill="#FFFFFF" stroke="#D1CFC5" stroke-width="1.5"/>
<text x="150" y="24" text-anchor="middle"
fill="#141413" font-family="sans-serif" font-size="12" font-weight="600">Input HTML file(s)</text>
<text x="150" y="38" text-anchor="middle"
fill="#87867F" font-family="sans-serif" font-size="10">--artifact path · --pair ref:candidate</text>
<!-- Arrow 1→2 -->
<line x1="150" y1="46" x2="150" y2="69" stroke="#D1CFC5" stroke-width="1.5" marker-end="url(#arr)"/>
<!-- Node 2: page_stats() -->
<rect x="30" y="76" width="240" height="38" rx="5"
fill="#FFFFFF" stroke="#D1CFC5" stroke-width="1.5"/>
<text x="150" y="92" text-anchor="middle"
fill="#141413" font-family="sans-serif" font-size="12" font-weight="600">page_stats()</text>
<text x="150" y="106" text-anchor="middle"
fill="#87867F" font-family="sans-serif" font-size="10">StatsParser · CSS vars · Birch CSS check</text>
<!-- Arrow 2→3 -->
<line x1="150" y1="114" x2="150" y2="137" stroke="#D1CFC5" stroke-width="1.5" marker-end="url(#arr)"/>
<!-- Node 3: contract_findings() — clay accent -->
<rect x="30" y="144" width="240" height="38" rx="5"
fill="#FAF9F5" stroke="#D97757" stroke-width="1.5"/>
<text x="150" y="160" text-anchor="middle"
fill="#141413" font-family="sans-serif" font-size="12" font-weight="600">contract_findings()</text>
<text x="150" y="174" text-anchor="middle"
fill="#87867F" font-family="sans-serif" font-size="10">8 parsers · gradient check · SVG fill</text>
<!-- Arrow 3→4 -->
<line x1="150" y1="182" x2="150" y2="205" stroke="#D1CFC5" stroke-width="1.5" marker-end="url(#arr)"/>
<!-- Node 4: Chrome capture — dashed (optional) -->
<rect x="30" y="212" width="240" height="38" rx="5"
fill="#FFFFFF" stroke="#D1CFC5" stroke-width="1.5" stroke-dasharray="5,3"/>
<text x="150" y="228" text-anchor="middle"
fill="#141413" font-family="sans-serif" font-size="12" font-weight="600">capture() + pixel metrics</text>
<text x="150" y="242" text-anchor="middle"
fill="#87867F" font-family="sans-serif" font-size="10">Chrome headless · Pillow palette analysis</text>
<!-- Arrow 4→5 -->
<line x1="150" y1="250" x2="150" y2="273" stroke="#D1CFC5" stroke-width="1.5" marker-end="url(#arr)"/>
<!-- Node 5: geometry_audit() — dashed (optional) -->
<rect x="30" y="280" width="240" height="38" rx="5"
fill="#FFFFFF" stroke="#D1CFC5" stroke-width="1.5" stroke-dasharray="5,3"/>
<text x="150" y="296" text-anchor="middle"
fill="#141413" font-family="sans-serif" font-size="12" font-weight="600">geometry_audit()</text>
<text x="150" y="310" text-anchor="middle"
fill="#87867F" font-family="sans-serif" font-size="10">Injected JS · --dump-dom · overflow</text>
<!-- Arrow 5→6 -->
<line x1="150" y1="318" x2="150" y2="341" stroke="#D1CFC5" stroke-width="1.5" marker-end="url(#arr)"/>
<!-- Node 6: Reports — olive accent -->
<rect x="30" y="348" width="240" height="38" rx="5"
fill="#FAF9F5" stroke="#788C5D" stroke-width="1.5"/>
<text x="150" y="364" text-anchor="middle"
fill="#141413" font-family="sans-serif" font-size="12" font-weight="600">JSON + Markdown reports</text>
<text x="150" y="378" text-anchor="middle"
fill="#87867F" font-family="sans-serif" font-size="10">reports/birch-rendering-check.{json,md}</text>
</svg>
<p class="chart-caption">Dashed borders = requires Chrome and Pillow. Source: <code>scripts/check_birch_renderings.py</code><code>main()</code>, <code>check_artifact()</code>, <code>compare_pair()</code>.</p>
</div>
<ol class="flow-list">
<li class="flow-step">
<span class="flow-num">1</span>
<div class="stack" data-gap="sm">
<h3 class="flow-title">Parse arguments, find Chrome</h3>
<p class="flow-detail"><code>main()</code> collects <code>--artifact</code> or <code>--pair</code> flags and resolves all paths relative to the repository root (two levels above the script via <code>ROOT = Path(__file__).resolve().parents[1]</code>). It reads <code>styles/birch-system.css</code> to extract the full set of defined CSS variables — these become the allowed set for the unknown-var check. <code>find_chrome()</code> probes PATH for <code>google-chrome</code>, <code>google-chrome-stable</code>, <code>chromium</code>, and <code>chromium-browser</code> in that order.</p>
</div>
</li>
<li class="flow-step">
<span class="flow-num">2</span>
<div class="stack" data-gap="sm">
<h3 class="flow-title">Collect page statistics</h3>
<p class="flow-detail"><code>page_stats()</code> reads the full HTML, strips the Birch system CSS block (matched by the <code>data-birch-system</code> attribute), and feeds the remainder through <code>StatsParser</code>, which accumulates tag counts, CSS class counts, data-attribute counts, linked stylesheets, and text-word tokens. It separately scans the raw HTML for CSS variables used and defined. The Birch CSS block is validated: it must be at least 10 000 bytes and contain all five signatures — <code>--color-ivory</code>, <code>--font-serif</code>, <code>.page</code>, <code>.section</code>, <code>.card</code>.</p>
</div>
</li>
<li class="flow-step">
<span class="flow-num">3</span>
<div class="stack" data-gap="sm">
<h3 class="flow-title">Run static contract checks</h3>
<p class="flow-detail"><code>contract_findings()</code> receives the <code>PageStats</code> dataclass and the set of system-defined CSS variables. It runs eight named checks (doctype, viewport, Birch CSS, <code>.page</code> shell, layout primitive count, semantic component count, no unknown CSS vars, no patch marker lines). It then checks for gradient backgrounds and hard-coded colors outside the Birch palette, and runs eight dedicated HTML parser subclasses for component-level contracts. Each parser produces a list of failing examples appended as <code>Finding(level="fail", …)</code>.</p>
</div>
</li>
<li class="flow-step">
<span class="flow-num">4</span>
<div class="stack" data-gap="sm">
<h3 class="flow-title">Capture screenshot (Chrome)</h3>
<p class="flow-detail">If Chrome was found, <code>capture()</code> runs <code>--headless=new --screenshot</code> using the artifact's real <code>file://</code> URI so relative stylesheet links resolve. For <code>deep</code> or <code>mobile-deep</code> viewports, <code>capture_height_for_viewport()</code> calls <code>measure_document_height()</code> first: it injects a small load-event script into a temp file, measures <code>document.scrollHeight</code> via <code>--dump-dom --virtual-time-budget=300</code>, then caps the capture at <code>min(viewport_max, measured + 80)</code> to avoid blank space at the bottom of short pages.</p>
</div>
</li>
<li class="flow-step">
<span class="flow-num">5</span>
<div class="stack" data-gap="sm">
<h3 class="flow-title">Measure pixel metrics (Pillow)</h3>
<p class="flow-detail"><code>artifact_screenshot_metrics()</code> opens the PNG, down-samples to one-quarter resolution, and walks every pixel. It counts pixels close to the ivory background (delta ≤ 26), pixels close to any Birch palette colour (delta ≤ 26), and near-black pixels (R/G/B &lt; 12). The resulting fractions drive three findings: <code>blank_render</code> fail if non-background fraction &lt; 2.5 %, <code>palette_drift</code> warn if palette-close fraction &lt; 40 %, <code>black_pixel_fraction</code> warn if blackish fraction &gt; 8 %.</p>
</div>
</li>
<li class="flow-step">
<span class="flow-num">6</span>
<div class="stack" data-gap="sm">
<h3 class="flow-title">Run geometry audit (injected JS)</h3>
<p class="flow-detail"><code>geometry_audit()</code> prepends <code>&lt;base href="repo-root/"&gt;</code> inside <code>&lt;head&gt;</code> (so the shared stylesheet loads from the correct location) and appends the audit JavaScript before <code>&lt;/head&gt;</code>. Chrome runs the modified page through <code>--dump-dom --virtual-time-budget=800</code>. The JS writes all measurements to <code>data-birch-audit</code> on <code>document.body</code>; the function regex-extracts and JSON-parses that attribute, then <code>geometry_findings()</code> converts each symptom into a <code>Finding</code>.</p>
</div>
</li>
<li class="flow-step">
<span class="flow-num">7</span>
<div class="stack" data-gap="sm">
<h3 class="flow-title">Write reports and exit</h3>
<p class="flow-detail"><code>main()</code> assembles the full payload dict, writes <code>reports/birch-rendering-check.json</code> (structured, with per-artifact stats, screenshot metrics, geometry data, and findings arrays) and <code>reports/birch-rendering-check.md</code> (Markdown table via <code>render_markdown()</code>). It prints both paths to stdout, then exits 1 if failures (or warnings with <code>--fail-on-warn</code>) exist.</p>
</div>
</li>
</ol>
</section>
<!-- ── SECTION 3: File tour ──────────────────────────────── -->
<section class="section stack" data-gap="lg">
<div class="section-head">
<div>
<span class="eyebrow">File tour</span>
<h2>Read these in order</h2>
</div>
</div>
<ol class="flow-list">
<li class="flow-step">
<span class="flow-num">1</span>
<div class="stack" data-gap="sm">
<h3 class="flow-title">scripts/check_birch_renderings.py</h3>
<p class="flow-detail">The main entry point. Start with <code>main()</code> to see argument parsing and mode selection. Then read <code>check_artifact()</code> (artifact mode) and <code>compare_pair()</code> (pair mode) to understand which sub-functions each calls. Work down through <code>page_stats()</code>, <code>contract_findings()</code>, and <code>geometry_audit()</code>. The eight HTML parser subclasses (<code>StatsParser</code>, <code>MetricRowParser</code>, <code>DiffRowParser</code>, <code>DiffPolarityParser</code>, <code>FlowStepParser</code>, <code>GridListParser</code>, <code>ChecklistParser</code>, <code>TimelineParser</code>) are the canonical definitions of every component contract the checker enforces. Finish with <code>render_markdown()</code> and <code>geometry_findings()</code>.</p>
</div>
</li>
<li class="flow-step">
<span class="flow-num">2</span>
<div class="stack" data-gap="sm">
<h3 class="flow-title">scripts/birch_mpl.py</h3>
<p class="flow-detail">The Matplotlib SVG helper. Read <code>COLORS</code> and <code>PALETTE</code> first — these are the Birch hex values used for both chart colours and the pixel-distance palette check. Read <code>rc_params()</code> to understand the full chart theme (font, axis colours, grid, legend, transparent background). Then read <code>birch_theme()</code> (context manager), <code>polish_axes()</code> (removes top/right spines, adds grid), and <code>svg_string()</code>, which strips Matplotlib metadata and returns a bare SVG element ready for inlining. Callers patch in <code>class="chart-svg"</code> and <code>role="img"</code> via <code>re.sub()</code> after the call.</p>
</div>
</li>
<li class="flow-step">
<span class="flow-num">3</span>
<div class="stack" data-gap="sm">
<h3 class="flow-title">evals/charts/build_chart_brief.py</h3>
<p class="flow-detail">A complete artifact builder and canonical usage example. Shows the full loop: load CSV data from <code>evals/charts/sample-data.csv</code>, call <code>birch_theme()</code> as a context manager, call <code>svg_string(fig)</code> then patch the SVG tag with <code>re.sub()</code>, then f-string-compose the full HTML artifact to <code>24-birch-chart-brief.html</code> in the repository root. Study <code>metric_rows()</code> and <code>table_rows()</code> for exact <code>.metric-row</code> and <code>.numeric-table</code> contract examples. The builder links <code>styles/birch-system.css</code> directly — the checker handles both linked and embedded forms.</p>
</div>
</li>
<li class="flow-step">
<span class="flow-num">4</span>
<div class="stack" data-gap="sm">
<h3 class="flow-title">evals/charts/run_eval.py</h3>
<p class="flow-detail">The eval orchestrator. Calls <code>build_chart_brief.py</code> first via <code>uv run</code>, then runs the checker four times with different <code>--viewport</code> flags: <code>desktop:1365x900</code>, <code>mobile:390x900</code>, <code>deep:1365x2400</code>, and <code>mobile-deep:390x3000</code>. Each run receives distinct <code>--out</code> and <code>--markdown</code> paths so reports do not overwrite each other. To add a new eval artifact: create a builder script, add viewport entries to the <code>VIEWPORTS</code> dict, and add the builder call at the top of <code>main()</code> before the checker loop.</p>
</div>
</li>
</ol>
</section>
<!-- ── SECTION 4: Checker components ────────────────────── -->
<section class="section stack" data-gap="md">
<div class="section-head">
<div>
<span class="eyebrow">Components</span>
<h2>HTML parsers and what they enforce</h2>
</div>
</div>
<div class="auto-grid" style="--grid-min: 260px">
<div class="card stack" data-variant="outlined">
<h3>StatsParser</h3>
<p class="caption">Used by: <code>page_stats()</code></p>
<ul class="plain-list">
<li>Counts every HTML tag, CSS class, and <code>data-*</code> attribute.</li>
<li>Extracts the page title, text words, inline style values, and linked stylesheets.</li>
<li>Skips content inside <code>&lt;style&gt;</code> and <code>&lt;script&gt;</code> using a <code>skip_depth</code> counter to avoid false class/text matches.</li>
</ul>
</div>
<div class="card stack" data-variant="outlined">
<h3>MetricRowParser</h3>
<p class="caption">Used by: <code>contract_findings()</code></p>
<ul class="plain-list">
<li>Validates that every <code>.metric-row</code> div has exactly three direct children: <code>.caption</code>, <code>.meter</code>, <code>code</code>.</li>
<li>A fourth child creates an extra implicit grid column, misaligning the value against its meter bar.</li>
</ul>
</div>
<div class="card stack" data-variant="outlined">
<h3>DiffRowParser + DiffPolarityParser</h3>
<p class="caption">Used by: <code>contract_findings()</code></p>
<ul class="plain-list">
<li><strong>DiffRowParser</strong>: checks each <code>.diff-row</code> has exactly <code>span.ln</code> + <code>span.mark</code> + <code>span.code</code>. Loose text in the row lands in the narrow line-number column and wraps to one character per line.</li>
<li><strong>DiffPolarityParser</strong>: rows with a <code>+</code> mark must carry <code>.add</code> or <code>data-kind=add</code> so Birch applies the green colour token.</li>
</ul>
</div>
<div class="card stack" data-variant="outlined">
<h3>FlowStepParser</h3>
<p class="caption">Used by: <code>contract_findings()</code></p>
<ul class="plain-list">
<li>Checks each <code>.flow-step</code> has exactly two direct children: <code>.flow-num</code> first, then one content wrapper.</li>
<li>Loose title or detail elements placed directly in the step (without a wrapper) can overflow into the narrow number column.</li>
</ul>
</div>
<div class="card stack" data-variant="outlined">
<h3>GridListParser + ChecklistParser</h3>
<p class="caption">Used by: <code>contract_findings()</code></p>
<ul class="plain-list">
<li><strong>GridListParser</strong>: items in <code>.insight-list</code> and <code>.takeaway-list</code> must wrap their content in exactly one direct child — these lists use CSS grid and extra direct children become accidental grid cells.</li>
<li><strong>ChecklistParser</strong>: flags checklist items with multiple direct inline elements when the list uses flex layout.</li>
</ul>
</div>
<div class="card stack" data-variant="outlined">
<h3>TimelineParser</h3>
<p class="caption">Used by: <code>contract_findings()</code></p>
<ul class="plain-list">
<li>Each <code>.timeline-item</code> must have both a <code>.timeline-time</code> child and a <code>.timeline-body</code> child, unless the parent timeline uses a custom scoped class.</li>
</ul>
</div>
<div class="card stack" data-variant="outlined">
<h3>svg_visual_findings()</h3>
<p class="caption">Used by: <code>contract_findings()</code></p>
<ul class="plain-list">
<li>Scans all <code>&lt;polyline&gt;</code> and <code>&lt;path&gt;</code> elements with chart-line classes (<code>chart-line</code>, <code>line-pass</code>, <code>trend-line</code>).</li>
<li>Fails if the element lacks <code>fill="none"</code>: the browser default fills the path solid black, producing a large wedge instead of a line chart.</li>
<li>Fails if no explicit <code>stroke</code> colour is set.</li>
</ul>
</div>
<div class="card stack" data-variant="outlined">
<h3>geometry_audit() — JS probes</h3>
<p class="caption">Used by: <code>check_artifact()</code>, <code>compare_pair()</code></p>
<ul class="plain-list">
<li>Measures <code>scrollWidth − clientWidth</code> and <code>getBoundingClientRect().right − viewportWidth</code> for text, code, chips, badges, and stat-values.</li>
<li>Computes meter x-position spread within each <code>.metric-list</code> and flags if spread exceeds 2 px.</li>
<li>Detects stat-card squeeze (card width &lt; 150 px or value clips right edge).</li>
<li>Checks <code>.section-rail</code>: aside rendered to the <em>left</em> of main content triggers a warning.</li>
<li>Also checks timeline marker geometry, code-block underfill, and pathological text wrapping.</li>
</ul>
</div>
</div>
</section>
<!-- ── SECTION 5: Gotchas + Extension points ────────────── -->
<section class="section stack" data-gap="lg">
<div class="section-head">
<div>
<span class="eyebrow">Gotchas &amp; extension points</span>
<h2>Where to be careful and where to make changes</h2>
</div>
</div>
<div class="section-rail">
<div class="stack" data-gap="md">
<h3>Common gotchas</h3>
<div class="card stack" data-variant="outlined">
<h3>Substitute the Birch CSS before checking</h3>
<p>If an artifact still carries the unsubstituted Birch CSS placeholder in its <code>data-birch-system</code> style block, the embedded CSS will be far below the 10 000-byte minimum and the <code>uses_birch_system_css</code> check will fail. Run <code>uv run skill/scripts/finish_birch_html.py &lt;output.html&gt;</code> before invoking the checker — or link <code>styles/birch-system.css</code> via a <code>&lt;link&gt;</code> tag instead.</p>
</div>
<div class="card stack" data-variant="outlined">
<h3>--artifact and --pair are mutually exclusive</h3>
<p>Passing both flags in the same invocation raises <code>SystemExit</code> immediately in <code>main()</code>. All generated eval scripts use <code>--artifact</code>; <code>--pair</code> is only for manual comparison runs against a reference page.</p>
</div>
<div class="card stack" data-variant="outlined">
<h3>Gradient backgrounds are a hard fail</h3>
<p><code>linear-gradient</code>, <code>radial-gradient</code>, and <code>conic-gradient</code> anywhere in page-local CSS produce a <code>no_birch_gradients</code> fail finding. Use flat Birch token surface colours instead.</p>
</div>
<div class="card stack" data-variant="outlined">
<h3>SVG polylines require fill=none</h3>
<p>Browsers default SVG fill to black. Any <code>&lt;polyline&gt;</code> or chart <code>&lt;path&gt;</code> without an explicit <code>fill="none"</code> attribute (or CSS equivalent) fails <code>svg_visual_findings()</code>. The result in a screenshot is a large black wedge rather than a line.</p>
</div>
<div class="card stack" data-variant="outlined">
<h3>Geometry audit adds a base element to a temp copy</h3>
<p><code>geometry_audit()</code> writes a modified copy of the artifact to a <code>tempfile.TemporaryDirectory</code> and adds a <code>&lt;base href="repo-root/"&gt;</code> element so relative stylesheet links resolve. Artifacts that inline Birch CSS avoid this dependency entirely.</p>
</div>
<div class="card stack" data-variant="outlined">
<h3>Deep viewport height uses a second Chrome pass</h3>
<p>Viewport specs containing <code>deep</code> (e.g. <code>deep:1365x2400</code>) trigger <code>measure_document_height()</code>, which injects a tiny measurement script into a temp file and retrieves the natural page scroll height. The final capture height is clamped to <code>min(viewport_max, measured + 80)</code>. Changing a viewport spec string changes which path executes.</p>
</div>
</div>
<aside class="reference-panel stack" data-gap="md">
<h3>Extension points</h3>
<ul class="plain-list">
<li><strong>New component contract:</strong> write a new <code>HTMLParser</code> subclass modelled on <code>MetricRowParser</code>. Instantiate and feed it in <code>contract_findings()</code>. Append a <code>Finding</code> if the bad-items list is non-empty.</li>
<li><strong>New geometry symptom:</strong> add a <code>querySelectorAll</code> block to <code>audit_js</code> in <code>geometry_audit()</code>, include the key in the JSON written to <code>data-birch-audit</code>, and add a handler in <code>geometry_findings()</code>.</li>
<li><strong>Track a new Birch class:</strong> add it to <code>BIRCH_CLASSES</code>, <code>LAYOUT_CLASSES</code>, or <code>SEMANTIC_CLASSES</code> at the top of the file. Change <code>SEMANTIC_CLASS_MIN</code> to adjust the warn threshold.</li>
<li><strong>New palette colour:</strong> add it to <code>PALETTE</code> in <code>check_birch_renderings.py</code> (RGB tuple) and to <code>COLORS</code> in <code>birch_mpl.py</code> (hex string). Keep both in sync.</li>
<li><strong>New eval artifact:</strong> create a builder script following <code>evals/charts/build_chart_brief.py</code>. In <code>run_eval.py</code>, add the builder call to <code>main()</code> and add entries to <code>VIEWPORTS</code>.</li>
<li><strong>Redirect report paths:</strong> pass <code>--out</code> and <code>--markdown</code>. Both default to <code>reports/</code> in the repository root. Use distinct paths when running multiple viewports.</li>
</ul>
<h3>Key thresholds</h3>
<ul class="plain-list">
<li><code>BIRCH_CSS_MIN_BYTES = 10_000</code> — minimum embedded CSS size</li>
<li><code>SEMANTIC_CLASS_MIN = 6</code> — minimum semantic class count (warn if below)</li>
<li><code>0.40</code> — palette-close warn threshold in artifact mode (<code>0.55</code> in pair mode)</li>
<li><code>0.025</code> — non-background fraction fail threshold (blank render)</li>
<li><code>0.08</code> — blackish fraction warn threshold</li>
<li><code>2500</code> bytes — local CSS size warn threshold</li>
</ul>
</aside>
</div>
</section>
<!-- ── SECTION 6: Commands and outputs ──────────────────── -->
<section class="section stack" data-gap="lg">
<div class="section-head">
<div>
<span class="eyebrow">Commands &amp; outputs</span>
<h2>Running the checker</h2>
</div>
</div>
<div class="auto-grid" style="--grid-min: 280px">
<div class="card stack" data-variant="flat">
<h3>Check a single artifact</h3>
<div class="code-block" data-wrap="true">uv run --with pillow python scripts/check_birch_renderings.py --artifact eval-runs/my-run/my-artifact.html</div>
</div>
<div class="card stack" data-variant="flat">
<h3>Check at mobile viewport</h3>
<div class="code-block" data-wrap="true">uv run --with pillow python scripts/check_birch_renderings.py --artifact eval-runs/my-run/my-artifact.html --viewport mobile:390x900</div>
</div>
<div class="card stack" data-variant="flat">
<h3>Compare reference vs. candidate</h3>
<div class="code-block" data-wrap="true">uv run --with pillow python scripts/check_birch_renderings.py --pair 05-design-system.html:05-design-system-birch.html</div>
</div>
<div class="card stack" data-variant="flat">
<h3>Run full chart eval</h3>
<div class="code-block" data-wrap="true">uv run python evals/charts/run_eval.py</div>
</div>
<div class="card stack" data-variant="flat">
<h3>Rebuild chart artifact, then check</h3>
<div class="code-block" data-wrap="true">uv run --with matplotlib python evals/charts/build_chart_brief.py &amp;&amp; uv run --with pillow python scripts/check_birch_renderings.py --artifact 24-birch-chart-brief.html</div>
</div>
<div class="card stack" data-variant="flat">
<h3>Fail on warnings too</h3>
<div class="code-block" data-wrap="true">uv run --with pillow python scripts/check_birch_renderings.py --artifact eval-runs/my-run/my-artifact.html --fail-on-warn</div>
</div>
</div>
<h3>Report outputs</h3>
<div class="numeric-table-wrap">
<table class="numeric-table">
<thead>
<tr>
<th>File</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>reports/birch-rendering-check.json</code></td>
<td>Full structured payload: mode, per-artifact stats, screenshot metrics, geometry audit data, and the findings array with level/name/evidence.</td>
</tr>
<tr>
<td><code>reports/birch-rendering-check.md</code></td>
<td>Human-readable Markdown table of findings. Printed to stdout on completion.</td>
</tr>
<tr>
<td><code>reports/birch-screenshots/&lt;stem&gt;-&lt;viewport&gt;.png</code></td>
<td>PNG screenshot per artifact per viewport. Only written when Chrome is available.</td>
</tr>
<tr>
<td><code>reports/birch-screenshots/&lt;ref&gt;__vs__&lt;cand&gt;-diff.png</code></td>
<td>Amplified pixel-difference image (pair mode only). Brightness ×4 applied via Pillow.</td>
</tr>
<tr>
<td><code>reports/birch-screenshots/&lt;ref&gt;__vs__&lt;cand&gt;-contact.png</code></td>
<td>Contact sheet: source, candidate, and amplified diff side-by-side (pair mode only).</td>
</tr>
</tbody>
</table>
</div>
<div class="card stack" data-variant="flat">
<h3>Viewport spec format</h3>
<p>The <code>--viewport</code> flag takes the form <code>name:widthxheight</code>. The name is used only as a label in report and screenshot filenames. The word <code>deep</code> anywhere in the name triggers the second-pass height measurement. Examples from <code>evals/charts/run_eval.py</code>: <code>desktop:1365x900</code>, <code>mobile:390x900</code>, <code>deep:1365x2400</code>, <code>mobile-deep:390x3000</code>.</p>
</div>
</section>
</main>
</body>
</html>

Xet Storage Details

Size:
66.5 kB
·
Xet hash:
fc4bb9bc182a980b48f8da1b0a957ff0bf86dd25c6a05e7d9a83ea4c6696bf89

Xet efficiently stores files, intelligently splitting them into unique chunks and accelerating uploads and downloads. More info.