Chapter 08 / 12 · Feedback & overlays

Feedback & overlays

How MagicBlocks tells the user what's happening. Transient toasts, persistent alerts, top-of-page banners, hover tooltips, rich popovers, blocking modals, slide-over drawers, shimmering skeletons, and branded spinners.

8.1 Toasts

Transient notifications that appear in a corner of the screen. Paper card with a coloured left rail for semantic type, tinted icon chip, title + short description, dismiss button. Auto-dismiss after 5–7 seconds except for errors.

Toast variants

.toast

Four tones: success (green), info (blue), warn (amber), error (pink). Left rail is the single colour signal — the rest of the toast stays on paper.

Meeting booked
Thursday, 2:30 PM with Alicia Chen.
Agent is warming up
New persona is training on your 12 call transcripts.
Webhook retry
Two events queued, next attempt in 30s.
Couldn't reach Twilio
Outbound SMS paused. View status
<div class="toast-demo">
  <div class="toast">
    <span class="toast-ic toast-ic-success"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><polyline points="20 6 9 17 4 12"/></svg></span>
    <div class="toast-body">
      <div class="toast-title">Meeting booked</div>
      <div class="toast-desc">Thursday, 2:30 PM with Alicia Chen.</div>
    </div>
    <button class="toast-close" aria-label="Dismiss"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round"><path d="M18 6 6 18M6 6l12 12"/></svg></button>
  </div>
  <div class="toast toast-info">
    <span class="toast-ic toast-ic-info"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><path d="M12 16v-4M12 8h.01"/></svg></span>
    <div class="toast-body">
      <div class="toast-title">Agent is warming up</div>
      <div class="toast-desc">New persona is training on your 12 call transcripts.</div>
    </div>
    <button class="toast-close" aria-label="Dismiss"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round"><path d="M18 6 6 18M6 6l12 12"/></svg></button>
  </div>
  <div class="toast toast-warn">
    <span class="toast-ic toast-ic-warn"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M10.29 3.86 1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"/><path d="M12 9v4M12 17h.01"/></svg></span>
    <div class="toast-body">
      <div class="toast-title">Webhook retry</div>
      <div class="toast-desc">Two events queued, next attempt in 30s.</div>
    </div>
    <button class="toast-close" aria-label="Dismiss"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round"><path d="M18 6 6 18M6 6l12 12"/></svg></button>
  </div>
  <div class="toast toast-error">
    <span class="toast-ic toast-ic-error"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><path d="M12 8v4M12 16h.01"/></svg></span>
    <div class="toast-body">
      <div class="toast-title">Couldn't reach Twilio</div>
      <div class="toast-desc">Outbound SMS paused. <a href="#">View status</a></div>
    </div>
    <button class="toast-close" aria-label="Dismiss"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round"><path d="M18 6 6 18M6 6l12 12"/></svg></button>
  </div>
</div>
.toast-demo { display: flex; flex-direction: column; gap: var(--s-3); width: 100%; max-width: 380px; }
.toast {
  display: flex; align-items: flex-start; gap: var(--s-3);
  padding: 12px var(--s-4);
  background: var(--bg-paper);
  border: 1px solid var(--hair);
  border-radius: var(--r-md);
  box-shadow: var(--sh-3);
  position: relative;
}
.toast::before {
  content: ""; position: absolute;
  left: 0; top: 10px; bottom: 10px; width: 3px;
  border-radius: 3px;
  background: var(--success);
}
.toast.toast-info::before  { background: var(--info); }
.toast.toast-warn::before  { background: var(--warning); }
.toast.toast-error::before { background: var(--accent); }

.toast-ic {
  width: 28px; height: 28px; border-radius: 50%;
  display: inline-flex; align-items: center; justify-content: center;
  flex-shrink: 0; margin-top: 1px;
}
.toast-ic-success { background: var(--success-soft); color: var(--success-text); }
.toast-ic-info    { background: var(--info-soft);    color: var(--info-text); }
.toast-ic-warn    { background: var(--warning-soft); color: var(--warning-text); }
.toast-ic-error   { background: var(--accent-soft);  color: var(--accent-text); }

.toast-body { flex: 1; min-width: 0; }
.toast-title { font: 600 14px/1.3 var(--f-display); color: var(--fg); letter-spacing: -0.005em; }
.toast-desc { font: 400 13px/1.5 var(--f-body); color: var(--fg-soft); margin-top: 2px; }
.toast-desc a { color: var(--accent-text); text-decoration: none; font-weight: 500; }
.toast-desc a:hover { text-decoration: underline; }
.toast-close {
  background: transparent; border: 0; padding: 4px; margin: -4px;
  color: var(--fg-dim); cursor: pointer;
  border-radius: var(--r-xs);
  align-self: flex-start;
}
.toast-close:hover { color: var(--fg); background: var(--bg-sunken); }

