diff --git a/ui_kits/web/App.jsx b/ui_kits/web/App.jsx
index 6eb3a4c..d1ab5e8 100644
--- a/ui_kits/web/App.jsx
+++ b/ui_kits/web/App.jsx
@@ -2,7 +2,7 @@
function App() {
const [entity, setEntity] = React.useState("all");
- const [screen, setScreen] = React.useState("review");
+ const [screen, setScreen] = React.useState("dashboard");
const [query, setQuery] = React.useState("");
const [sort, setSort] = React.useState("date-desc");
const [transactions, setTransactions] = React.useState(window.AKEFIN_DATA.transactions);
@@ -86,6 +86,7 @@ function App() {
case "switch-personal": setEntity("personal"); break;
case "switch-9tfox": setEntity("tfox"); break;
case "switch-finacode": setEntity("finacode"); break;
+ case "go-dashboard": setScreen("dashboard"); break;
case "go-review": setScreen("review"); break;
case "go-rules": setScreen("rules"); break;
case "go-ledger": setScreen("ledger"); break;
@@ -128,6 +129,7 @@ function App() {
+ {screen === "dashboard" && }
{screen === "review" && (
{
+ // Pseudo-random but stable. Mix of small-spend days, larger ones, and one big income.
+ const seedSpend = [42, 86, 31, 124, 58, 73, 18, 96, 240, 64, 47, 39, 28, 71, 110, 56, 33, 88, 49, 62, 38, 145, 72, 80, 41, 53, 117, 64, 91, 38]; // ×1000 KRW
+ const seedIncomeIdx = { 4: 2400, 12: 1240, 19: 1180, 27: 2400 }; // ×1000 KRW positive on these indexes
+ return seedSpend.map((s, i) => ({
+ day: i + 1,
+ spend: -s * 1000,
+ income: (seedIncomeIdx[i] || 0) * 1000,
+ }));
+ })();
+
+ // Spend by category (current month, KRW)
+ const CATEGORIES = [
+ { name: "Groceries", amount: 481600, entity: "personal" },
+ { name: "Food · Delivery", amount: 224000, entity: "personal" },
+ { name: "Housing · Maint", amount: 286000, entity: "personal" },
+ { name: "Food · Coffee", amount: 84600, entity: "personal" },
+ { name: "Convenience", amount: 62400, entity: "personal" },
+ { name: "Software", amount: 96000, entity: "tfox" },
+ { name: "Infrastructure", amount: 42800, entity: "tfox" },
+ { name: "Transit", amount: 38000, entity: "personal" },
+ { name: "Clothing", amount: 28400, entity: "personal" },
+ ];
+
+ // Confidence distribution (count of transactions currently staged across history)
+ const CONFIDENCE_DIST = [
+ { tier: "rules", count: 287, label: "Rules" },
+ { tier: "high", count: 124, label: "LLM ≥ 0.85"},
+ { tier: "mid", count: 46, label: "0.70–0.85" },
+ { tier: "low", count: 18, label: "< 0.70" },
+ ];
+
+ // Sparkline series for KPI cards (last 14 points)
+ const SPARK = {
+ staged: [12, 18, 21, 16, 24, 29, 22, 25, 31, 27, 23, 28, 19, 23],
+ spend: [128, 142, 119, 156, 138, 161, 149, 152, 174, 168, 145, 159, 138, 142], // ×1000 KRW
+ income: [0, 0, 2400, 0, 0, 1240, 0, 0, 0, 2400, 0, 1180, 0, 0],
+ conf: [0.81, 0.83, 0.79, 0.84, 0.86, 0.85, 0.87, 0.88, 0.86, 0.89, 0.87, 0.88, 0.90, 0.89],
+ };
+
+ // ---- Pure SVG components ----------------------------------------------
+ function Sparkline({ data, color = "var(--fg-muted)", height = 28, width = 96, fill = false }) {
+ if (!data || data.length === 0) return null;
+ const min = Math.min(...data, 0);
+ const max = Math.max(...data, 0.0001);
+ const span = max - min || 1;
+ const stepX = width / (data.length - 1);
+ const points = data.map((v, i) => [i * stepX, height - ((v - min) / span) * (height - 2) - 1]);
+ const d = points.map((p, i) => (i === 0 ? `M${p[0]},${p[1]}` : `L${p[0]},${p[1]}`)).join(" ");
+ const dFill = `${d} L${width},${height} L0,${height} Z`;
+ return (
+
+ );
+ }
+
+ function StatCard({ eyebrow, value, unit, delta, deltaDir, spark, sparkColor, footnote }) {
+ return (
+
+
{eyebrow}
+
+
+ {value}
+ {unit && {unit}}
+
+ {spark &&
}
+
+
+ {delta && (
+
+ {deltaDir === "up" ? "↑" : deltaDir === "down" ? "↓" : "·"}
+ {delta}
+
+ )}
+ {footnote && {footnote}}
+
+
+ );
+ }
+
+ // Cashflow bar chart — bars per day, income up (green) / spend down (ink), 30 days
+ function CashflowChart({ data }) {
+ const W = 720, H = 220, PAD_L = 44, PAD_R = 12, PAD_T = 16, PAD_B = 28;
+ const plotW = W - PAD_L - PAD_R;
+ const plotH = H - PAD_T - PAD_B;
+ const maxPos = Math.max(...data.map(d => d.income), 1);
+ const maxNeg = Math.max(...data.map(d => -d.spend), 1);
+ const maxAbs = Math.max(maxPos, maxNeg);
+ const zeroY = PAD_T + plotH / 2;
+ const half = plotH / 2;
+ const colW = plotW / data.length;
+ const barW = Math.max(3, colW * 0.55);
+
+ const yScale = (v) => zeroY - (v / maxAbs) * half;
+
+ // Y-axis ticks: 0, ±50%, ±100%
+ const ticks = [-1, -0.5, 0, 0.5, 1];
+ const fmtTick = (frac) => {
+ if (frac === 0) return "0";
+ const v = Math.round(frac * maxAbs / 1000);
+ return `${frac > 0 ? "+" : "−"}${Math.abs(v).toLocaleString()}k`;
+ };
+
+ return (
+
+ );
+ }
+
+ // Donut for confidence distribution
+ function Donut({ data, size = 168, thickness = 22 }) {
+ const total = data.reduce((a, b) => a + b.count, 0);
+ const r = (size - thickness) / 2;
+ const cx = size / 2, cy = size / 2;
+ const colors = {
+ rules: "var(--conf-rules)",
+ high: "var(--conf-high)",
+ mid: "var(--conf-mid)",
+ low: "var(--conf-low)",
+ };
+ let cum = 0;
+ const segs = data.map(d => {
+ const start = cum / total;
+ cum += d.count;
+ const end = cum / total;
+ return { ...d, start, end };
+ });
+ const arc = (s, e) => {
+ const a0 = s * Math.PI * 2 - Math.PI / 2;
+ const a1 = e * Math.PI * 2 - Math.PI / 2;
+ const x0 = cx + r * Math.cos(a0), y0 = cy + r * Math.sin(a0);
+ const x1 = cx + r * Math.cos(a1), y1 = cy + r * Math.sin(a1);
+ const large = e - s > 0.5 ? 1 : 0;
+ return `M${x0},${y0} A${r},${r} 0 ${large} 1 ${x1},${y1}`;
+ };
+ return (
+
+ );
+ }
+
+ // ---- Sub-cards --------------------------------------------------------
+ function CashflowCard() {
+ const totalIn = CASHFLOW.reduce((a, b) => a + b.income, 0);
+ const totalOut = CASHFLOW.reduce((a, b) => a + Math.abs(b.spend), 0);
+ const net = totalIn - totalOut;
+ return (
+
+
+
+
CASHFLOW · LAST 30 DAYS
+
Net = 0 ? "pos" : "neg"}>
+ {net >= 0 ? "+ " : "− "}{Math.abs(net).toLocaleString()}
+ KRW
+
+
+
+
+ INCOME
+
+ SPEND
+
+
+
+
+
+
+
+ INCOME
+ + {totalIn.toLocaleString()} KRW
+
+
+ SPEND
+ − {totalOut.toLocaleString()} KRW
+
+
+ AVG. CONF.
+ 0.87
+
+
+ RUNS
+ 12
+
+
+
+ );
+ }
+
+ function ConfidenceCard() {
+ return (
+
+
+
+
CATEGORIZATION · BY TIER
+
Confidence mix
+
+
+
+
+
+ {CONFIDENCE_DIST.map(d => {
+ const total = CONFIDENCE_DIST.reduce((a, b) => a + b.count, 0);
+ const pct = ((d.count / total) * 100).toFixed(0);
+ return (
+
+ {pct}%
+ {d.label}
+ {d.count}
+
+ );
+ })}
+
+
+
+
+ AUTO-POSTED
+ 86%
+
+
+ NEED REVIEW
+ 64
+
+
+
+ );
+ }
+
+ function CategoryCard() {
+ const max = Math.max(...CATEGORIES.map(c => c.amount));
+ const total = CATEGORIES.reduce((a, b) => a + b.amount, 0);
+ const entityColor = {
+ personal: "var(--entity-personal)",
+ tfox: "var(--entity-9tfox)",
+ finacode: "var(--entity-finacode)",
+ };
+ return (
+
+
+
+
SPEND · BY CATEGORY · MAR 2026
+
+ − {total.toLocaleString()} KRW
+
+
+
+
+
+ {CATEGORIES.map((c, i) => {
+ const w = (c.amount / max) * 100;
+ const pct = ((c.amount / total) * 100).toFixed(1);
+ return (
+
+
{c.name}
+
+
+ − {c.amount.toLocaleString()}
+ KRW
+
+
{pct}%
+
+ );
+ })}
+
+
+ );
+ }
+
+ function EntitySplitCard() {
+ const entities = [
+ { id: "personal", label: "Personal", income: 0, spend: 1205000, txCount: 38, color: "var(--entity-personal)" },
+ { id: "tfox", label: "9TFox", income: 4800000, spend: 138800, txCount: 17, color: "var(--entity-9tfox)" },
+ { id: "finacode", label: "Finacode", income: 1810000, spend: 0, txCount: 3, color: "var(--entity-finacode)" },
+ ];
+ const total = entities.reduce((a, b) => a + b.income + b.spend, 0);
+ return (
+
+
+
+
ENTITY · ACTIVITY MIX
+
3 entities · 58 transactions
+
+
+ {/* Stacked bar across entities */}
+
+ {entities.map(e => {
+ const w = ((e.income + e.spend) / total) * 100;
+ return (
+
+ {e.label}
+
+ );
+ })}
+
+
+
+
+ INCOME
+ SPEND
+ NET
+ TX
+
+ {entities.map(e => (
+
+ {e.label}
+ {e.income ? `+ ${e.income.toLocaleString()}` : "—"} KRW
+ {e.spend ? `− ${e.spend.toLocaleString()}` : "—"} KRW
+ = 0 ? "pos" : ""}`}>
+ {e.income - e.spend >= 0 ? "+ " : "− "}
+ {Math.abs(e.income - e.spend).toLocaleString()}
+ KRW
+
+ {e.txCount}
+
+ ))}
+
+
+ );
+ }
+
+ function PipelineStatusCard() {
+ const lines = [
+ { t: "09:14", src: "Toss · 신한 입출금", rows: 247, msg: "153 auto · 44 high · 35 review · 15 failed", status: "warn" },
+ { t: "08:02", src: "Kakao Bank · 9TFox", rows: 62, msg: "58 auto · 3 high · 1 review", status: "ok" },
+ { t: "07:48", src: "Wise · 9TFox", rows: 14, msg: "12 auto · 2 high", status: "ok" },
+ { t: "06:01", src: "Garanti BBVA", rows: 89, msg: "61 auto · 14 high · 11 review · 3 failed", status: "warn" },
+ ];
+ return (
+
+
+
+
PIPELINE · LAST 24 HOURS
+
4 imports · 412 rows · 50 staged
+
+
+
+
+ {lines.map((l, i) => (
+
+
+ {l.t}
+ {l.src}
+ {l.rows} rows
+ {l.msg}
+
+ ))}
+
+
+ );
+ }
+
+ function DashboardScreen({ entity, onJump }) {
+ return (
+
+
+
+
OVERVIEW · MAR 2026 · {entity === "all" ? "ALL ENTITIES" : entity.toUpperCase()}
+
Ledger status
+
8 days into the period · 58 transactions · 64 staged for review · ledger clean on main
+
+
+
+
+
+
+
+ {/* KPI row */}
+
+
+
+
+
+
+
+ {/* Main 2-col grid */}
+
+
+
+
+
+ {/* Secondary 2-col grid */}
+
+
+
+
+
+ {/* Pipeline */}
+
+
+ );
+ }
+
+ Object.assign(window, { DashboardScreen });
+})();
diff --git a/ui_kits/web/dashboard.css b/ui_kits/web/dashboard.css
index 03f9dd3..a501fa1 100644
--- a/ui_kits/web/dashboard.css
+++ b/ui_kits/web/dashboard.css
@@ -1575,3 +1575,397 @@ button { font-family: inherit; cursor: pointer; }
background: var(--paper);
}
.entity-radio-item:hover { background: var(--paper); }
+
+/* =========================================================================
+ DASHBOARD / OVERVIEW SCREEN
+ ========================================================================= */
+.dashboard-screen {
+ padding: 24px;
+ overflow-y: auto;
+ height: 100%;
+}
+.screen-eyebrow {
+ font-family: var(--font-mono);
+ font-size: 10px;
+ font-weight: 500;
+ letter-spacing: 0.16em;
+ color: var(--fg-subtle);
+ text-transform: uppercase;
+ margin-bottom: 6px;
+}
+
+/* ---- KPI row -------------------------------------------------------- */
+.dash-stat-grid {
+ display: grid;
+ grid-template-columns: repeat(4, 1fr);
+ gap: 12px;
+ margin-bottom: 16px;
+}
+.dash-stat {
+ background: var(--paper);
+ border: 1px solid var(--rule);
+ border-radius: var(--r-md);
+ padding: 14px 16px 12px;
+ display: flex; flex-direction: column;
+ gap: 10px;
+ min-height: 116px;
+}
+.dash-stat-eyebrow {
+ font-family: var(--font-mono);
+ font-size: 9.5px;
+ font-weight: 500;
+ letter-spacing: 0.16em;
+ color: var(--fg-subtle);
+ text-transform: uppercase;
+}
+.dash-stat-value-row {
+ display: flex; align-items: flex-end; justify-content: space-between;
+ gap: 8px;
+}
+.dash-stat-value {
+ font-family: var(--font-mono);
+ font-size: 26px;
+ font-weight: 500;
+ font-variant-numeric: tabular-nums;
+ color: var(--fg-strong);
+ line-height: 1;
+ letter-spacing: -0.01em;
+}
+.dash-stat-unit {
+ font-family: var(--font-mono);
+ font-size: 12px;
+ font-weight: 400;
+ color: var(--fg-subtle);
+ margin-left: 4px;
+ letter-spacing: 0.04em;
+}
+.dash-stat-foot {
+ display: flex; align-items: center; gap: 10px;
+ font-family: var(--font-mono);
+ font-size: 10.5px;
+ letter-spacing: 0.04em;
+ color: var(--fg-muted);
+ margin-top: auto;
+ padding-top: 4px;
+ border-top: 1px dashed var(--rule);
+}
+.dash-delta {
+ display: inline-flex; align-items: center; gap: 3px;
+ font-variant-numeric: tabular-nums;
+}
+.dash-delta.up { color: var(--conf-rules); }
+.dash-delta.down { color: var(--conf-low); }
+.dash-delta.flat { color: var(--fg-muted); }
+.dash-delta .arrow { font-weight: 600; }
+.dash-stat-foot-note {
+ color: var(--fg-subtle);
+ margin-left: auto;
+ font-size: 10px;
+ letter-spacing: 0.04em;
+}
+
+.spark { display: block; }
+
+/* ---- Generic dash card --------------------------------------------- */
+.dash-card {
+ background: var(--paper);
+ border: 1px solid var(--rule);
+ border-radius: var(--r-md);
+ padding: 16px 20px;
+ display: flex; flex-direction: column;
+ gap: 14px;
+}
+.dash-card-tall { min-height: 360px; }
+
+.dash-card-head {
+ display: flex; align-items: flex-start; justify-content: space-between;
+ gap: 16px;
+}
+.dash-card-eyebrow {
+ font-family: var(--font-mono);
+ font-size: 9.5px;
+ font-weight: 500;
+ letter-spacing: 0.16em;
+ color: var(--fg-subtle);
+ text-transform: uppercase;
+ margin-bottom: 4px;
+}
+.dash-card-title {
+ font-family: var(--font-sans);
+ font-size: 18px;
+ font-weight: 500;
+ color: var(--fg-strong);
+ letter-spacing: -0.005em;
+}
+.dash-card-title .pos { color: var(--conf-rules); font-family: var(--font-mono); font-variant-numeric: tabular-nums; }
+.dash-card-title .neg { color: var(--fg-strong); font-family: var(--font-mono); font-variant-numeric: tabular-nums; }
+.dash-card-title .ccy { color: var(--fg-subtle); font-weight: 400; font-size: 12px; }
+.dash-card-title .ccy:not(:first-child) { margin-left: 2px; }
+.dash-card-legend {
+ display: flex; align-items: center; gap: 6px;
+ font-family: var(--font-mono);
+ font-size: 9.5px;
+ letter-spacing: 0.16em;
+ color: var(--fg-subtle);
+}
+.dash-card-legend .lg-dot {
+ display: inline-block;
+ width: 8px; height: 8px;
+ border-radius: 1px;
+ margin-right: 2px;
+}
+.dash-card-action {
+ background: transparent;
+ border: none;
+ padding: 0;
+ font-family: var(--font-mono);
+ font-size: 10px;
+ letter-spacing: 0.14em;
+ color: var(--fg-muted);
+}
+.dash-card-action:hover { color: var(--fg-strong); }
+
+/* ---- Cashflow chart ------------------------------------------------- */
+.dash-chart-wrap {
+ width: 100%;
+ flex: 1;
+ min-height: 200px;
+}
+.chart-svg {
+ width: 100%;
+ height: 240px;
+ display: block;
+ overflow: visible;
+}
+.axis-label {
+ font-family: var(--font-mono);
+ font-size: 9.5px;
+ fill: var(--fg-subtle);
+ letter-spacing: 0.06em;
+}
+
+.dash-card-foot {
+ display: grid;
+ grid-template-columns: repeat(4, 1fr);
+ gap: 12px;
+ padding-top: 12px;
+ border-top: 1px solid var(--rule);
+}
+.dash-foot-cell { display: flex; flex-direction: column; gap: 4px; }
+.dash-foot-lbl {
+ font-family: var(--font-mono);
+ font-size: 9.5px;
+ letter-spacing: 0.16em;
+ color: var(--fg-subtle);
+}
+.dash-foot-val {
+ font-family: var(--font-mono);
+ font-size: 14px;
+ font-weight: 500;
+ font-variant-numeric: tabular-nums;
+ color: var(--fg-strong);
+}
+.dash-foot-val.pos { color: var(--conf-rules); }
+.dash-foot-val .ccy { color: var(--fg-subtle); font-weight: 400; font-size: 11px; margin-left: 2px; }
+
+/* ---- Donut ---------------------------------------------------------- */
+.dash-donut-wrap {
+ display: flex; align-items: center; gap: 24px;
+ flex: 1;
+ padding: 8px 0;
+}
+.donut { flex-shrink: 0; }
+.donut-num {
+ font-family: var(--font-mono);
+ font-size: 24px;
+ font-weight: 500;
+ fill: var(--fg-strong);
+ font-variant-numeric: tabular-nums;
+}
+.donut-lbl {
+ font-family: var(--font-mono);
+ font-size: 8px;
+ letter-spacing: 0.18em;
+ fill: var(--fg-subtle);
+}
+.dash-donut-legend {
+ flex: 1;
+ display: flex; flex-direction: column;
+ gap: 8px;
+}
+.dash-legend-row {
+ display: grid;
+ grid-template-columns: 56px 1fr auto;
+ align-items: center;
+ gap: 12px;
+ padding: 4px 0;
+ border-bottom: 1px dashed var(--rule);
+}
+.dash-legend-row:last-child { border-bottom: none; }
+.dash-legend-lbl {
+ font-family: var(--font-sans);
+ font-size: 12.5px;
+ color: var(--fg);
+}
+.dash-legend-num {
+ font-family: var(--font-mono);
+ font-size: 13px;
+ font-weight: 500;
+ font-variant-numeric: tabular-nums;
+ color: var(--fg-strong);
+}
+
+/* ---- Two-column grids ---------------------------------------------- */
+.dash-grid-main {
+ display: grid;
+ grid-template-columns: minmax(0, 1.55fr) minmax(0, 1fr);
+ gap: 12px;
+ margin-bottom: 16px;
+}
+.dash-grid-secondary {
+ display: grid;
+ grid-template-columns: minmax(0, 1.1fr) minmax(0, 1fr);
+ gap: 12px;
+ margin-bottom: 16px;
+}
+
+/* ---- Category bars -------------------------------------------------- */
+.dash-bars {
+ display: flex; flex-direction: column;
+ gap: 2px;
+}
+.dash-bar-row {
+ display: grid;
+ grid-template-columns: 140px 1fr 140px 44px;
+ align-items: center;
+ gap: 12px;
+ padding: 7px 0;
+ border-bottom: 1px solid var(--rule);
+}
+.dash-bar-row:last-child { border-bottom: none; }
+.dash-bar-lbl {
+ font-family: var(--font-sans);
+ font-size: 12.5px;
+ color: var(--fg);
+}
+.dash-bar-track {
+ position: relative;
+ height: 8px;
+ background: var(--bg);
+ border: 1px solid var(--rule);
+ border-radius: 1px;
+ overflow: hidden;
+}
+.dash-bar-fill {
+ height: 100%;
+ background: var(--fg-strong);
+ transition: width var(--dur-slow) var(--ease);
+}
+.dash-bar-amt {
+ font-family: var(--font-mono);
+ font-size: 12px;
+ font-variant-numeric: tabular-nums;
+ color: var(--fg);
+ text-align: right;
+}
+.dash-bar-amt .ccy { color: var(--fg-subtle); }
+.dash-bar-pct {
+ font-family: var(--font-mono);
+ font-size: 11px;
+ font-variant-numeric: tabular-nums;
+ color: var(--fg-subtle);
+ text-align: right;
+ letter-spacing: 0.04em;
+}
+
+/* ---- Entity stacked --------------------------------------------- */
+.dash-stacked {
+ display: flex;
+ height: 28px;
+ width: 100%;
+ border: 1px solid var(--rule);
+ border-radius: var(--r-sm);
+ overflow: hidden;
+}
+.dash-stacked-seg {
+ display: flex; align-items: center; justify-content: center;
+ font-family: var(--font-mono);
+ font-size: 10px;
+ font-weight: 500;
+ letter-spacing: 0.14em;
+ color: var(--bg);
+ text-transform: uppercase;
+ white-space: nowrap;
+ overflow: hidden;
+ border-right: 1px solid rgba(255,255,255,0.18);
+}
+.dash-stacked-seg:last-child { border-right: none; }
+.dash-stacked-lbl { padding: 0 8px; }
+
+.dash-entity-table {
+ display: flex; flex-direction: column;
+ gap: 0;
+ margin-top: 4px;
+}
+.dash-entity-head,
+.dash-entity-row {
+ display: grid;
+ grid-template-columns: 110px 1fr 1fr 1fr 50px;
+ gap: 12px;
+ align-items: center;
+ padding: 8px 0;
+}
+.dash-entity-head {
+ font-family: var(--font-mono);
+ font-size: 9.5px;
+ letter-spacing: 0.16em;
+ color: var(--fg-subtle);
+ border-bottom: 1px solid var(--rule);
+ padding-bottom: 6px;
+}
+.dash-entity-head > span:not(:first-child) { text-align: right; }
+.dash-entity-row {
+ border-bottom: 1px solid var(--rule);
+}
+.dash-entity-row:last-child { border-bottom: none; }
+.dash-entity-row .amount { text-align: right; }
+.dash-entity-row .amount.pos { color: var(--conf-rules); }
+.dash-tx-count {
+ font-family: var(--font-mono);
+ font-size: 13px;
+ font-variant-numeric: tabular-nums;
+ text-align: right;
+ color: var(--fg-strong);
+}
+
+/* ---- Pipeline log card --------------------------------------------- */
+.dash-log {
+ background: var(--surface);
+ border: 1px solid var(--rule-ink);
+ border-radius: var(--r-sm);
+ font-family: var(--font-mono);
+ font-size: 12px;
+ color: var(--fg-on-ink);
+ padding: 4px 0;
+}
+.dash-log-row {
+ display: grid;
+ grid-template-columns: 14px 56px 220px 84px 1fr;
+ align-items: center;
+ gap: 12px;
+ padding: 8px 16px;
+ border-bottom: 1px dashed var(--rule-ink);
+}
+.dash-log-row:last-child { border-bottom: none; }
+.dash-log-dot {
+ width: 7px; height: 7px;
+ border-radius: 50%;
+}
+.dash-log-dot.dash-ok { background: var(--conf-rules); }
+.dash-log-dot.dash-warn { background: var(--conf-mid); }
+.dash-log-dot.dash-fail { background: var(--conf-low); }
+.dash-log-time { color: var(--fg-on-ink-muted); letter-spacing: 0.06em; }
+.dash-log-src { color: var(--fg-on-ink); font-family: var(--font-sans-kr); }
+.dash-log-rows { color: var(--fg-on-ink-muted); font-variant-numeric: tabular-nums; }
+.dash-log-msg { color: var(--fg-on-ink-muted); }
+
diff --git a/ui_kits/web/index.html b/ui_kits/web/index.html
index 1df8286..644f719 100644
--- a/ui_kits/web/index.html
+++ b/ui_kits/web/index.html
@@ -24,6 +24,7 @@
+