15.1 Sage AI drawer
A right-anchored AI assistant. 360px wide (resizable 320–520 via drag-handle). Header with the “Sage” Fraunces wordmark + context chip (“on this deal” / “everywhere”) + close (⌘.). Message stream below: assistant + user turns, plus tool-call cards (collapsed by default) and proposal cards (highlighted with --accent-soft + Accept / Edit / Dismiss). Composer at the bottom with starter-prompt chips when empty. Streaming dots while thinking. Reduced motion: no pulsing dots, replaces with “thinking…” text.
Conversation in flight
.sage-drawerShowing user message, assistant reply, expanded tool-call card, proposal card with action buttons, then streaming dots while a follow-up generates. In production the drawer is fixed to the right edge of the viewport at 100vh; the demo uses a constrained box to fit on the chapter page.
tool get_recent_activity(deal_id=42)
<aside class="sage-drawer">
<header class="sage-head">
<div class="sage-row1">
<span class="sage-wordmark">
<svg class="sage-spark">...</svg>Sage
</span>
<button class="sage-close">×</button>
</div>
<span class="sage-context">On this deal · BlueRock</span>
</header>
<div class="sage-stream">
<div class="sage-msg is-user">...</div>
<div class="sage-msg is-assistant">...</div>
<!-- Tool call (collapsed by default) -->
<details class="sage-tool-call">
<summary>tool · get_recent_activity(deal_id=42)</summary>
<div class="sage-tc-body">...result...</div>
</details>
<!-- Proposal card (highlighted; Accept/Edit/Dismiss) -->
<div class="sage-proposal">
<div class="sage-proposal-head">✦ Sage suggests</div>
<div class="sage-proposal-body">...</div>
<div class="sage-proposal-actions">
<button class="sage-prop-accept">Accept & draft</button>
<button class="sage-prop-edit">Edit prompt</button>
<button class="sage-prop-dismiss">Dismiss</button>
</div>
</div>
<!-- Streaming dots -->
<div class="sage-typing">
<span class="sage-typing-dot"></span>
<span class="sage-typing-dot"></span>
<span class="sage-typing-dot"></span>
</div>
</div>
<footer class="sage-composer">
<div class="sage-prompts">
<button class="sage-prompt-chip">Summarise this deal</button>
...
</div>
<div class="sage-input-row">
<textarea class="sage-input" placeholder="Ask anything"></textarea>
<button class="sage-send">Send</button>
</div>
</footer>
</aside>.sage-drawer { width: 360px; height: 100vh;
position: fixed; right: 0; top: 0;
background: var(--bg-paper); border-left: 1px solid var(--hair);
display: grid; grid-template-rows: auto 1fr auto; }
.sage-head { background: var(--gradient-glow-soft);
padding: var(--s-4) var(--s-5); border-bottom: 1px solid var(--hair); }
.sage-wordmark { font-family: var(--f-serif); font-style: italic;
font-size: 22px; font-variation-settings: "SOFT" 80; }
.sage-typing-dot { animation: sage-pulse 1.2s ease infinite; }
.sage-typing-dot:nth-child(2) { animation-delay: 0.2s; }
.sage-typing-dot:nth-child(3) { animation-delay: 0.4s; }
@media (prefers-reduced-motion: reduce) {
.sage-typing-dot { display: none; }
.sage-typing::after { content: "thinking…"; }
}
/* Resizable: drag the left edge between 320px and 520px */
.sage-drawer { resize: horizontal; min-width: 320px; max-width: 520px; }15.2 Compose drawer
The single email composer used everywhere we send mail. Right-side overlay 540px. Header: From (rep selector for admin), To/CC/BCC autocomplete, schedule pill, tracking toggle, expand-to-fullscreen icon. Subject input. Rich-text body with slash-commands palette, ;intro shortcuts, and {{ }} variable autocomplete. AI assist bar above the body: Make warmer · Shorter · More direct · Add ROI line · Custom prompt. Footer: Send · Schedule · Save snippet. Draft autosaves silently — pulse → check.
Composer mid-write
.composeShowing one recipient chip, schedule + tracking pills, AI assist bar, and a body with a variable token ({{first_name}}) that highlights as it's typed. The body is contenteditable in production; here it's static HTML for the demo.
New email
Hi {{first_name}},
Wanted to drop a friendly note about the renewal proposal we sent on Apr 18. Happy to walk through any questions on a quick 15-min sync this week if helpful — here's my calendar.
Either way, looking forward to closing this out together. The team at MagicBlocks is genuinely excited about the next year with you.
Best,
Jay
<aside class="compose">
<header class="compose-head">
<h4>New email</h4>
<div class="compose-head-actions">
<button class="compose-icon-btn">⛶</button> <!-- expand -->
<button class="compose-icon-btn">×</button> <!-- close -->
</div>
</header>
<div class="compose-headers">
<div class="compose-header-row">...From...</div>
<div class="compose-header-row">...To with chips...</div>
<div class="compose-header-row">...Cc / Bcc reveal...</div>
</div>
<div class="compose-meta-row">
<span class="compose-meta-pill">⏰ Schedule</span>
<span class="compose-meta-pill is-on">✓ Tracking on</span>
</div>
<input class="compose-subject" placeholder="Subject…">
<!-- AI assist bar -->
<div class="compose-ai-bar">
<span class="compose-ai-eyebrow">✦ Sage assist</span>
<button class="compose-ai-chip">Make warmer</button>
<button class="compose-ai-chip">Shorter</button>
...
</div>
<div class="compose-body" contenteditable="true">
<p>Hi <code class="var-tag">{{first_name}}</code>,</p>
...
</div>
<footer class="compose-foot">
<div class="compose-autosave">
<span class="compose-autosave-dot"></span>
Draft saved · 2s ago
</div>
<div class="compose-foot-actions">
<button>Save snippet</button>
<button>Schedule</button>
<button class="compose-send-btn">Send</button>
</div>
</footer>
</aside>.compose { width: 540px;
background: var(--bg-paper); border: 1px solid var(--hair);
border-radius: var(--r-lg); box-shadow: var(--sh-3);
display: grid; grid-template-rows: auto auto auto auto auto 1fr auto; }
.compose-ai-bar { background: var(--ai-glow-bg);
padding: var(--s-3) var(--s-4); }
.compose-ai-chip { background: var(--bg-paper);
border: 1px solid color-mix(in oklab, var(--accent) 20%, transparent);
border-radius: var(--r-pill); padding: 5px 10px;
font: 500 11px var(--f-body); color: var(--fg); }
.compose-body code.var-tag {
display: inline-block; padding: 1px 6px;
background: var(--accent-soft); color: var(--accent-text);
border-radius: var(--r-xs);
font: 500 12.5px var(--f-mono); }15.3 AI suggestion card
Sage's proactive surface inside detail pages. Card with a subtle gradient (using --ai-glow-bg), spark icon + “Sage suggests” eyebrow, one-paragraph suggestion, primary CTA + secondary “Edit” + dismiss. Three states: generating (shimmer), ready (default), dismissed (collapsed to a chip “Sage has 1 idea”).
Three states
.ai-cardIn the right rail of a detail page (13.1) it sits naturally above other context cards. The dismiss action shouldn't lose the suggestion — it collapses to a small chip the user can re-open.
Send the renewal proposal today. BlueRock's renewal date is in 23 days. The pattern from past closes: send proposal > 14 days out for a 70% acceptance rate.
<!-- Ready state -->
<div class="ai-card">
<div class="ai-card-head">
<span class="ai-card-eyebrow">✦ Sage suggests</span>
<button class="ai-card-dismiss">×</button>
</div>
<p class="ai-card-body">Send the renewal proposal <em>today</em>. BlueRock's renewal date is in 23 days...</p>
<div class="ai-card-actions">
<button class="ai-card-cta">Draft email</button>
<button class="ai-card-edit">Edit prompt</button>
</div>
</div>
<!-- Generating state -->
<div class="ai-card is-generating">...</div>
<!-- Dismissed → collapsed chip -->
<button class="ai-card-chip">✦ Sage has 1 idea</button>.ai-card { background: var(--bg-paper);
border: 1px solid color-mix(in oklab, var(--accent) 25%, var(--hair));
border-radius: var(--r-lg); padding: var(--s-4);
position: relative; overflow: hidden; }
.ai-card::before { content: ""; position: absolute; inset: 0;
background: var(--ai-glow-bg); /* dark mode: 1.5× opacity */
pointer-events: none; }
.ai-card > * { position: relative; z-index: 1; }
.ai-card.is-generating .ai-card-body { color: transparent; }
.ai-card.is-generating .ai-card-body::after {
content: "Sage is thinking…";
font: 400 13px var(--f-mono); color: var(--fg-dim); font-style: italic; }15.4 KB suggestion card
Sage's “assist” surface for support agents. Shows a relevant KB article with title, snippet (matching highlight), helpful/not-helpful counts, and three actions: Insert into reply, Open article, Mark not helpful. The matched search term is wrapped in a <mark> with --accent-soft.
Three suggestions for the current ticket
.kb-card-suggestStacks vertically in the right rail. The agent can insert any one with one click; helpful counts feed back into Sage's ranking model.
Configuring SAML SSO with Okta
If your customer is using Okta, navigate to Settings → Authentication and choose SAML SSO. The metadata URL we provide goes into Okta's app settings; we'll handle the rest…
Troubleshooting SAML signature mismatch
Most SAML errors come from a clock skew between the IdP and our service. Confirm both sides are syncing to NTP, then re-test…
<div class="kb-card-suggest">
<div class="kb-card-suggest-head">
<span class="kb-card-suggest-icon">...</span>
<div>
<h4 class="kb-card-suggest-title">Configuring SAML SSO</h4>
<div class="kb-card-suggest-meta">KB · 4 min · last updated Apr 12</div>
</div>
</div>
<p class="kb-card-suggest-snippet">
If your customer is using Okta… choose <mark>SAML SSO</mark> …
</p>
<div class="kb-card-suggest-foot">
<span class="kb-card-suggest-helpful">
<strong>92%</strong> helpful · 47 reads
</span>
<div class="kb-card-suggest-actions">
<button class="kb-action-primary">Insert into reply</button>
<button>Open</button>
</div>
</div>
</div>.kb-card-suggest { background: var(--bg-paper);
border: 1px solid var(--hair); border-radius: var(--r-md);
padding: var(--s-4); }
.kb-card-suggest-snippet mark {
background: var(--accent-soft); color: var(--fg);
padding: 1px 3px; border-radius: 2px; }
.kb-action-primary { background: var(--accent); color: var(--paper);
border-color: var(--accent); }15.5 Confetti win moment
Reserved for closed-won deals (and only those). Full-screen transparent overlay; ~30 small spans drift down from centre over 1.4s with brand-palette particle colours (pink, blue, warm, ink). Paired with a one-line toast (“🎉 Won — $36k ARR. Nice.”). Under reduced motion, particles are suppressed; the toast still fires.
Click to fire
.confetti-stage · MB.confetti.fire()In production this is a tiny JS helper (per spec § 27): 30 spans appended to body, CSS keyframes do the drift, helper removes them after 1.6s. The demo scopes it inside the stage so it doesn't take over the chapter page.
<!-- Markup is just the toast; particles are injected by JS -->
<div class="confetti-toast">
🎉 Won — <span class="em">$36k</span> ARR. Nice.
</div>
<!-- Helper (drop into _shared.js) -->
<script>
window.MB = window.MB || {};
MB.confetti = {
fire(opts = {}) {
const reduce = matchMedia('(prefers-reduced-motion: reduce)').matches;
if (reduce) return; // toast still fires; particles don't
const colors = ['var(--accent)', 'var(--info)', 'var(--warning)', 'var(--ink)'];
const host = opts.host || document.body;
for (let i = 0; i < 30; i++) {
const el = document.createElement('span');
el.className = 'confetti-particle';
el.style.background = colors[i % colors.length];
// Random end position (full-screen spread)
el.style.setProperty('--cx', `${(Math.random() * 600 - 300)}px`);
el.style.setProperty('--cr', `${Math.random() * 720 - 360}deg`);
el.style.animationDelay = `${Math.random() * 0.2}s`;
host.appendChild(el);
setTimeout(() => el.remove(), 1600);
}
}
};
</script>.confetti-particle {
position: absolute; top: 50%; left: 50%;
width: 8px; height: 12px;
background: var(--accent);
opacity: 0; pointer-events: none;
}
.confetti-stage.is-firing .confetti-particle {
animation: confetti-drift 1.4s var(--ease) forwards;
}
@keyframes confetti-drift {
0% { opacity: 0; transform: translate(-50%, -50%) rotate(0deg); }
10% { opacity: 1; }
100% { opacity: 0; transform: translate(var(--cx, 0), 250px) rotate(var(--cr, 360deg)); }
}
@media (prefers-reduced-motion: reduce) {
.confetti-stage.is-firing .confetti-particle { animation: none; opacity: 0; }
}