8.2 Inline alerts

Persistent, contextual messages that live inside a page surface (not floating). Bigger than toasts, smaller than modals. Good for 'your integration is connected', config warnings, or post-action confirmations.

Alert variants

.alert

Left rail in the semantic colour, tinted icon chip. Supports a title + paragraph + action row.

Connected to HubSpot

Contacts and deals will start syncing in the next few minutes.

This agent runs in simulation mode

Messages won't be sent to real contacts until you flip the switch to live.

Quiet hours will apply

Outbound SMS is paused between 9pm and 7am in each contact's timezone.

Couldn't verify your sending domain

DNS records are missing. Outbound email is paused until SPF and DKIM are in place.

<div class="alerts">
  <div class="alert alert-success">
    <span class="alert-ic"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><polyline points="20 6 9 17 4 12"/></svg></span>
    <div class="alert-body">
      <div class="alert-title">Connected to HubSpot</div>
      <p>Contacts and deals will start syncing in the next few minutes.</p>
    </div>
  </div>
  <div class="alert alert-info">
    <span class="alert-ic"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><path d="M12 16v-4M12 8h.01"/></svg></span>
    <div class="alert-body">
      <div class="alert-title">This agent runs in simulation mode</div>
      <p>Messages won't be sent to real contacts until you flip the switch to live.</p>
      <div class="alert-actions"><a href="#">Read the simulation guide →</a></div>
    </div>
  </div>
  <div class="alert alert-warn">
    <span class="alert-ic"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M10.29 3.86 1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"/><path d="M12 9v4M12 17h.01"/></svg></span>
    <div class="alert-body">
      <div class="alert-title">Quiet hours will apply</div>
      <p>Outbound SMS is paused between 9pm and 7am in each contact's timezone.</p>
    </div>
  </div>
  <div class="alert alert-error">
    <span class="alert-ic"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><path d="M12 8v4M12 16h.01"/></svg></span>
    <div class="alert-body">
      <div class="alert-title">Couldn't verify your sending domain</div>
      <p>DNS records are missing. Outbound email is paused until SPF and DKIM are in place.</p>
      <div class="alert-actions">
        <a href="#" class="alert-btn">Fix DNS</a>
        <a href="#">View log</a>
      </div>
    </div>
  </div>
</div>
.alerts { display: flex; flex-direction: column; gap: var(--s-4); }
.alert {
  display: flex; gap: var(--s-3);
  padding: var(--s-4);
  background: var(--bg-paper);
  border: 1px solid var(--hair);
  border-left: 3px solid var(--success);
  border-radius: var(--r-md);
}
.alert.alert-info  { border-left-color: var(--info); }
.alert.alert-warn  { border-left-color: var(--warning); }
.alert.alert-error { border-left-color: var(--accent); }

.alert-ic {
  width: 32px; height: 32px; border-radius: 50%;
  display: inline-flex; align-items: center; justify-content: center;
  flex-shrink: 0;
}
.alert-success .alert-ic { background: var(--success-soft); color: var(--success-text); }
.alert-info    .alert-ic { background: var(--info-soft);    color: var(--info-text); }
.alert-warn    .alert-ic { background: var(--warning-soft); color: var(--warning-text); }
.alert-error .alert-ic   { background: var(--error-soft);   color: var(--error-text); }

.alert-body { flex: 1; }
.alert-title { font: 600 15px/1.3 var(--f-display); color: var(--fg); letter-spacing: -0.005em; }
.alert-body p { font: 400 14px/1.55 var(--f-body); color: var(--fg-soft); margin: 4px 0 0; }
.alert-actions { margin-top: var(--s-3); display: flex; gap: var(--s-4); align-items: center; }
.alert-actions a { font: 500 13px/1 var(--f-body); color: var(--accent-text); text-decoration: none; }
.alert-actions a:hover { text-decoration: underline; }
.alert-btn {
  background: var(--accent) !important; color: var(--paper) !important;
  padding: 8px 14px; border-radius: var(--r-md);
  box-shadow: var(--sh-pink);
  font: 600 13px/1 var(--f-display) !important;
}
.alert-btn:hover { text-decoration: none !important; transform: translateY(-1px); }

8.3 Top-of-page banners

