Chapter 02 · Type system

Typography. The voice on the page.

Four typefaces, ten sizes, one italic. Every word in the product, the site, and the sales motion reads from this page.

2.1 Type families

Four typefaces in defined roles. Bricolage Grotesque carries headlines. DM Sans carries everything you read. Fraunces lends italic warmth to hero moments. JetBrains Mono labels the technical.

Bricolage Grotesque
var(--f-display)
Every lead.
Human touch.
Weights used: 400, 500, 600, 700
Display face. Headings, wordmarks, marketing heroes. Variable — uses optical sizing.
DM Sans
var(--f-body)
Qualify leads with conversation, not pressure. The agent listens, then moves the deal forward.
Weights used: 400, 500, 600
Body face. Paragraphs, UI text, forms. Calm, humanist, high-legibility.
Fraunces
var(--f-serif)
every time.
Italic only · SOFT axis = 80 · weight 400
Editorial italic. Used only in em inside display headings — a warm, literary counterpoint to Bricolage.
JetBrains Mono
var(--f-mono)
const response = await agent.qualify(lead);
Weights used: 400, 500, 600
Code, tokens, technical chrome. Also used in breadcrumbs, eyebrows, meta captions.

Reaching the families

--f-display · --f-body · --f-serif · --f-mono

Always reference the family token, not the typeface name — that way upstream font swaps stay one-line changes.

<h1 style="font-family: var(--f-display);">Display</h1>
<p  style="font-family: var(--f-body);">Body copy</p>
<em style="font-family: var(--f-serif); font-style: italic;">warm italic</em>
<code style="font-family: var(--f-mono);">token</code>
:root {
  --f-display: "Bricolage Grotesque", system-ui, sans-serif;
  --f-body:    "DM Sans", system-ui, sans-serif;
  --f-serif:   "Fraunces", Georgia, serif;
  --f-mono:    "JetBrains Mono", ui-monospace, monospace;
}

h1, h2, h3 { font-family: var(--f-display); }
p, li      { font-family: var(--f-body); }
em         { font-family: var(--f-serif); font-style: italic; }
code, kbd  { font-family: var(--f-mono); }
// Tailwind preset exposes the four families as: font-display, font-body,
// font-serif, font-mono — wired to the kit's CSS variables.
export default function Hero() {
  return (
    <header className="font-display">
      <h1>Every lead. <em className="font-serif italic text-accent">every time.</em></h1>
      <p className="font-body text-fg-soft">Calm, legible body copy.</p>
      <code className="font-mono text-sm">var(--accent)</code>
    </header>
  );
}

2.2 Type scale

Ten type sizes, never more. Each step has a defined role. When in doubt, choose the smaller size — this system reads warmest when it's not shouting.

The scale

56 / 44 / 32 / 24 / 20 / 17 / 19 / 16 / 13 / 11

Every lead.
Display
56px · 1.05
Hero headline. Marketing only.
Qualify faster.
Headline
44px · 1.08
Section hero on product pages.
Conversation.
Title
32px · 1.15
Page titles, big card headers.
In minutes.
Heading
24px · 1.25
Sub-sections.
What it does
Subheading
20px · 1.3
Card titles, field groups.
A common H4
Strong
17px · 1.4
Lists, table headers, callouts.
Warm intro text that sets tone.
Lede
19px · 1.55
Sentence after a headline.
Default paragraph size. Calm, legible, generous line height.
Body
16px · 1.6
Everything.
Metadata / helper text.
Caption
13px · 1.55
Timestamps, hints, micro-copy.
Legal + labels
Micro
11px · 1.5
Footnotes, tags, eyebrow text.
<h1 class="display">Every lead.</h1>           <!-- 56px · hero only -->
<h2 class="headline">Qualify faster.</h2>     <!-- 44px -->
<h2 class="title">Conversation.</h2>          <!-- 32px -->
<h3 class="heading">In minutes.</h3>          <!-- 24px -->
<h4 class="subheading">What it does</h4>    <!-- 20px -->
<h5 class="strong">A common H4</h5>            <!-- 17px -->
<p  class="lede">Warm intro text.</p>           <!-- 19px -->
<p  class="body">Default paragraph.</p>          <!-- 16px -->
<p  class="caption">Metadata.</p>                <!-- 13px -->
<p  class="micro">Legal + labels</p>             <!-- 11px -->
/* canonical scale */
.display    { font: 700 clamp(36px, 5vw, 56px)/1.05 var(--f-display); letter-spacing: -0.025em; }
.headline   { font: 700 44px/1.08 var(--f-display); letter-spacing: -0.02em; }
.title      { font: 700 32px/1.15 var(--f-display); letter-spacing: -0.015em; }
.heading    { font: 600 24px/1.25 var(--f-display); letter-spacing: -0.01em; }
.subheading { font: 600 20px/1.3  var(--f-display); letter-spacing: -0.005em; }

