Layout tree (as in design/layout.json)
{
"pattern": "cta.dot-signal-tile",
"kind": "cta",
"slots": [
"title",
"body",
"newsletter",
"image"
],
"tree": {
"el": "box",
"bg": "accent",
"pad": [
12,
10
],
"justify": "between",
"radius": "md",
"children": [
{
"el": "grid",
"gap": 6,
"align": "center",
"children": [
{
"el": "box",
"span": "1-8",
"gap": 6,
"justify": "center",
"children": [
{
"el": "text",
"color": "onAccent",
"slot": "title",
"role": "h2",
"step": 6,
"weight": 700,
"leading": "tight",
"maxCh": 14
},
{
"el": "text",
"color": "onAccent",
"slot": "body",
"role": "body",
"maxCh": 40
},
{
"el": "form",
"slot": "newsletter",
"fields": [
"email"
],
"inline": true
}
]
},
{
"el": "media",
"span": "10-12",
"mobile": "hide",
"alignSelf": "center",
"slot": "image",
"treatment": "dot-matrix",
"frame": "circle"
}
]
}
]
}
}Component example: reference/cta.dot-signal-tile.html
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1">
<title>cta.dot-signal-tile — Dot Matrix</title>
<link rel="stylesheet" href="base.css">
<style>
section { padding: var(--section) 0; }
.widget.signal { padding: 48px 40px; display: grid; grid-template-columns: repeat(12, 1fr); gap: 24px; align-items: center; }
.copy { grid-column: 1 / 9; display: grid; gap: 24px; }
.copy h2 { font-size: var(--step-6); max-width: 14ch; }
form { display: flex; gap: 8px; max-width: 520px; }
input { flex: 1; min-width: 0; height: 52px; padding: 0 20px; border-radius: 999px; border: 1px solid color-mix(in srgb, var(--on-accent) 55%, transparent);
background: color-mix(in srgb, #000 12%, var(--accent)); color: var(--on-accent); font: inherit; }
input::placeholder { color: color-mix(in srgb, var(--on-accent) 80%, transparent); }
.btn.white { background: var(--on-accent); color: var(--accent); }
.pic { grid-column: 10 / 13; }
@media (max-width: 640px) { .widget.signal { padding: 32px 20px; } .copy h2 { font-size: 44px; } form { flex-direction: column; } }
</style>
</head>
<body>
<section class="wrap">
<div class="widget signal">
<div class="copy">
<h2>Be first in the queue</h2>
<p>One email when the next drop opens. Nothing else.</p>
<form><label class="mono" for="e" hidden>Email</label><input id="e" type="email" placeholder="you@example.com"><button class="btn white" type="submit">Notify me</button></form>
</div>
<div class="pic hide-phone"><svg class="halftone round" viewBox="0 0 280 280" role="img" aria-label="Halftone of the phone"></svg></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>