System-wide announcements that span the full width of the app shell. Use sparingly — never more than one at a time. Reserved for launch news, outages, and account-wide notices.

Banner tones

.banner

Info (blue) for feature launches, warn (amber) for maintenance, accent (pink) for promo. Always dismissible.

<div class="banners">
  <div class="banner banner-info">
    <span class="banner-ic"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 3 14 9l6 2-6 2-2 6-2-6-6-2 6-2z"/></svg></span>
    <div class="banner-body">
      <strong>Agent Composer</strong> is out of beta.
      <a href="#">See what changed →</a>
    </div>
    <button class="banner-close" aria-label="Dismiss"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round"><path d="M18 6 6 18M6 6l12 12"/></svg></button>
  </div>
  <div class="banner banner-warn">
    <span class="banner-ic"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M10.29 3.86 1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"/><path d="M12 9v4M12 17h.01"/></svg></span>
    <div class="banner-body">
      Scheduled maintenance <strong>Thu 2:00 AM UTC</strong> — outbound voice paused ~15 min.
    </div>
    <button class="banner-close" aria-label="Dismiss"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round"><path d="M18 6 6 18M6 6l12 12"/></svg></button>
  </div>
  <div class="banner banner-accent">
    <div class="banner-body">
      <strong>Summer credit:</strong> first 1,000 agent minutes free when you upgrade this week.
      <a href="#">Apply credit →</a>
    </div>
    <button class="banner-close" aria-label="Dismiss"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round"><path d="M18 6 6 18M6 6l12 12"/></svg></button>
  </div>