.lede       { font: 400 19px/1.55 var(--f-body); color: var(--fg-soft); }
.body       { font: 400 16px/1.6  var(--f-body); }
.caption    { font: 400 13px/1.55 var(--f-body); color: var(--fg-dim); }
.micro      { font: 500 11px/1.5  var(--f-mono); letter-spacing: 0.06em;
              text-transform: uppercase; color: var(--fg-dim); }
import { Heading, Lede, Caption } from "@magicblocksai/ui";
// The kit ships React wrappers for the semantic levels (Heading, Lede, Caption).
// For named display sizes reach for the className shipped with @magicblocksai/css.

export default function ScaleDemo() {
  return (
    <>
      <h1 className="display">Every lead.</h1>
      <h2 className="headline">Qualify faster.</h2>
      <Heading level={2}>Conversation.</Heading>
      <Heading level={3}>In minutes.</Heading>
      <Heading level={4}>What it does</Heading>
      <Lede>Warm intro text that sets tone.</Lede>
      <p>Default paragraph size. Calm, legible, generous line height.</p>
      <Caption>Metadata / helper text.</Caption>
      <p className="micro">Legal + labels</p>
    </>
  );
}

2.3 Headings

h1–h6 map directly to the scale. One Fraunces italic per headline, maximum. Balance-wrapped for short lines.

Heading hierarchy

<h1>–<h6>

Every lead. Every time. In minutes.

h1 — display

How the agent qualifies

h2 — title

Always on. Never annoying.

h3 — heading

What it does

h4 — subheading
Supporting facet
h5 — strong
Caption level
h6 — micro
<h1 class="display">Every lead. <em>Every time.</em> In minutes.</h1>
<h2 class="title">How the agent qualifies</h2>
<h3 class="heading">Always on. Never annoying.</h3>
<h4 class="subheading">What it does</h4>
<h5 class="strong">Supporting facet</h5>
<h6 class="micro">Caption level</h6>
h1, h2, h3, h4, h5, h6 {
  font-family: var(--f-display);
  color: var(--fg);
  margin: 0 0 var(--s-4);
  font-weight: 700;
  letter-spacing: -0.02em;
  line-height: 1.12;
  text-wrap: balance;
}
h1 { font-size: clamp(36px, 5vw, 56px); line-height: 1.05; letter-spacing: -0.025em; }
h2 { font-size: 32px; }
h3 { font-size: 24px; font-weight: 600; letter-spacing: -0.01em; }
h4 { font-size: 20px; font-weight: 600; }
h5 { font-size: 17px; font-weight: 600; letter-spacing: 0; }
h6 { font-family: var(--f-mono); font-size: 11px; font-weight: 500;
     text-transform: uppercase; letter-spacing: 0.08em; color: var(--fg-dim); }

/* The one italic. Fraunces, accent-coloured, inside any display heading. */
h1 em, h2 em, .headline em, .display em {
  font-family: var(--f-serif);
  font-style: italic;
  font-weight: 400;
  color: var(--accent);
  font-variation-settings: "SOFT" 80;
}
import { Heading } from "@magicblocksai/ui";

export default function Demo() {
  return (
    <>
      <Heading level={1} display>
        Every lead. <em>Every time.</em> In minutes.
      </Heading>
      <Heading level={2}>How the agent qualifies</Heading>
      <Heading level={3}>Always on. Never annoying.</Heading>
      <Heading level={4}>What it does</Heading>
      <Heading level={5}>Supporting facet</Heading>
      <Heading level={6}>Caption level</Heading>
    </>
  );
}

2.4 Body text

Paragraphs are 16px DM Sans with 1.6 line-height and a max width of 62ch. First paragraph after a headline is often a lede — 19px, darker, shorter line-length.

Paragraph, lede, emphasis, link

<p> <strong> <em> <a>

Max line-length is a readability token too. Never let paragraphs exceed ~65 characters.

