Layout tree (as in design/layout.json)
{
"pattern": "features.dot-widget-board",
"kind": "features",
"grid": {
"intro": "1-4",
"picture": "5-8",
"widgets": "9-12"
},
"slots": [
"sectionLabel",
"title",
"body",
"cta",
"image",
"features[4..4]"
],
"tree": {
"el": "grid",
"gap": 3,
"rowGap": 3,
"align": "stretch",
"children": [
{
"el": "box",
"span": "1-4",
"rowSpan": "1-2",
"bg": "surface",
"gap": 6,
"pad": [
8,
6
],
"justify": "between",
"radius": "md",
"children": [
{
"el": "box",
"alignSelf": "start",
"dir": "row",
"pad": [
1,
4
],
"align": "center",
"border": "all",
"radius": "full",
"children": [
{
"el": "text",
"color": "text",
"slot": "sectionLabel",
"role": "label",
"font": "mono"
}
]
},
{
"el": "text",
"slot": "title",
"role": "h2",
"step": 5,
"weight": 700,
"leading": "tight",
"maxCh": 14
},
{
"el": "text",
"color": "textMuted",
"slot": "body",
"role": "body",
"maxCh": 36
},
{
"el": "button",
"slot": "cta",
"variant": "secondary",
"arrow": true
}
]
},
{
"el": "media",
"span": "5-8",
"rowSpan": "1-2",
"slot": "image",
"aspect": "4/5",
"treatment": "dot-matrix",
"fill": true,
"frame": "rounded"
},
{
"el": "repeat",
"span": "9-12",
"rowSpan": "1-2",
"slot": "features",
"min": 4,
"max": 4,
"layout": "grid",
"cols": 2,
"phoneCols": 1,
"gap": 3,
"of": {
"el": "box",
"bg": "surfaceAlt",
"gap": 8,
"pad": [
5,
5
],
"justify": "between",
"radius": "md",
"children": [
{
"el": "text",
"color": "link",
"slot": "number",
"role": "label",
"font": "mono"
},
{
"el": "box",
"gap": 1,
"children": [
{
"el": "text",
"slot": "title",
"role": "h4",
"weight": 700,
"font": "body"
},
{
"el": "text",
"color": "textMuted",
"slot": "body",
"role": "small",
"maxCh": 26
}
]
}
]
}
}
]
}
}Component example: reference/features.dot-widget-board.html
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1">
<title>features.dot-widget-board — Dot Matrix</title>
<link rel="stylesheet" href="base.css">
<style>
section { padding: var(--section) 0; }
.intro { grid-column: 1 / 5; grid-row: 1 / 3; padding: 32px 24px; }
.intro h2 { font-size: var(--step-5); max-width: 14ch; }
.pic { grid-column: 5 / 9; grid-row: 1 / 3; border-radius: var(--radius); overflow: hidden; }
.pic svg { height: 100%; }
.widgets { grid-column: 9 / 13; grid-row: 1 / 3; display: grid; grid-template-columns: 1fr 1fr; gap: var(--gap); }
.widgets .widget { padding: 20px; gap: 32px; }
@media (max-width: 640px) { .widgets { grid-template-columns: 1fr; } }
</style>
</head>
<body>
<section class="wrap grid">
<div class="widget intro">
<span class="chip">Why it works</span>
<h2>Less screen, more signal</h2>
<p class="muted">Every light on the back means one thing. Calls, timers and charge glow in dots, not notifications.</p>
<a class="btn secondary sm" href="#">All features →</a>
</div>
<div class="pic"><svg class="halftone" viewBox="0 0 280 280" preserveAspectRatio="xMidYMid slice" role="img" aria-label="Halftone of the phone back"></svg></div>
<div class="widgets">
<div class="widget alt"><span class="mono idx">01</span><div><p class="item-title">Glyph timer</p><p class="small muted">Counts down in light.</p></div></div>
<div class="widget alt"><span class="mono idx">02</span><div><p class="item-title">Charge meter</p><p class="small muted">See percent at a glance.</p></div></div>
<div class="widget alt"><span class="mono idx">03</span><div><p class="item-title">Quiet calls</p><p class="small muted">A pattern per contact.</p></div></div>
<div class="widget alt"><span class="mono idx">04</span><div><p class="item-title">Flip to dot</p><p class="small muted">Face down, focus on.</p></div></div>
</div>
</section>
<script>
/* Halftone: a 28-dot square grid; each dot radius follows the tone of a lit sphere; a small moon in the signal red. */
for (const svg of document.querySelectorAll("svg.halftone")) {
const n = 28, c = 10, out = [];
for (let i = 0; i < n; i++) for (let j = 0; j < n; j++) {
const x = (i + .5) * c, y = (j + .5) * c, dx = (x - 130) / 95, dy = (y - 150) / 95, d2 = dx * dx + dy * dy;
let tone = d2 <= 1 ? .18 + .82 * (1 - Math.max(0, -.55 * dx - .6 * dy + .58 * Math.sqrt(1 - d2))) : .06;
const mx = (x - 225) / 26, my = (y - 70) / 26, sig = mx * mx + my * my <= 1; if (sig) tone = .85;
const r = Math.min(.48, tone * .5) * c; if (r < c * .05) continue;
out.push(`<circle class="${sig ? "sig" : "ink"}" cx="${x}" cy="${y}" r="${r.toFixed(2)}"/>`);
}
svg.innerHTML = out.join("");
}
</script>
</body>
</html>