</div>
.banners { display: flex; flex-direction: column; gap: var(--s-3); }
.banner {
  display: flex; align-items: center; gap: var(--s-3);
  padding: 10px var(--s-4);
  background: var(--bg-sunken);
  border: 1px solid var(--hair);
  border-radius: var(--r-md);
  font: 400 13.5px/1.5 var(--f-body); color: var(--fg);
}
.banner strong { font-weight: 600; }
.banner a { color: var(--accent-text); text-decoration: none; font-weight: 500; white-space: nowrap; }
.banner a:hover { text-decoration: underline; }
.banner-info   { background: var(--info-soft); border-color: color-mix(in oklab, var(--info) 30%, transparent); color: var(--info-text); }
.banner-info a { color: var(--info-text); }
.banner-warn  { background: #FFF0D6; border-color: color-mix(in oklab, #8A5A00 20%, transparent); color: #6B4300; }
.banner-warn a { color: #8A5A00; }
.banner-accent {
  background: var(--accent); color: var(--paper);
  border-color: transparent;
  box-shadow: var(--sh-pink);
}
.banner-accent a { color: var(--paper); text-decoration: underline; text-decoration-thickness: 2px; text-underline-offset: 3px; }
.banner-accent .banner-close { color: var(--paper); opacity: 0.7; }
.banner-accent .banner-close:hover { opacity: 1; }

.banner-ic {
  width: 24px; height: 24px; border-radius: 50%;
  display: inline-flex; align-items: center; justify-content: center;
  flex-shrink: 0;
  background: color-mix(in oklab, currentColor 12%, transparent);
  color: inherit;
}
.banner-body { flex: 1; }
.banner-close {
  background: transparent; border: 0; padding: 4px; margin: -4px;
  color: inherit; cursor: pointer; opacity: 0.55;
  border-radius: var(--r-xs);
}
.banner-close:hover { opacity: 1; }

8.4 Tooltips

Small, ink-coloured callouts that appear on hover or focus. Short labels only — if you need two sentences, use a popover instead. The tooltip ALWAYS has a pointing arrow.

Tooltip anchors

.tt

Default ink, pink for accent / brand-relevant actions. Include a tiny kbd pill when the action has a keyboard shortcut.

Keyboard shortcut ⌘K Switches to agent mode MagicBlocks agents run 24/7 and hand off to humans when the conversation gets complex.
<div class="tt-demo">
  <span class="tt-wrap">
    <button class="tt-trigger">Hover me</button>
    <span class="tt tt-top">Keyboard shortcut <kbd class="tt-kbd">⌘K</kbd></span>
  </span>
  <span class="tt-wrap">
    <button class="tt-trigger tt-trigger-accent">Pink anchor</button>
    <span class="tt tt-top tt-accent">Switches to agent mode</span>
  </span>
  <span class="tt-wrap">
    <button class="tt-trigger">Longer content</button>
    <span class="tt tt-top tt-wide">MagicBlocks agents run 24/7 and hand off to humans when the conversation gets complex.</span>
  </span>
</div>
.tt-demo { display: flex; gap: var(--s-5); align-items: center; flex-wrap: wrap; padding: var(--s-7) 0; }
.tt-wrap { position: relative; display: inline-block; }
.tt-trigger {
  background: var(--bg-paper); border: 1px solid var(--hair);
  padding: 8px 14px; border-radius: var(--r-md);
  font: 500 13.5px/1 var(--f-body); color: var(--fg); cursor: help;
}
.tt-trigger-accent { color: var(--accent-text); border-color: var(--accent); }
.tt {
  position: absolute; z-index: 50;
  background: var(--ink); color: var(--paper);
  padding: 7px 10px;
  font: 500 12.5px/1.4 var(--f-body);
  border-radius: var(--r-sm);
  box-shadow: var(--sh-2);
  pointer-events: none;
  opacity: 0; visibility: hidden;
  transform: translate(-50%, 4px);
  transition: opacity var(--dur-2) var(--ease), transform var(--dur-2) var(--ease), visibility var(--dur-2) var(--ease);
  white-space: nowrap;
}
.tt-top { left: 50%; bottom: calc(100% + 8px); }
.tt-top::after {
  content: ""; position: absolute; left: 50%; top: 100%;
  transform: translateX(-50%);
  border: 5px solid transparent; border-top-color: var(--ink);
}
.tt-wrap:hover .tt, .tt-trigger:focus + .tt {
  opacity: 1; visibility: visible;
  transform: translate(-50%, 0);
}
.tt-kbd {
  font: 600 11px/1 var(--f-mono);
  background: color-mix(in oklab, var(--paper) 15%, transparent);
  padding: 2px 5px; border-radius: var(--r-xs); margin-left: 4px;
  border: 1px solid color-mix(in oklab, var(--paper) 25%, transparent);
}
.tt-accent { background: var(--accent); }
.tt-accent::after { border-top-color: var(--accent); }
.tt-wide { white-space: normal; width: 240px; text-align: left; }

8.5 Popover

A richer cousin of the tooltip. Paper surface, pink eyebrow, title + paragraph + optional actions. Use for onboarding tours, in-context tips, and explain-this-feature moments.

Onboarding popover

.pop

Always anchored to a trigger with a little arrow. Keep copy tight — one paragraph max. One primary button + one dismiss link.

Tip

Set a goal for this agent

Agents work best when they have a single crisp objective. Start with "book a 15-minute call" and branch out from there.

<div class="pop-demo">
  <div class="pop">
    <div class="pop-arrow"></div>
    <div class="pop-head">
      <span class="pop-eyebrow mono">Tip</span>
      <button class="pop-close" aria-label="Dismiss"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round"><path d="M18 6 6 18M6 6l12 12"/></svg></button>
    </div>
    <h4 class="pop-title">Set a goal for this agent</h4>
    <p class="pop-desc">Agents work best when they have a single crisp objective. Start with "book a 15-minute call" and branch out from there.</p>
    <div class="pop-foot">
      <button class="pop-link">Skip tour</button>
      <button class="pop-btn">Got it</button>
    </div>
  </div>
</div>
.pop-demo { padding: var(--s-6) 0; }
.pop {
  position: relative;
  width: 320px;
  background: var(--bg-paper);
  border: 1px solid var(--hair);
  border-radius: var(--r-md);
  padding: var(--s-4);
  box-shadow: var(--sh-3);
}
.pop-arrow {
  position: absolute; top: -7px; left: 36px;
  width: 12px; height: 12px;
  background: var(--bg-paper);
  border-top: 1px solid var(--hair);
  border-left: 1px solid var(--hair);
  transform: rotate(45deg);
}
.pop-head { display: flex; justify-content: space-between; align-items: center; margin-bottom: var(--s-2); }
.pop-eyebrow { font-size: 10.5px; text-transform: uppercase; letter-spacing: 0.1em; color: var(--accent-text); font-weight: 600; }
.pop-close { background: transparent; border: 0; color: var(--fg-dim); cursor: pointer; padding: 2px; margin: -2px; border-radius: var(--r-xs); }
.pop-close:hover { color: var(--fg); background: var(--bg-sunken); }
.pop-title { font: 600 15px/1.3 var(--f-display); color: var(--fg); letter-spacing: -0.005em; margin: 0 0 6px; }
.pop-desc { font: 400 13.5px/1.55 var(--f-body); color: var(--fg-soft); margin: 0 0 var(--s-4); }
.pop-foot { display: flex; justify-content: space-between; align-items: center; gap: var(--s-3); }
.pop-link { background: transparent; border: 0; color: var(--fg-soft); font: 500 13px/1 var(--f-body); cursor: pointer; padding: 6px 8px; margin-left: -8px; }
.pop-link:hover { color: var(--fg); }
.pop-btn {
  background: var(--accent); color: var(--paper); border: 0;
  font: 600 13px/1 var(--f-display); padding: 8px 14px;
  border-radius: var(--r-md); cursor: pointer;
  box-shadow: var(--sh-pink);
  transition: transform var(--dur-2) var(--ease);
}
.pop-btn:hover { transform: translateY(-1px); }

8.7 Slide-over drawer

A right-anchored panel for detail views that don't warrant a full page. Same rules as a modal (backdrop, dismiss, focus trap) but slides in from the edge.

Contact drawer

.dr

Stacked layout: header, scroll body, sunken footer. Use for record previews, inspector panels, settings overlays.

<div class="dr-stage">
  <div class="dr-backdrop"></div>
  <aside class="dr" role="dialog" aria-labelledby="dr-title">
    <header class="dr-head">
      <div>
        <p class="dr-eyebrow mono">Contact</p>
        <h3 id="dr-title" class="dr-title">Alicia Chen</h3>
      </div>
      <button class="dr-close" aria-label="Close"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round"><path d="M18 6 6 18M6 6l12 12"/></svg></button>
    </header>
    <div class="dr-body">
      <dl class="dr-kv">
        <div><dt>Email</dt><dd>alicia@northpeak.co</dd></div>
        <div><dt>Phone</dt><dd><span class="mono">+1 415 555 0193</span></dd></div>
        <div><dt>Stage</dt><dd><span class="chip chip-blue">Qualified</span></dd></div>
        <div><dt>Owner</dt><dd>Jay Stockwell</dd></div>
      </dl>
      <h4 class="dr-section">Recent activity</h4>
      <ul class="dr-feed">
        <li><span class="dr-dot"></span> Replied by SMS <span class="dr-when mono">2h</span></li>
        <li><span class="dr-dot"></span> Opened follow-up email <span class="dr-when mono">4h</span></li>
        <li><span class="dr-dot"></span> Meeting booked for Thursday <span class="dr-when mono">1d</span></li>
      </ul>
    </div>
    <footer class="dr-foot">
      <button class="dr-cancel">Close</button>
      <button class="dr-primary">Send a message</button>
    </footer>
  </aside>
</div>
.dr-stage {
  position: relative;
  width: 100%; min-height: 520px;
  background: var(--bg-sunk);
  border: 1px dashed var(--hair);
  border-radius: var(--r-lg);
  overflow: hidden;
}
.dr-backdrop {
  position: absolute; inset: 0;
  background: color-mix(in oklab, var(--ink) 32%, transparent);
}
.dr {
  position: absolute; top: 0; right: 0; bottom: 0;
  width: min(440px, 92%);
  background: var(--bg-paper);
  border-left: 1px solid var(--hair);
  box-shadow: -20px 0 60px -20px color-mix(in oklab, var(--ink) 30%, transparent);
  display: flex; flex-direction: column;
  animation: dr-in var(--dur-3) var(--ease);
}
@keyframes dr-in { from { transform: translateX(100%); } to { transform: translateX(0); } }
.dr-head {
  display: flex; justify-content: space-between; align-items: flex-start;
  padding: var(--s-5) var(--s-5) var(--s-4);
  border-bottom: 1px solid var(--hair);
}
.dr-eyebrow { font-size: 10.5px; text-transform: uppercase; letter-spacing: 0.1em; color: var(--fg-dim); font-weight: 600; }
.dr-title { font: 600 22px/1.2 var(--f-display); letter-spacing: -0.015em; color: var(--fg); margin: 4px 0 0; }
.dr-close { background: transparent; border: 0; color: var(--fg-dim); cursor: pointer; padding: 6px; margin: -6px; border-radius: var(--r-xs); }
.dr-close:hover { color: var(--fg); background: var(--bg-sunken); }
.dr-body { flex: 1; overflow: auto; padding: var(--s-4) var(--s-5); }
.dr-kv { margin: 0 0 var(--s-5); display: flex; flex-direction: column; gap: 10px; }
.dr-kv > div { display: grid; grid-template-columns: 90px 1fr; gap: var(--s-3); align-items: baseline; }
.dr-kv dt { font: 500 11px/1 var(--f-mono); text-transform: uppercase; letter-spacing: 0.08em; color: var(--fg-dim); }
.dr-kv dd { margin: 0; color: var(--fg); font-size: 14px; }
.dr-section { font: 500 11px/1 var(--f-mono); text-transform: uppercase; letter-spacing: 0.08em; color: var(--fg-dim); margin: 0 0 10px; }
.dr-feed { list-style: none; padding: 0; margin: 0; }
.dr-feed li {
  display: flex; align-items: center; gap: 10px;
  padding: 10px 0;
  font-size: 14px; color: var(--fg);
  border-bottom: 1px solid var(--hair-soft);
}
.dr-feed li:last-child { border-bottom: 0; }
.dr-dot { width: 6px; height: 6px; border-radius: 50%; background: var(--accent); flex-shrink: 0; }
.dr-when { color: var(--fg-dim); margin-left: auto; font-size: 12.5px; }
.dr-foot {
  display: flex; justify-content: flex-end; gap: var(--s-3);
  padding: var(--s-4) var(--s-5);
  background: var(--bg-sunken);
  border-top: 1px solid var(--hair);
}
.dr-cancel { background: var(--bg-paper); border: 1px solid var(--hair); padding: 10px 16px; border-radius: var(--r-md); font: 500 14px/1 var(--f-display); color: var(--fg); cursor: pointer; }
.dr-primary { background: var(--accent); color: var(--paper); border: 0; padding: 10px 16px; border-radius: var(--r-md); font: 600 14px/1 var(--f-display); cursor: pointer; box-shadow: var(--sh-pink); }

8.8 Skeleton loaders

Shimmering placeholders that hold layout while content loads. Use instead of spinners when the shape of the result is predictable — it makes the wait feel shorter and prevents layout shift.

Shimmer skeletons

.skel

Linear-gradient background with infinite position animation. Match the rough shape of the content that will replace it.

<div class="skel-demo">
  <div class="skel-card">
    <div class="skel skel-av"></div>
    <div class="skel-lines">
      <div class="skel skel-line" style="width: 45%;"></div>
      <div class="skel skel-line" style="width: 70%;"></div>
      <div class="skel skel-line" style="width: 62%;"></div>
    </div>
  </div>
  <div class="skel-card">
    <div class="skel-stack">
      <div class="skel skel-chip"></div>
      <div class="skel skel-line skel-title" style="width: 58%;"></div>
      <div class="skel skel-line" style="width: 90%;"></div>
      <div class="skel skel-line" style="width: 78%;"></div>
      <div class="skel skel-line" style="width: 40%;"></div>
    </div>
  </div>
</div>
.skel-demo { display: grid; grid-template-columns: 1fr 1fr; gap: var(--s-4); }
.skel-card {
  background: var(--bg-paper);
  border: 1px solid var(--hair);
  border-radius: var(--r-lg);
  padding: var(--s-4);
  display: flex;
  gap: var(--s-3);
}
.skel {
  background: linear-gradient(90deg,
    color-mix(in oklab, var(--fg) 6%, transparent) 0%,
    color-mix(in oklab, var(--fg) 10%, transparent) 50%,
    color-mix(in oklab, var(--fg) 6%, transparent) 100%);
  background-size: 200% 100%;
  border-radius: var(--r-sm);
  animation: skel-shimmer 1.8s ease-in-out infinite;
}
@keyframes skel-shimmer { 0% { background-position: 200% 0; } 100% { background-position: -200% 0; } }
.skel-av { width: 40px; height: 40px; border-radius: 50%; flex-shrink: 0; }
.skel-lines { flex: 1; display: flex; flex-direction: column; gap: 8px; justify-content: center; }
.skel-line { height: 10px; }
.skel-title { height: 14px; margin-bottom: 4px; }
.skel-chip { width: 64px; height: 22px; border-radius: var(--r-pill); }
.skel-stack { display: flex; flex-direction: column; gap: 8px; flex: 1; }
@media (max-width: 640px) { .skel-demo { grid-template-columns: 1fr; } }

8.9 Spinner & inline loader

Pink ring spinner for branded loading moments, ink variant for utility. Inline loader pairs the spinner with a word or two so the user knows what's happening.

Spinner sizes + inline

.spinner · .loader-inline

Default 18px, sm 12px (inline with text), lg 28px (centre-of-stage). Use skeletons, not spinners, when layout is predictable.

Default
Large
Ink on paper
Reaching Twilio…
<div class="spin-demo">
  <div class="spin-item">
    <span class="spinner"></span>
    <span class="spin-label">Default</span>
  </div>
  <div class="spin-item">
    <span class="spinner spinner-lg"></span>
    <span class="spin-label">Large</span>
  </div>
  <div class="spin-item">
    <span class="spinner spinner-ink"></span>
    <span class="spin-label">Ink on paper</span>
  </div>
  <div class="spin-item">
    <div class="loader-inline">
      <span class="spinner spinner-sm"></span>
      <span>Reaching Twilio…</span>
    </div>
  </div>
</div>
.spin-demo { display: flex; gap: var(--s-6); align-items: center; flex-wrap: wrap; }
.spin-item { display: flex; align-items: center; gap: var(--s-3); }
.spin-label { font: 500 12px/1 var(--f-mono); text-transform: uppercase; letter-spacing: 0.08em; color: var(--fg-dim); }
.spinner {
  width: 18px; height: 18px;
  border: 2px solid color-mix(in oklab, var(--accent) 20%, transparent);
  border-top-color: var(--accent);
  border-radius: 50%;
  animation: spin 0.8s linear infinite;
  display: inline-block;
}
.spinner-sm { width: 12px; height: 12px; border-width: 1.5px; }
.spinner-lg { width: 28px; height: 28px; border-width: 2.5px; }
.spinner-ink { border-color: color-mix(in oklab, var(--fg) 20%, transparent); border-top-color: var(--fg); }
@keyframes spin { to { transform: rotate(360deg); } }
.loader-inline {
  display: inline-flex; align-items: center; gap: 8px;
  padding: 8px 12px;
  background: var(--bg-warm);
  border: 1px solid var(--hair);
  border-radius: var(--r-pill);
  font: 500 13px/1 var(--f-body); color: var(--fg-soft);
}

8.10 Inline message

Short feedback attached to a specific field or action — think "Saved", "Password too short", "Connection restored". Lives next to what it's describing; small, quiet, no icon by default.

Inline messages

.inline-msg

Four tones matching the alert palette, but a single row. Use for form feedback and micro-confirmations.

✓ Changes saved
✕ Email is already in use
! You have unsaved changes
i Verification email sent — check your inbox
<div class="inline-msg is-success">✓ Changes saved</div>
<div class="inline-msg is-error">✕ Email is already in use</div>
<div class="inline-msg is-warn">! You have unsaved changes</div>
<div class="inline-msg is-info">i Verification email sent</div>
.inline-msg { display: inline-flex; align-items: center; gap: var(--s-2);
  padding: 6px 10px; border-radius: var(--r-xs);
  font: 500 13px/1.4 var(--f-body); }
.inline-msg.is-success { color: var(--success-text); background: var(--success-soft); }
.inline-msg.is-error   { color: var(--error-text);   background: var(--error-soft);   }
.inline-msg.is-warn    { color: var(--warning-text); background: var(--warning-soft); }
.inline-msg.is-info    { color: var(--info-text);    background: var(--info-soft);    }

8.11 Confirmation dialog

A short modal that stops a destructive action. Title states what's happening; body states the consequence. Primary button is the affirmative action, labelled as a verb — never just "OK".

Destructive confirm

.confirm

The primary button is pink for regular confirmations, red for destructive. Cancel is always ghost.

Delete this lead?

This will permanently remove Acme Corp and the 14 messages in this conversation. This can't be undone.

<div class="confirm" role="alertdialog"
     aria-labelledby="cf-title" aria-describedby="cf-body">
  <h3 class="confirm-title" id="cf-title">Delete this lead?</h3>
  <p class="confirm-body" id="cf-body">This can't be undone.</p>
  <div class="confirm-actions">
    <button class="btn btn-ghost">Cancel</button>
    <button class="btn confirm-danger">Delete lead</button>
  </div>
</div>
.confirm-backdrop { position: absolute; inset: 0;
  background: rgba(25,30,50,.35); backdrop-filter: blur(2px);
  border-radius: var(--r-md); }
.confirm { position: absolute; top: 50%; left: 50%;
  transform: translate(-50%, -50%);
  width: calc(100% - 40px); max-width: 380px;
  background: var(--bg-paper); border-radius: var(--r-lg);
  box-shadow: var(--sh-3); padding: var(--s-5); }
.confirm-title { font: 700 18px/1.25 var(--f-display); color: var(--fg); margin: 0 0 var(--s-2); }
.confirm-body  { font: 400 14px/1.55 var(--f-body); color: var(--fg-soft); margin: 0 0 var(--s-4); }
.confirm-actions { display: flex; justify-content: flex-end; gap: var(--s-2); }
.confirm-danger { background: var(--error); color: var(--paper); border: 0; }
.confirm-danger:hover { background: color-mix(in oklab, var(--error) 85%, black); }

8.12 Snackbar

Bottom-centred transient message with an optional action. More utilitarian than a toast — ink surface, single line, one action button. Commonly paired with an "Undo" for reversible commits.

Snackbar + undo

.snackbar

Dark surface, paper text. The action is styled as a pink link. Snackbars auto-dismiss after 5s unless hovered.

Lead "Acme Corp" archived.
<div class="snackbar" role="status">
  <span class="snack-body">Lead "Acme Corp" archived.</span>
  <button class="snack-action">Undo</button>
</div>
.snackbar { display: inline-flex; align-items: center; gap: var(--s-4);
  padding: 12px 16px; background: var(--ink); color: var(--paper);
  border-radius: var(--r-md); box-shadow: var(--sh-3);
  font: 400 14px/1 var(--f-body); }
.snack-body { color: rgba(255,255,255,.92); }
.snack-action { background: transparent; border: 0;
  color: var(--accent-text); font: 600 13px/1 var(--f-body);
  text-transform: uppercase; letter-spacing: 0.06em; cursor: pointer; }
.snack-action:hover { color: #FE84A9; }

8.13 Anatomy — toast

Every toast has exactly these five parts. Remove any of them and the component stops reading as 'a MagicBlocks notification' and starts reading as generic chrome.

Five required parts

Couldn't reach Twilio
Outbound SMS paused. View status
1
2
3
4
5
  1. 1
    Coloured left rail
    3px solid bar in the semantic colour — the only chroma signal on the surface.
  2. 2
    Tinted icon chip
    28px circle with a soft tint of the semantic colour and a stronger glyph tone.
  3. 3
    Title — 14px semibold display
    Bricolage 600, -0.005em tracking. Four to six words.
  4. 4
    Description + optional link
    13px body soft. Keep to one sentence. Action link in pink.
  5. 5
    Dismiss button
    Always dismissible for info/warn/success. Errors may auto-persist.

8.14 Bulk action bar

Sticky-bottom action bar that appears when one or more list rows or kanban cards are selected. Pinned to bottom-centre, ink surface, slides up + fades in. Pairs with a Snackbar (8.12) for "Undo" on destructive actions. Disabled actions stay visible but greyed; never hide an action you'd otherwise show.

Sticky bulk bar

.bulk-bar

Left: count + clear-selection. Centre: vertical divider. Right: action buttons. Show/hide with slide-up + fade. In real use, position fixed to viewport bottom with z-index: var(--z-sticky); here, contained in the demo stage.

↓ appears when rows are selected ↓
<div class="bulk-bar" role="toolbar" aria-label="Bulk actions">
  <span class="bb-count">7 selected</span>
  <button class="bb-clear" type="button">Clear</button>
  <span class="bb-spacer" aria-hidden="true"></span>
  <button class="bb-action" type="button">
    <svg ...>...</svg> <span>Assign</span>
  </button>
  <button class="bb-action" type="button">...</button>
  <button class="bb-action is-danger" type="button">...Delete</button>
</div>

<!-- In real use, this is fixed to viewport bottom: -->
<style>
  .bulk-bar.is-pinned {
    position: fixed; bottom: 24px; left: 50%;
    transform: translateX(-50%);
    z-index: var(--z-sticky);
    animation: bulk-bar-slide-up 0.18s var(--ease) backwards;
  }
  @keyframes bulk-bar-slide-up {
    from { opacity: 0; transform: translate(-50%, 8px); }
    to   { opacity: 1; transform: translate(-50%, 0); }
  }
  @media (prefers-reduced-motion: reduce) {
    .bulk-bar.is-pinned { animation: none; }
  }
</style>
.bulk-bar { display: flex; align-items: center; gap: var(--s-4);
  padding: 10px 16px; background: var(--ink); color: var(--paper);
  border-radius: var(--r-pill); box-shadow: var(--sh-3); }

.bulk-bar .bb-count { padding: 6px 12px;
  background: rgba(255,255,255,0.08); border-radius: var(--r-pill); }
.bulk-bar .bb-spacer { flex: 1; height: 18px;
  border-left: 1px solid rgba(244,236,228,0.18); margin-left: var(--s-2); }
.bulk-bar .bb-action { background: transparent; border: 0; cursor: pointer;
  padding: 8px 12px; border-radius: var(--r-pill); }
.bulk-bar .bb-action:hover { background: rgba(255,255,255,0.08); }
.bulk-bar .bb-action.is-danger { color: #FFB8B8; }

@media (max-width: 640px) {
  .bulk-bar .bb-action span { display: none; }  /* icon-only on mobile */
}