MagicBlocks helps businesses respond faster, qualify better, and convert more leads — without needing a bigger human team.

Our AI sales agents operate 24/7 across chat, email, SMS, and voice. They feel human, but they behave like a consistent, high-performing operator: patient, persistent, and always on context.

When a deal gets nuanced, the agent hands off cleanly to your team. No dropped threads. No lost momentum.

<p class="lede">MagicBlocks helps…</p>
<p>Our AI sales agents operate <strong>patient, persistent…</strong></p>
<p>… the agent <a href="#">hands off cleanly</a>.</p>
p { font: 400 16px/1.6 var(--f-body); color: var(--fg-soft); margin: 0 0 var(--s-5); max-width: 62ch; }
p.lede { font-size: 19px; line-height: 1.55; color: var(--fg); font-weight: 400; max-width: 58ch; }

strong { font-weight: 600; color: var(--fg); }
em     { font-style: italic; }

a { color: var(--fg); text-decoration-color: var(--hair);
    text-decoration-thickness: 1px; text-underline-offset: 3px;
    transition: color var(--dur-2) var(--ease), text-decoration-color var(--dur-2) var(--ease); }
a:hover { color: var(--accent-text); text-decoration-color: currentColor; }
import { Lede } from "@magicblocksai/ui";

export default function Demo() {
  return (
    <>
      <Lede>
        MagicBlocks helps businesses respond faster, qualify better,
        and convert more leads — without needing a bigger human team.
      </Lede>
      <p>
        Our AI sales agents operate 24/7 across chat, email, SMS, and voice.
        They feel human, but they behave like a consistent, high-performing
        operator: <strong>patient, persistent, and always on context</strong>.
      </p>
      <p>
        When a deal gets nuanced, the agent <a href="#">hands off cleanly</a>
        to your team. No dropped threads. No lost momentum.
      </p>
    </>
  );
}

2.5 Eyebrow · lede · caption

Three utilities you'll use in every hero, every card, every feature block.

Utility classes

.eyebrow · .lede · .caption

How it works

Speed-to-lead, without the pressure

Every inbound lead gets a real reply inside 30 seconds — on the channel they arrived.

Updated · April 22, 2026

<p class="eyebrow">How it works</p>
<h2>Speed-to-lead, without the pressure</h2>
<p class="lede">Every inbound lead gets a real reply…</p>
<p class="caption">Updated · April 22, 2026</p>
.eyebrow {
  font-family: var(--f-mono);
  font-size: 11.5px;
  font-weight: 500;
  color: var(--fg-dim);
  text-transform: uppercase;
  letter-spacing: 0.1em;
  margin: 0 0 var(--s-3);
}
.lede { font: 400 19px/1.55 var(--f-body); color: var(--fg); max-width: 58ch; }
.caption { font: 400 13px/1.55 var(--f-body); color: var(--fg-dim); margin-top: var(--s-4); }
.micro { font: 500 11px/1.4 var(--f-mono); text-transform: uppercase; letter-spacing: 0.08em; color: var(--fg-dim); }
import { Eyebrow, Heading, Lede, Caption } from "@magicblocksai/ui";

export default function Demo() {
  return (
    <>
      <Eyebrow>How it works</Eyebrow>
      <Heading level={2}>Speed-to-lead, without the pressure</Heading>
      <Lede>Every inbound lead gets a real reply inside 30 seconds.</Lede>
      <Caption>Updated · April 22, 2026</Caption>
    </>
  );
}

2.6 Lists

Unordered lists use a pink dot, not a bullet glyph. Ordered lists use numbered chips. Description lists put the label in mono.

ul · ol · dl

three flavours

Nest no deeper than two levels — if you need three, it probably wants to be a table.

Unordered
  • Speed-to-lead under 30 seconds
  • 24/7 coverage across every channel
  • Clean handoff to humans when it matters
Ordered
  1. Lead arrives on any channel
  2. Agent responds in seconds
  3. Qualified lead is handed off
Descriptive
Speed
Reply in seconds, not hours.
Tone
Calm, empathetic, consistent.
Memory
Full context across every touch.
<!-- Unordered: pink dot bullets -->
<ul>
  <li>Speed-to-lead under 30 seconds</li>
  <li>24/7 coverage across every channel</li>
  <li>Clean handoff to humans when it matters</li>
</ul>

