Comics — Narration Caption Box
A letterer's toolkit for comic narration captions, built in the classic sequential-art tradition with thick ink borders, halftone dot textures, and signature tilts. It pairs a live sample panel with an editor that lets you type caption copy and pick a style — the yellow Meanwhile box, a location and time stamp, a first-person narration plate, or a chapter-title banner — then pin it to any corner of the panel. A reference strip shows all four variants, and a copy-HTML action and surprise-me presets round out the demo.
MCP
Código
:root {
--ink: #0e0e12;
--ink-2: #23232b;
--paper: #fdfcf7;
--panel: #ffffff;
--accent: #ff2e4d;
--accent-2: #ffd23f;
--accent-blue: #2e6bff;
--muted: #6b6b78;
--line: rgba(14, 14, 18, 0.14);
--line-2: rgba(14, 14, 18, 0.28);
--halftone: radial-gradient(circle, rgba(14, 14, 18, 0.18) 1px, transparent 1.6px);
--r-sm: 6px;
--r-md: 12px;
--r-lg: 18px;
--shadow-ink: 6px 6px 0 var(--ink);
--shadow-soft: 4px 4px 0 var(--ink);
}
*,
*::before,
*::after {
box-sizing: border-box;
}
html {
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
body {
margin: 0;
font-family: "Inter", system-ui, sans-serif;
line-height: 1.5;
color: var(--ink);
background-color: var(--paper);
background-image: var(--halftone);
background-size: 22px 22px;
}
.page {
max-width: 1080px;
margin: 0 auto;
padding: 32px 20px 64px;
}
/* ---------- Masthead ---------- */
.masthead {
text-align: center;
margin-bottom: 28px;
}
.kicker {
display: inline-block;
margin: 0 0 10px;
padding: 4px 12px;
font-size: 0.72rem;
font-weight: 800;
letter-spacing: 0.12em;
text-transform: uppercase;
color: var(--paper);
background: var(--ink);
border-radius: 999px;
}
.title {
font-family: "Bangers", system-ui, sans-serif;
font-weight: 400;
font-size: clamp(2.6rem, 9vw, 5rem);
line-height: 0.9;
letter-spacing: 0.02em;
margin: 0 0 14px;
text-transform: uppercase;
color: var(--ink);
text-shadow: 4px 4px 0 var(--accent-2);
}
.title span {
color: var(--accent);
text-shadow: 4px 4px 0 var(--ink);
}
.lede {
max-width: 60ch;
margin: 0 auto;
color: var(--ink-2);
font-size: 1rem;
}
.lede em {
font-style: italic;
font-weight: 600;
}
/* ---------- Layout ---------- */
.layout {
display: grid;
grid-template-columns: 1.4fr 1fr;
gap: 24px;
align-items: start;
}
/* ---------- Stage / live panel ---------- */
.stage {
background: var(--panel);
border: 3px solid var(--ink);
border-radius: var(--r-lg);
box-shadow: var(--shadow-ink);
padding: 18px;
}
.stage-head {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 14px;
}
.stage-head h2,
.editor h2 {
font-family: "Bangers", system-ui, sans-serif;
font-weight: 400;
font-size: 1.5rem;
letter-spacing: 0.04em;
text-transform: uppercase;
margin: 0;
}
.badge {
font-size: 0.72rem;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.08em;
padding: 4px 10px;
border: 2px solid var(--ink);
border-radius: 999px;
background: var(--accent-2);
}
.panel {
position: relative;
margin: 0;
aspect-ratio: 4 / 3;
border: 3px solid var(--ink);
border-radius: var(--r-md);
overflow: hidden;
box-shadow: inset 0 0 0 3px var(--paper);
}
.panel-art {
position: absolute;
inset: 0;
}
.art-sky {
position: absolute;
inset: 0;
background: linear-gradient(180deg, #1b1142 0%, #3a1b6b 45%, #7a1f6b 78%, #ff5a7a 100%);
}
.art-skyline {
position: absolute;
inset: auto 0 0 0;
height: 42%;
background:
linear-gradient(90deg, var(--ink) 0 18%, transparent 18% 22%),
linear-gradient(90deg, transparent 0 28%, var(--ink) 28% 44%, transparent 44% 50%),
linear-gradient(90deg, transparent 0 56%, var(--ink) 56% 72%, transparent 72% 78%),
linear-gradient(90deg, transparent 0 82%, var(--ink) 82% 100%);
background-repeat: no-repeat;
-webkit-mask-image: linear-gradient(180deg, transparent, #000 35%);
mask-image: linear-gradient(180deg, transparent, #000 35%);
}
.art-moon {
position: absolute;
top: 14%;
right: 16%;
width: 84px;
height: 84px;
border-radius: 50%;
background: radial-gradient(circle at 38% 36%, #fff7d6, var(--accent-2));
box-shadow: 0 0 0 6px rgba(255, 210, 63, 0.22), 0 0 40px rgba(255, 210, 63, 0.5);
}
.art-hero {
position: absolute;
left: 50%;
bottom: 6%;
width: 46px;
height: 36%;
transform: translateX(-50%);
background: var(--ink);
clip-path: polygon(38% 0, 62% 0, 70% 22%, 60% 32%, 78% 60%, 64% 100%, 36% 100%, 22% 60%, 40% 32%, 30% 22%);
}
.halftone-overlay {
position: absolute;
inset: 0;
pointer-events: none;
background-image: var(--halftone);
background-size: 6px 6px;
mix-blend-mode: multiply;
opacity: 0.55;
}
/* ---------- Captions ---------- */
.caption {
position: absolute;
max-width: 62%;
padding: 8px 12px;
border: 2.5px solid var(--ink);
border-radius: var(--r-sm);
font-weight: 700;
font-size: 0.84rem;
line-height: 1.28;
box-shadow: var(--shadow-soft);
transition: transform 0.18s ease, box-shadow 0.18s ease, background 0.2s ease;
z-index: 3;
}
.caption.static {
position: absolute;
box-shadow: 3px 3px 0 var(--ink);
}
.caption-text {
display: block;
}
/* Corner pinning + signature comic tilt */
.cap-top-left {
top: 12px;
left: 12px;
transform: rotate(-2deg);
}
.cap-top-right {
top: 12px;
right: 12px;
transform: rotate(2deg);
}
.cap-bottom-left {
bottom: 12px;
left: 12px;
transform: rotate(2deg);
}
.cap-bottom-right {
bottom: 12px;
right: 12px;
transform: rotate(-2deg);
}
/* Style variants */
.cap-yellow {
background: var(--accent-2);
color: var(--ink);
text-transform: uppercase;
letter-spacing: 0.02em;
font-weight: 800;
}
.cap-location {
background: var(--ink);
color: var(--accent-2);
text-transform: uppercase;
letter-spacing: 0.1em;
font-weight: 800;
font-size: 0.78rem;
}
.cap-narration {
background: var(--paper);
color: var(--ink);
font-style: italic;
font-weight: 600;
text-transform: none;
letter-spacing: 0;
box-shadow: 3px 3px 0 var(--accent-blue);
}
.cap-chapter {
background: var(--accent);
color: var(--paper);
text-align: center;
font-family: "Bangers", system-ui, sans-serif;
font-weight: 400;
letter-spacing: 0.04em;
font-size: 1.3rem;
line-height: 1;
text-shadow: 2px 2px 0 var(--ink);
}
.cap-chapter small {
display: block;
font-family: "Inter", sans-serif;
font-size: 0.62rem;
font-weight: 800;
letter-spacing: 0.18em;
text-transform: uppercase;
color: var(--accent-2);
text-shadow: none;
margin-bottom: 2px;
}
.panel-meta {
display: flex;
flex-wrap: wrap;
gap: 8px;
align-items: center;
justify-content: center;
margin-top: 14px;
font-size: 0.72rem;
font-weight: 700;
letter-spacing: 0.08em;
text-transform: uppercase;
color: var(--muted);
}
.panel-meta .dot {
color: var(--accent);
}
/* ---------- Editor ---------- */
.editor {
background: var(--panel);
border: 3px solid var(--ink);
border-radius: var(--r-lg);
box-shadow: var(--shadow-ink);
padding: 18px;
}
.editor h2 {
margin-bottom: 16px;
}
.field {
margin-bottom: 18px;
}
.field label,
.field-label {
display: block;
margin-bottom: 8px;
font-size: 0.74rem;
font-weight: 800;
letter-spacing: 0.08em;
text-transform: uppercase;
color: var(--ink-2);
}
textarea {
width: 100%;
resize: vertical;
font-family: inherit;
font-size: 0.95rem;
font-weight: 600;
color: var(--ink);
padding: 10px 12px;
border: 2.5px solid var(--ink);
border-radius: var(--r-sm);
background: var(--paper);
}
textarea:focus-visible {
outline: none;
box-shadow: var(--shadow-soft);
}
.counter {
margin: 6px 0 0;
text-align: right;
font-size: 0.72rem;
font-weight: 700;
color: var(--muted);
}
/* Segmented style picker */
.seg {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 8px;
}
.seg-btn {
font: inherit;
font-size: 0.8rem;
font-weight: 700;
cursor: pointer;
padding: 9px 8px;
border: 2.5px solid var(--ink);
border-radius: var(--r-sm);
background: var(--paper);
color: var(--ink);
transition: transform 0.12s ease, background 0.15s ease, box-shadow 0.12s ease;
}
.seg-btn:hover {
transform: translate(-1px, -1px);
box-shadow: 3px 3px 0 var(--ink);
}
.seg-btn:active {
transform: translate(1px, 1px);
box-shadow: none;
}
.seg-btn.is-active {
background: var(--accent);
color: var(--paper);
box-shadow: 3px 3px 0 var(--ink);
}
/* Corner picker */
.corner-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 8px;
max-width: 160px;
}
.corner-btn {
font-size: 1.4rem;
line-height: 1;
cursor: pointer;
aspect-ratio: 1.6 / 1;
display: grid;
place-items: center;
border: 2.5px solid var(--ink);
border-radius: var(--r-sm);
background: var(--paper);
color: var(--ink);
transition: transform 0.12s ease, background 0.15s ease, box-shadow 0.12s ease;
}
.corner-btn:hover {
transform: translate(-1px, -1px);
box-shadow: 3px 3px 0 var(--ink);
}
.corner-btn.is-active {
background: var(--accent-blue);
color: var(--paper);
box-shadow: 3px 3px 0 var(--ink);
}
/* Action buttons */
.actions {
display: flex;
gap: 10px;
flex-wrap: wrap;
}
.btn {
font: inherit;
font-weight: 800;
letter-spacing: 0.04em;
text-transform: uppercase;
font-size: 0.8rem;
cursor: pointer;
padding: 11px 16px;
border: 2.5px solid var(--ink);
border-radius: var(--r-sm);
background: var(--panel);
color: var(--ink);
box-shadow: var(--shadow-soft);
transition: transform 0.12s ease, box-shadow 0.12s ease;
}
.btn:hover {
transform: translate(-1px, -1px);
box-shadow: 5px 5px 0 var(--ink);
}
.btn:active {
transform: translate(2px, 2px);
box-shadow: 2px 2px 0 var(--ink);
}
.btn-primary {
background: var(--accent-2);
}
.btn:focus-visible,
.seg-btn:focus-visible,
.corner-btn:focus-visible {
outline: 3px solid var(--accent-blue);
outline-offset: 2px;
}
/* ---------- Gallery ---------- */
.gallery {
margin-top: 36px;
}
.gallery-h {
font-family: "Bangers", system-ui, sans-serif;
font-weight: 400;
font-size: 1.8rem;
letter-spacing: 0.04em;
text-transform: uppercase;
margin: 0 0 16px;
}
.gallery-grid {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 16px;
}
.mini-panel {
position: relative;
border: 3px solid var(--ink);
border-radius: var(--r-md);
background: var(--panel);
box-shadow: var(--shadow-soft);
overflow: hidden;
padding-bottom: 38px;
}
.mini-art {
position: relative;
aspect-ratio: 1 / 1;
}
.mini-art-a {
background: linear-gradient(135deg, #2e6bff, #1b1142);
}
.mini-art-b {
background: linear-gradient(135deg, #0e0e12, #3a1b6b);
}
.mini-art-c {
background: linear-gradient(135deg, #ff2e4d, #7a1f6b);
}
.mini-art-d {
background: linear-gradient(135deg, #ffd23f, #ff5a7a);
}
.mini-cap {
position: absolute;
left: 0;
right: 0;
bottom: 0;
margin: 0;
padding: 9px 10px;
font-size: 0.7rem;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.05em;
color: var(--ink-2);
border-top: 2px solid var(--ink);
background: var(--paper);
}
.mini-panel .caption {
max-width: 78%;
font-size: 0.66rem;
}
.mini-panel .cap-chapter {
font-size: 1rem;
}
/* ---------- Toast ---------- */
.toast {
position: fixed;
left: 50%;
bottom: 28px;
transform: translate(-50%, 24px);
padding: 12px 20px;
background: var(--ink);
color: var(--paper);
font-weight: 700;
font-size: 0.85rem;
border: 2.5px solid var(--accent-2);
border-radius: var(--r-sm);
box-shadow: 4px 4px 0 var(--accent);
opacity: 0;
pointer-events: none;
transition: transform 0.25s ease, opacity 0.25s ease;
z-index: 50;
}
.toast.is-visible {
opacity: 1;
transform: translate(-50%, 0);
}
/* ---------- Responsive ---------- */
@media (max-width: 860px) {
.layout {
grid-template-columns: 1fr;
}
.gallery-grid {
grid-template-columns: repeat(2, 1fr);
}
}
@media (max-width: 520px) {
.page {
padding: 24px 14px 56px;
}
.stage,
.editor {
padding: 14px;
box-shadow: var(--shadow-soft);
}
.panel {
aspect-ratio: 1 / 1;
}
.caption {
max-width: 74%;
font-size: 0.78rem;
}
.seg {
grid-template-columns: 1fr;
}
.gallery-grid {
grid-template-columns: 1fr;
}
.title {
text-shadow: 3px 3px 0 var(--accent-2);
}
}(function () {
"use strict";
var input = document.getElementById("caption-input");
var charCount = document.getElementById("char-count");
var live = document.getElementById("live-caption");
var liveText = document.getElementById("live-caption-text");
var styleBtns = Array.prototype.slice.call(document.querySelectorAll(".seg-btn"));
var cornerBtns = Array.prototype.slice.call(document.querySelectorAll(".corner-btn"));
var presetBtn = document.getElementById("preset-btn");
var copyBtn = document.getElementById("copy-btn");
var toastEl = document.getElementById("toast");
var STYLE_CLASSES = {
yellow: "cap-yellow",
location: "cap-location",
narration: "cap-narration",
chapter: "cap-chapter"
};
var CORNER_CLASSES = {
"top-left": "cap-top-left",
"top-right": "cap-top-right",
"bottom-left": "cap-bottom-left",
"bottom-right": "cap-bottom-right"
};
var state = {
style: "yellow",
corner: "top-left",
text: input.value
};
// Fictional, style-matched preset captions.
var PRESETS = [
{ style: "yellow", corner: "top-left", text: "MEANWHILE, ON THE NEON RONIN ROOFTOP…" },
{ style: "location", corner: "top-right", text: "SECTOR 9 — 03:14 A.M." },
{ style: "narration", corner: "bottom-left", text: "I never asked to carry the blade. It chose me." },
{ style: "chapter", corner: "top-left", text: "RAIN CIRCUIT" },
{ style: "location", corner: "bottom-right", text: "IRON VANGUARD H.Q. — PRESENT DAY" },
{ style: "narration", corner: "bottom-right", text: "The rain hadn't stopped in forty-one days." },
{ style: "yellow", corner: "bottom-left", text: "LATER, BENEATH THE COPPER SPIRE…" }
];
var presetIndex = 0;
var toastTimer = null;
function toast(msg) {
toastEl.textContent = msg;
toastEl.classList.add("is-visible");
if (toastTimer) clearTimeout(toastTimer);
toastTimer = setTimeout(function () {
toastEl.classList.remove("is-visible");
}, 1900);
}
function render() {
// Reset to base class, then re-apply current style + corner.
live.className = "caption " + STYLE_CLASSES[state.style] + " " + CORNER_CLASSES[state.corner];
live.setAttribute("data-style", state.style);
live.setAttribute("data-corner", state.corner);
var text = state.text.trim() || "Your caption…";
if (state.style === "chapter") {
liveText.innerHTML =
'<small>Chapter Seven</small>' + escapeHtml(text);
} else {
liveText.textContent = text;
}
charCount.textContent = String(state.text.length);
}
function escapeHtml(str) {
return str
.replace(/&/g, "&")
.replace(/</g, "<")
.replace(/>/g, ">");
}
function setActive(buttons, attr, value) {
buttons.forEach(function (b) {
var on = b.getAttribute(attr) === value;
b.classList.toggle("is-active", on);
b.setAttribute("aria-checked", on ? "true" : "false");
});
}
// --- Text input ---
input.addEventListener("input", function () {
state.text = input.value;
render();
});
// --- Style picker ---
styleBtns.forEach(function (btn) {
btn.addEventListener("click", function () {
state.style = btn.getAttribute("data-style");
setActive(styleBtns, "data-style", state.style);
render();
});
});
// --- Corner picker ---
cornerBtns.forEach(function (btn) {
btn.addEventListener("click", function () {
state.corner = btn.getAttribute("data-corner");
setActive(cornerBtns, "data-corner", state.corner);
render();
});
});
// Keyboard arrow navigation within each radiogroup.
function wireArrows(buttons, key) {
buttons.forEach(function (btn, i) {
btn.addEventListener("keydown", function (e) {
var next = -1;
if (e.key === "ArrowRight" || e.key === "ArrowDown") next = (i + 1) % buttons.length;
else if (e.key === "ArrowLeft" || e.key === "ArrowUp") next = (i - 1 + buttons.length) % buttons.length;
if (next === -1) return;
e.preventDefault();
var target = buttons[next];
state[key] = target.getAttribute(key === "style" ? "data-style" : "data-corner");
setActive(buttons, key === "style" ? "data-style" : "data-corner", state[key]);
render();
target.focus();
});
});
}
wireArrows(styleBtns, "style");
wireArrows(cornerBtns, "corner");
// --- Surprise me preset ---
presetBtn.addEventListener("click", function () {
var p = PRESETS[presetIndex % PRESETS.length];
presetIndex++;
state.style = p.style;
state.corner = p.corner;
state.text = p.text;
input.value = p.text;
setActive(styleBtns, "data-style", state.style);
setActive(cornerBtns, "data-corner", state.corner);
render();
toast("Lettered a fresh caption ✍");
});
// --- Copy HTML ---
function buildHtml() {
var cls = "caption " + STYLE_CLASSES[state.style] + " " + CORNER_CLASSES[state.corner];
var text = state.text.trim() || "Your caption…";
if (state.style === "chapter") {
return (
'<div class="' + cls + '">\n' +
' <span class="caption-text"><small>Chapter Seven</small>' + escapeHtml(text) + "</span>\n" +
"</div>"
);
}
return (
'<div class="' + cls + '">\n' +
' <span class="caption-text">' + escapeHtml(text) + "</span>\n" +
"</div>"
);
}
copyBtn.addEventListener("click", function () {
var html = buildHtml();
if (navigator.clipboard && navigator.clipboard.writeText) {
navigator.clipboard.writeText(html).then(
function () {
toast("Caption HTML copied ✓");
},
function () {
fallbackCopy(html);
}
);
} else {
fallbackCopy(html);
}
});
function fallbackCopy(text) {
var ta = document.createElement("textarea");
ta.value = text;
ta.style.position = "fixed";
ta.style.opacity = "0";
document.body.appendChild(ta);
ta.select();
try {
document.execCommand("copy");
toast("Caption HTML copied ✓");
} catch (err) {
toast("Copy failed — select manually");
}
document.body.removeChild(ta);
}
render();
})();<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Comics — Narration Caption Box</title>
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link
href="https://fonts.googleapis.com/css2?family=Bangers&family=Inter:wght@400;500;600;700;800&display=swap"
rel="stylesheet"
/>
<link rel="stylesheet" href="style.css" />
</head>
<body>
<div class="page">
<header class="masthead">
<p class="kicker">Issue #12 — Letterer's Toolkit</p>
<h1 class="title">Narration <span>Caption Box</span></h1>
<p class="lede">
The four workhorse captions of sequential art — the yellow
<em>Meanwhile…</em> box, the location/time stamp, the
first-person narration box, and the chapter-title plate — pinned
to the corners of a single panel. Type into the editor to letter your
own.
</p>
</header>
<main class="layout">
<!-- Live editor panel -->
<section class="stage" aria-labelledby="stage-h">
<div class="stage-head">
<h2 id="stage-h">Live Panel</h2>
<span class="badge" id="caption-count">4 captions</span>
</div>
<figure class="panel" id="panel">
<div class="panel-art" role="img" aria-label="Comic panel art: a lone figure on a neon rooftop at night">
<div class="art-sky"></div>
<div class="art-skyline"></div>
<div class="art-moon"></div>
<div class="art-hero"></div>
<div class="halftone-overlay" aria-hidden="true"></div>
</div>
<!-- The live, editable caption -->
<div
class="caption cap-yellow cap-top-left"
id="live-caption"
data-style="yellow"
data-corner="top-left"
>
<span class="caption-text" id="live-caption-text">MEANWHILE, ON THE NEON RONIN ROOFTOP…</span>
</div>
</figure>
<figcaption class="panel-meta">
<span>NEON RONIN</span>
<span class="dot" aria-hidden="true">•</span>
<span>Vol. 3 — “Rain Circuit”</span>
<span class="dot" aria-hidden="true">•</span>
<span>Page 04</span>
</figcaption>
</section>
<!-- Editor controls -->
<section class="editor" aria-labelledby="editor-h">
<h2 id="editor-h">Caption Editor</h2>
<div class="field">
<label for="caption-input">Caption text</label>
<textarea
id="caption-input"
rows="3"
maxlength="120"
spellcheck="false"
>MEANWHILE, ON THE NEON RONIN ROOFTOP…</textarea>
<p class="counter"><span id="char-count">36</span>/120</p>
</div>
<div class="field">
<span class="field-label" id="style-label">Caption style</span>
<div class="seg" role="radiogroup" aria-labelledby="style-label">
<button class="seg-btn is-active" type="button" role="radio" aria-checked="true" data-style="yellow">
Yellow box
</button>
<button class="seg-btn" type="button" role="radio" aria-checked="false" data-style="location">
Location / time
</button>
<button class="seg-btn" type="button" role="radio" aria-checked="false" data-style="narration">
Narration
</button>
<button class="seg-btn" type="button" role="radio" aria-checked="false" data-style="chapter">
Chapter title
</button>
</div>
</div>
<div class="field">
<span class="field-label" id="corner-label">Corner position</span>
<div class="corner-grid" role="radiogroup" aria-labelledby="corner-label">
<button class="corner-btn is-active" type="button" role="radio" aria-checked="true" data-corner="top-left" aria-label="Top left">◢</button>
<button class="corner-btn" type="button" role="radio" aria-checked="false" data-corner="top-right" aria-label="Top right">◣</button>
<button class="corner-btn" type="button" role="radio" aria-checked="false" data-corner="bottom-left" aria-label="Bottom left">◥</button>
<button class="corner-btn" type="button" role="radio" aria-checked="false" data-corner="bottom-right" aria-label="Bottom right">◤</button>
</div>
</div>
<div class="actions">
<button class="btn btn-primary" type="button" id="preset-btn">Surprise me</button>
<button class="btn" type="button" id="copy-btn">Copy HTML</button>
</div>
</section>
</main>
<!-- Style reference strip -->
<section class="gallery" aria-labelledby="gallery-h">
<h2 id="gallery-h" class="gallery-h">The Four Captions</h2>
<div class="gallery-grid">
<article class="mini-panel">
<div class="mini-art mini-art-a"><span class="halftone-overlay" aria-hidden="true"></span></div>
<div class="caption cap-yellow cap-top-left static"><span class="caption-text">MEANWHILE…</span></div>
<p class="mini-cap">Yellow box — transitions & asides</p>
</article>
<article class="mini-panel">
<div class="mini-art mini-art-b"><span class="halftone-overlay" aria-hidden="true"></span></div>
<div class="caption cap-location cap-top-left static"><span class="caption-text">SECTOR 9 — 03:14 A.M.</span></div>
<p class="mini-cap">Location / time stamp</p>
</article>
<article class="mini-panel">
<div class="mini-art mini-art-c"><span class="halftone-overlay" aria-hidden="true"></span></div>
<div class="caption cap-narration cap-bottom-left static"><span class="caption-text">I never asked to carry the blade. It chose me.</span></div>
<p class="mini-cap">First-person narration</p>
</article>
<article class="mini-panel">
<div class="mini-art mini-art-d"><span class="halftone-overlay" aria-hidden="true"></span></div>
<div class="caption cap-chapter cap-top-left static"><span class="caption-text"><small>Chapter Seven</small>RAIN CIRCUIT</span></div>
<p class="mini-cap">Chapter-title plate</p>
</article>
</div>
</section>
</div>
<div class="toast" id="toast" role="status" aria-live="polite"></div>
<script src="script.js"></script>
</body>
</html>Narration Caption Box
The four workhorse captions of sequential art, rendered as one reusable component. A neon-rooftop sample panel — drawn entirely in CSS with a halftone overlay — hosts a single live caption, while a reference strip below shows each variant in context: the bright yellow Meanwhile… transition box, an inked location/time stamp, a paper-stock first-person narration plate, and a bold accent chapter-title banner set in display lettering. Every caption carries a hard ink border, a slight tilt, and a hand-set drop shadow.
The editor drives the live preview in real time. Type into the textarea to re-letter the caption, choose one of the four styles from a segmented radiogroup, and pin the caption to any of the four corners with the position picker. Both pickers are keyboard-navigable with arrow keys, the character counter tracks the 120-character limit, and the chapter-title style automatically adds its small kicker line.
A Surprise me button cycles through fictional, style-matched presets from the Neon Ronin and Iron Vanguard series, and Copy HTML writes the current caption markup to the clipboard so it can be dropped straight into a page. A small toast confirms each action, and the layout collapses to a single column with a square panel down to 360px.
Illustrative UI only — fictional series, characters, and data.