<!-- Ordered: pink-soft chip numerals -->
<ol>
  <li>Lead arrives on any channel</li>
  <li>Agent responds in seconds</li>
  <li>Qualified lead is handed off</li>
</ol>

<!-- Description: mono term, soft definition -->
<dl>
  <dt>Speed</dt><dd>Reply in seconds, not hours.</dd>
  <dt>Tone</dt><dd>Calm, empathetic, consistent.</dd>
  <dt>Memory</dt><dd>Full context across every touch.</dd>
</dl>
ul, ol {
  margin: 0 0 var(--s-5);
  padding: 0 0 0 var(--s-5);
  color: var(--fg-soft);
}
ul { list-style: none; padding-left: 0; }
ul li { position: relative; padding-left: var(--s-5); margin-bottom: var(--s-2); }
ul li::before {
  content: ""; position: absolute; left: 0; top: .7em;
  width: 6px; height: 6px; border-radius: 50%;
  background: var(--accent);
}
ol { counter-reset: step; list-style: none; padding-left: 0; }
ol li { counter-increment: step; position: relative; padding-left: var(--s-7); margin-bottom: var(--s-3); }
ol li::before {
  content: counter(step); position: absolute; left: 0; top: -1px;
  width: 20px; height: 20px; border-radius: 50%;
  background: var(--accent-soft); color: var(--accent-text);
  font: 600 11px/20px var(--f-mono); text-align: center;
}

dl { margin: 0; display: grid; grid-template-columns: auto 1fr; gap: var(--s-2) var(--s-5); }
dt { font-family: var(--f-mono); font-size: 12px; color: var(--fg-dim);
     text-transform: uppercase; letter-spacing: 0.06em; padding-top: 4px; }
dd { margin: 0; color: var(--fg-soft); }
// All three list flavours render via raw <ul>/<ol>/<dl> — the kit's
// stylesheet wires up pink-dot bullets, ordered chips, and dl grid.
export default function Lists() {
  return (
    <>
      <ul>
        <li>Speed-to-lead under 30 seconds</li>
        <li>24/7 coverage across every channel</li>
        <li>Clean handoff to humans when it matters</li>
      </ul>

      <ol>
        <li>Lead arrives on any channel</li>
        <li>Agent responds in seconds</li>
        <li>Qualified lead is handed off</li>
      </ol>

      <dl>
        <dt>Speed</dt><dd>Reply in seconds, not hours.</dd>
        <dt>Tone</dt><dd>Calm, empathetic, consistent.</dd>
        <dt>Memory</dt><dd>Full context across every touch.</dd>
      </dl>
    </>
  );
}

2.7 Blockquote

Pull quotes and testimonials. The glyph is Fraunces italic, the attribution is Bricolage + mono — never switch.

Figure + blockquote + figcaption

<figure class='quote'>

Most sales outcomes are driven by emotion, trust, and clarity — not just information. The goal isn't pressure. It's momentum.
Jay Stockwell Founder · MagicBlocks
<figure class="quote">
  <blockquote>
    Most sales outcomes are driven by <em>emotion…</em>
  </blockquote>
  <figcaption>
    <span>Jay Stockwell</span>
    <span>Founder · MagicBlocks</span>
  </figcaption>
</figure>
figure.quote {
  margin: 0; padding: var(--s-7) var(--s-7) var(--s-7) var(--s-9);
  background: var(--bg-paper);
  border-left: 3px solid var(--accent);
  border-radius: 0 var(--r-lg) var(--r-lg) 0;
  position: relative;
}
figure.quote::before {
  content: "\201C";
  position: absolute; top: -8px; left: var(--s-5);
  font-family: var(--f-serif); font-style: italic;
  font-size: 72px; line-height: 1; color: var(--accent);
  font-variation-settings: "SOFT" 80;
}
figure.quote blockquote {
  margin: 0 0 var(--s-4);
  font: 400 21px/1.4 var(--f-body);
  color: var(--fg);
  letter-spacing: -0.005em;
}
figure.quote blockquote em {
  font-family: var(--f-serif);
  font-style: italic; font-weight: 400;
  color: var(--accent);
  font-variation-settings: "SOFT" 80;
}
figure.quote figcaption {
  display: flex; flex-direction: column; gap: 2px;
}
figure.quote figcaption span:first-child {
  font-family: var(--f-display); font-weight: 600; font-size: 14px;
}
figure.quote figcaption span:last-child {
  font-family: var(--f-mono); font-size: 12px; color: var(--fg-dim);
}
import { Quote } from "@magicblocksai/ui";

export default function Demo() {
  return (
    <Quote cite={(
      <>
        <span>Jay Stockwell</span>
        <span>Founder · MagicBlocks</span>
      </>
    )}>
      Most sales outcomes are driven by <em>emotion, trust, and clarity</em>
      — not just information.
    </Quote>
  );
}

2.8 Inline code · kbd · mark

Technical punctuation for docs, changelogs, and agent configuration copy.

Inline technical

<code> <kbd> <mark> <pre>

Use var(--accent) for hero CTAs. Press K to open command palette. A highlighted phrase sits comfortably inside a sentence.

const agent = new Agent({
  voice:   "warm",
  channel: ["chat", "sms", "email"],
});
<p>
  Use <code>var(--accent)</code> for hero CTAs.
  Press <kbd></kbd> <kbd>K</kbd> to open command palette.
  A <mark>highlighted phrase</mark> sits comfortably inside a sentence.
</p>

<pre><code>const agent = new Agent({
  voice:   "warm",
  channel: ["chat", "sms", "email"],
});</code></pre>
code {
  font-family: var(--f-mono); font-size: 0.9em;
  padding: 1px 6px;
  background: var(--bg-sunk);
  border: 1px solid var(--hair);
  border-radius: var(--r-xs);
  color: var(--fg);
}
kbd {
  font-family: var(--f-mono); font-size: 0.85em;
  padding: 2px 7px;
  background: var(--bg-paper);
  border: 1px solid var(--hair);
  border-bottom-width: 2px;
  border-radius: var(--r-sm);
  box-shadow: 0 1px 0 var(--hair-soft);
}
mark {
  background: linear-gradient(transparent 60%, var(--yellow-300) 0);
  padding: 0 2px;
  color: var(--fg);
}
pre {
  font-family: var(--f-mono); font-size: 13px; line-height: 1.65;
  padding: var(--s-5);
  background: var(--ink); color: var(--warm-3);
  border-radius: var(--r-lg);
  overflow-x: auto;
  margin: var(--s-4) 0;
}
pre code { background: transparent; border: 0; padding: 0; color: inherit; }
// Inline technical tags use the same elements as HTML — the kit's stylesheet
// styles them automatically. No imports needed.
export default function InlineTechnical() {
  return (
    <>
      <p>
        Use <code>var(--accent)</code> for hero CTAs.
        Press <kbd></kbd> <kbd>K</kbd> to open command palette.
        A <mark>highlighted phrase</mark> sits comfortably inside a sentence.
      </p>
      <pre><code>{`const agent = new Agent({
  voice:   "warm",
  channel: ["chat", "sms", "email"],
});`}</code></pre>
    </>
  );
}

2.9 Code block

Block-level fenced code — for docs, changelogs, config snippets. Ink surface with a language pill and filename in the header. Syntax colours are desaturated and warm; the pink keyword picks up the brand accent even in code.

Code snippet

.codeblk

Keeps the ink surface contained to the block. Use the mono-uppercase language pill as a small brand hit.

JS agent.qualify.js
const response = await agent.qualify(lead);
if (response.intent === "book") {
  calendar.schedule(response.slot);
}
<figure class="codeblk">
  <header class="codeblk-head">
    <span class="codeblk-lang">JS</span>
    <span class="codeblk-file">agent.qualify.js</span>
    <button class="codeblk-copy">copy</button>
  </header>
  <pre><code>
    <span class="tk-kw">const</span> response = <span class="tk-kw">await</span> agent.<span class="tk-fn">qualify</span>(lead);
    <span class="tk-kw">if</span> (response.<span class="tk-pr">intent</span> === <span class="tk-st">"book"</span>) {
      calendar.<span class="tk-fn">schedule</span>(response.<span class="tk-pr">slot</span>);
    }
  </code></pre>
</figure>
.codeblk { background: var(--ink); border-radius: var(--r-md); overflow: hidden; }
.codeblk-head { display: flex; align-items: center; gap: var(--s-3);
  padding: var(--s-3) var(--s-4); border-bottom: 1px solid rgba(255,255,255,.08); }
.codeblk-lang { font: 600 10.5px/1 var(--f-mono); text-transform: uppercase;
  letter-spacing: 0.08em; color: var(--accent-text);
  padding: 3px 6px; border: 1px solid var(--accent); border-radius: var(--r-xs); }
.codeblk-file { font: 400 12px/1 var(--f-mono); color: rgba(255,255,255,.55); flex: 1; }
.codeblk-copy { font: 500 11px/1 var(--f-mono); color: rgba(255,255,255,.7);
  background: transparent; border: 1px solid rgba(255,255,255,.15);
  border-radius: var(--r-xs); padding: 4px 8px; cursor: pointer; }
.codeblk pre { margin: 0; padding: var(--s-4); overflow: auto;
  font: 400 13px/1.55 var(--f-mono); color: rgba(255,255,255,.88); }
.tk-kw { color: #FE84A9; } .tk-fn { color: #5BD9FC; }
.tk-pr { color: #FFD878; } .tk-st { color: #7DF4D0; }
// .codeblk styles ship via @magicblocksai/css. Compose the markup directly
// so consumers can drop in their own syntax-highlighter (Shiki, Prism, etc).
export default function CodeBlock() {
  return (
    <figure className="codeblk">
      <header className="codeblk-head">
        <span className="codeblk-lang">JS</span>
        <span className="codeblk-file">agent.qualify.js</span>
        <button className="codeblk-copy">copy</button>
      </header>
      <pre><code>{`const response = await agent.qualify(lead);
if (response.intent === "book") {
  calendar.schedule(response.slot);
}`}</code></pre>
    </figure>
  );
}

2.10 Text utilities

Small classes that prevent the most common typographic paper cuts: ugly widows, runaway strings in tables, mismatched digit widths.

Utilities

balance · pretty · truncate · nums

.text-balance

A long heading that balance-wraps across two lines gracefully

.text-pretty

Body copy that avoids ugly widows and orphans when the paragraph wraps.

.truncate

Very long text that we don't want to wrap — trimmed with ellipsis to keep rows tidy

.lining-nums

0123456789 — tabular, lining numerals for tables and dashboards

<p class="text-balance">A long heading that balance-wraps across two lines gracefully</p>
<p class="text-pretty">Body copy that avoids ugly widows and orphans when the paragraph wraps.</p>
<p class="truncate">Very long text that we don't want to wrap — trimmed with ellipsis to keep rows tidy</p>
<p class="lining-nums">0123456789 — tabular, lining numerals for tables and dashboards</p>
.text-balance { text-wrap: balance; }
.text-pretty  { text-wrap: pretty; }

.truncate {
  display: block;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  max-width: 100%;
}

.lining-nums {
  font-variant-numeric: tabular-nums lining-nums;
}

/* antialiasing on for marketing surfaces */
.antialias {
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
}
// Use the kit's class names directly via className. The Tailwind preset
// exposes equivalents (text-balance, text-pretty, truncate, tabular-nums)
// for callers who prefer Tailwind utilities.
export default function Utilities() {
  return (
    <>
      <p className="text-balance">A long heading that balance-wraps across two lines gracefully</p>
      <p className="text-pretty">Body copy that avoids ugly widows and orphans when the paragraph wraps.</p>
      <p className="truncate">Very long text that we don't want to wrap — trimmed with ellipsis to keep rows tidy</p>
      <p className="lining-nums">0123456789 — tabular, lining numerals for tables and dashboards</p>
    </>
  );
}

2.11 Anatomy

How a headline block comes together — a Bricolage display with one Fraunces italic em, an accent-coloured pink, and a DM Sans lede sitting 24px below.

MARKETING HERO

Every lead. In conversation.

A quiet lede sentence that carries the tone for everything below — written in DM Sans, 19px, 1.6 line height, capped at 62ch.

  1. 1Eyebrow · .chapter-eyebrow · mono 12 / uppercase
  2. 2Display · Bricolage 700 · clamp(36,5vw,56) · -0.025em
  3. 3Accent em · Fraunces italic 400 · SOFT 80 · colour --accent
  4. 4Lede · DM Sans 400 / 19 · line-height 1.6 · max 62ch
  5. 5Rhythm · 16/24/32/48 vertical scale between blocks

2.12 Inline headline

Click-to-edit page-title primitive. The resting heading uses headline typography (Bricolage 600 / 28px); clicking it swaps the heading for a styled text input that matches the same metrics — no metric jolt, no surface flash. Designed for the page-title slot on detail pages where the field is the headline. Pair with <PageHeader onTitleSave> for the canonical detail-page pattern; reach for this standalone primitive when your page chrome doesn’t fit the <PageHeader> shape (custom hero layouts, modal titles, drawer headers).

Resting display · with value and placeholder

.inline-headline

Two resting states: a headline with a populated value on the left and the empty-value placeholder shape on the right. Click either button to enter edit mode; Enter commits, Escape cancels, blur commits when the value differs.

With value

Empty · placeholder

<!-- Resting display with a value -->
<div class="inline-headline">
  <button type="button" class="inline-headline-display" aria-label="Edit headline">
    <h1 class="inline-headline-text">BlueRock renewal — Q2</h1>
  </button>
</div>

<!-- Empty value renders the placeholder span -->
<div class="inline-headline">
  <button type="button" class="inline-headline-display" aria-label="Edit headline">
    <h1 class="inline-headline-text"><span class="inline-headline-placeholder">Untitled ticket</span></h1>
  </button>
</div>

<!-- While editing, the kit swaps the display for an input -->
<div class="inline-headline is-editing">
  <input type="text" class="inline-headline-input" aria-label="Edit headline">
</div>
.inline-headline {
  display: inline-block;
  position: relative;
  width: 100%;
}

.inline-headline-display {
  appearance: none;
  background: transparent;
  border: 1px solid transparent;
  border-radius: var(--r-sm, 6px);
  padding: 2px 6px;
  margin: -2px -6px; /* keep external geometry identical to a non-editable headline */
  cursor: text;
  text-align: left;
  width: 100%;
  color: inherit;
  font: inherit;
  transition: background var(--dur-1, 80ms) var(--ease, ease),
              border-color var(--dur-1, 80ms) var(--ease, ease);
}

.inline-headline-display:hover {
  background: color-mix(in oklab, var(--accent) 5%, transparent);
  border-color: color-mix(in oklab, var(--accent) 18%, transparent);
}

.inline-headline-display:focus-visible {
  outline: 0;
  background: var(--bg-paper);
  border-color: var(--accent);
  box-shadow: var(--sh-focus, 0 0 0 3px color-mix(in oklab, var(--accent) 30%, transparent));
}

.inline-headline-display:disabled {
  cursor: default;
  opacity: 0.7;
}

.inline-headline-display:disabled:hover {
  background: transparent;
  border-color: transparent;
}

.inline-headline-text {
  display: inline-block;
  margin: 0;
  font: 600 28px/1.2 var(--f-display);
  letter-spacing: -0.015em;
  color: var(--fg);
  word-break: break-word;
}

.page-header-title-text .inline-headline,
.page-header-title-text .inline-headline-display {
  width: auto;
  max-width: 100%;
}

.page-header-title-text .inline-headline-text {
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  word-break: normal;
}

.inline-headline-placeholder {
  color: var(--fg-faint);
  font-style: italic;
}

.inline-headline.is-editing .inline-headline-input,
.inline-headline-input {
  display: block;
  width: 100%;
  appearance: none;
  background: var(--bg-paper);
  border: 1px solid var(--accent);
  border-radius: var(--r-sm, 6px);
  padding: 2px 6px;
  margin: -2px -6px;
  font: 600 28px/1.2 var(--f-display);
  letter-spacing: -0.015em;
  color: var(--fg);
  outline: 0;
  box-shadow: var(--sh-focus, 0 0 0 3px color-mix(in oklab, var(--accent) 30%, transparent));
}

.inline-headline.is-disabled .inline-headline-display {
  cursor: default;
}

@media (max-width: 640px) {
  .inline-headline-text,
  .inline-headline-input {
    font-size: 22px;
  }
}

@media (prefers-reduced-motion: reduce) {
  .inline-headline-display { transition: none; }
}
"use client";
import { useState } from "react";
import { InlineHeadline } from "@magicblocksai/ui";

function TicketSubject({ initial, onSave }: { initial: string; onSave: (next: string) => Promise<void> }) {
  const [subject, setSubject] = useState(initial);
  return (
    <InlineHeadline
      value={subject}
      onSave={async (next) => {
        setSubject(next);            // optimistic
        await onSave(next);
      }}
      placeholder="Untitled ticket"
    />
  );
}

// Or compose into the canonical detail-page chrome:
<PageHeader
  eyebrow="Tickets"
  title={ticket.subject}
  onTitleSave={save}
/>