| name | keynot |
| description | Create polished, self-contained HTML slide presentations — no PowerPoint or Keynote needed. Use this skill whenever the user asks for a presentation, slide deck, slides, or one-pager that will be shown in a browser. Also trigger when the user says things like "make it a deck", "turn this into slides", "build a presentation from this content", or "I need something to present". Prefer this over PPTX when the user has not explicitly asked for a .pptx file — HTML decks are more flexible, more visually rich, and easier to iterate on. Always use this skill when the user provides a brand style guide (PDF, URL, or description) alongside a presentation request. Trigger even if the user only says "make me a deck" with no further detail — start with content questions. |
Keynot
A skill for building browser-based slide presentations as single, self-contained HTML files. Keyboard navigation, swipe, fullscreen, animated reveals, embedded images, brand-accurate design. No external dependencies required at runtime.
When to Use This Skill
- User asks for a "presentation", "deck", "slides", "one-pager to present"
- User has a brand style guide and wants slides that match it
- User wants to iterate on slides quickly in the browser
- User wants a file they can open anywhere without software
Prefer HTML over PPTX unless:
- User explicitly asks for
.pptx
- User needs to edit slides in PowerPoint
- User needs to share with someone who must edit in Office
Step 1: Extract the Brand Guide
If the user provides a brand PDF, URL, or describes their brand, extract these before writing any code. Be explicit and precise.
What to Extract
Colors
- Primary (dominant background/text color)
- Accent (calls to action, rules, highlights)
- Neutrals (background, cream, gray)
- Document exact hex codes — never approximate
Typography
- Serif font (display headings, pull quotes)
- Sans-serif font (body, labels, UI)
- If proprietary fonts are specified (e.g., ITC Stone Serif, Acumin), find the closest Google Fonts equivalent
- Document the pairing explicitly
Layout Language
- Dominant color usage ratio (e.g., "blue should always dominate, red as accent only")
- Typical layout patterns visible in the guide (split panels, rules, dividers, margins)
- Tone: formal/editorial, warm/approachable, minimal/dense
Logo and Mark Rules
- Whether the logo/wordmark may be reproduced
- Clear space requirements
- Approved color variants (full color, white, black)
- Note: for institutional brands (universities, corporations), do NOT reproduce trademarked logos — use a text-based wordmark instead
Example Brand Record
Once extracted, record the brand parameters in a compact block at the top of the deck's <style> so they're easy to audit and swap:
Primary Colors:
Primary: #HEX (dominant — backgrounds, headings)
Accent: #HEX (rules, CTAs, status — used sparingly)
Cream: #HEX (light slide backgrounds)
Usage Rule: One color dominates. Accent appears sparingly —
rules, call-to-action bars, status indicators.
Never use the accent color as a full background.
Typography:
Serif: <Brand serif> → <closest Google Font>
Sans-serif: <Brand sans-serif> → <closest Google Font>
Layout Language:
- Which colors own which regions (dark panels vs. light panels)
- Any spine bars, rules, or dividers the brand uses
- Layout motifs (split panels, stat columns, card grids)
- Tone: editorial / warm / minimal / dense
Logo Rule: For institutional or trademarked brands, do NOT
reproduce the logo. Use a text-based wordmark instead:
<div class="wordmark">
<div class="w-line-1">Organization</div>
<div class="w-line-2">Subtitle or parent entity</div>
</div>
Step 2: Deck Architecture
Every deck is a single HTML file. All assets (fonts via CDN, images via base64) are embedded.
Shell Structure
<!DOCTYPE html>
<html lang="en">
<head>
</head>
<body>
<div class="deck" id="deck">
<div class="slide" id="s1">...</div>
<div class="slide" id="s2">...</div>
</div>
<div class="nav">
<button id="prev">←</button>
<div class="dots" id="dots"></div>
<button id="next">→</button>
<span class="counter" id="counter">1 / N</span>
<button id="fsBtn" onclick="toggleFS()">⛶</button>
</div>
<div class="rotate-overlay" aria-hidden="true">
<svg class="rotate-icon" width="56" height="56" viewBox="0 0 24 24"
fill="none" stroke="currentColor" stroke-width="1.4"
stroke-linecap="round" stroke-linejoin="round">
<rect x="5" y="2" width="14" height="20" rx="2.5" />
<line x1="12" y1="18.5" x2="12.01" y2="18.5" />
</svg>
<h2>Rotate your device</h2>
<p>This deck is designed for landscape viewing. Turn your phone sideways to continue.</p>
</div>
<script></script>
</body>
</html>
Core CSS Patterns
:root {
--primary: #1a1a2e;
--accent: #e94560;
--white: #ffffff;
--cream: #f7f4ee;
--serif: 'Cormorant Garamond', Georgia, serif;
--sans: 'DM Sans', system-ui, sans-serif;
}
html, body { width: 100%; height: 100%; overflow: hidden; background: #000; }
.deck { width: 100vw; height: 100vh; position: relative; }
.slide {
position: absolute; inset: 0;
opacity: 0; pointer-events: none;
transition: opacity .55s cubic-bezier(.4,0,.2,1);
}
.slide.active { opacity: 1; pointer-events: all; }
.slide.active .reveal { animation: fadeUp .6s both; }
.slide.active .reveal:nth-child(2) { animation-delay: .1s; }
.slide.active .reveal:nth-child(3) { animation-delay: .2s; }
@keyframes fadeUp {
from { opacity: 0; transform: translateY(22px); }
to { opacity: 1; transform: translateY(0); }
}
Navigation CSS
.nav {
position: fixed; bottom: 28px; left: 50%;
transform: translateX(-50%);
z-index: 999;
display: flex; align-items: center; gap: 14px;
background: rgba(26,26,46,.75);
backdrop-filter: blur(10px);
padding: 9px 20px; border-radius: 40px;
border: 1px solid rgba(255,255,255,.14);
opacity: 1;
transition: opacity .4s ease;
}
.nav-hidden { opacity: 0; pointer-events: none; }
.nav button {
background: none; border: none; cursor: pointer;
color: rgba(255,255,255,.65); font-size: 16px;
width: 30px; height: 30px;
display: flex; align-items: center; justify-content: center;
border-radius: 50%;
transition: color .2s, background .2s;
}
.nav button:hover { color: #fff; background: rgba(255,255,255,.12); }
.dot {
width: 7px; height: 7px; border-radius: 50%;
background: rgba(255,255,255,.3); cursor: pointer;
transition: background .2s, transform .2s;
}
.dot.active { background: var(--accent); transform: scale(1.35); }
JavaScript (complete — do not modify arithmetic)
function toggleFS() {
if (!document.fullscreenElement) {
document.documentElement.requestFullscreen().catch(() => {});
document.getElementById('fsBtn').innerHTML = '✕';
} else {
document.exitFullscreen();
document.getElementById('fsBtn').innerHTML = '⛶';
}
}
document.addEventListener('fullscreenchange', () => {
if (!document.fullscreenElement)
document.getElementById('fsBtn').innerHTML = '⛶';
});
function parseHash() {
const m = location.hash.match(/^#slide-(\d+)$/);
if (!m) return -1;
const idx = parseInt(m[1], 10) - 1;
return (idx >= 0 && idx < document.querySelectorAll('.slide').length) ? idx : -1;
}
function initialSlide() {
const fromHash = parseHash();
if (fromHash >= 0) return fromHash;
const stored = sessionStorage.getItem('keynot-slide');
if (stored !== null) {
const idx = parseInt(stored, 10);
if (idx >= 0 && idx < document.querySelectorAll('.slide').length) return idx;
}
return 0;
}
function persistSlide(idx) {
sessionStorage.setItem('keynot-slide', idx);
history.replaceState(null, '', '#slide-' + (idx + 1));
}
const slides = document.querySelectorAll('.slide');
const dotsEl = document.getElementById('dots');
const counter = document.getElementById('counter');
let cur = initialSlide();
slides.forEach(s => s.classList.remove('active'));
slides.forEach((_, i) => {
const d = document.createElement('div');
d.className = 'dot';
d.addEventListener('click', () => go(i));
dotsEl.appendChild(d);
});
slides[cur].classList.add('active');
dotsEl.children[cur].classList.add('active');
counter.textContent = `${cur + 1} / ${slides.length}`;
persistSlide(cur);
function go(n) {
slides[cur].classList.remove('active');
dotsEl.children[cur].classList.remove('active');
cur = (n + slides.length) % slides.length;
slides[cur].classList.add('active');
dotsEl.children[cur].classList.add('active');
counter.textContent = `${cur + 1} / ${slides.length}`;
persistSlide(cur);
}
window.addEventListener('hashchange', () => {
const idx = parseHash();
if (idx >= 0 && idx !== cur) go(idx);
});
document.getElementById('next').addEventListener('click', () => go(cur + 1));
document.getElementById('prev').addEventListener('click', () => go(cur - 1));
document.addEventListener('keydown', e => {
if (e.key === 'ArrowRight' || e.key === 'ArrowDown' || e.key === ' ') go(cur + 1);
if (e.key === 'ArrowLeft' || e.key === 'ArrowUp') go(cur - 1);
if (e.key === 'f' || e.key === 'F') toggleFS();
});
const nav = document.querySelector('.nav');
let hideTimer;
function showNav() {
nav.classList.remove('nav-hidden');
clearTimeout(hideTimer);
hideTimer = setTimeout(() => nav.classList.add('nav-hidden'), 1800);
}
document.addEventListener('mousemove', showNav);
document.addEventListener('touchstart', showNav, { passive: true });
document.addEventListener('keydown', showNav);
showNav();
let tx = 0;
document.addEventListener('touchstart', e => { tx = e.touches[0].clientX; }, { passive: true });
document.addEventListener('touchend', e => {
const dx = e.changedTouches[0].clientX - tx;
if (Math.abs(dx) > 50) go(dx < 0 ? cur + 1 : cur - 1);
}, { passive: true });
Portrait / Mobile Handling
Slide decks are a landscape medium. Rather than reflowing split-panel layouts into unreadable mobile stacks, show a "rotate your device" overlay when the viewport is portrait + narrow. This covers iPhones held vertically without doubling the CSS for every layout pattern. Landscape phone viewing keeps working because clamp() font sizing already scales down.
Add this CSS to every deck, near the bottom of the <style> block so it overrides nothing:
.rotate-overlay {
display: none;
position: fixed; inset: 0;
z-index: 9999;
background: var(--primary);
color: var(--white);
flex-direction: column;
align-items: center; justify-content: center;
text-align: center; padding: 40px;
font-family: var(--sans);
}
.rotate-overlay .rotate-icon {
margin-bottom: 24px;
color: var(--accent);
animation: rotate-hint 2.4s ease-in-out infinite;
}
.rotate-overlay h2 {
font-family: var(--serif);
font-size: 30px; font-weight: 400;
margin-bottom: 12px; letter-spacing: -.01em;
}
.rotate-overlay p {
font-size: 14px; line-height: 1.6;
opacity: .72; max-width: 280px;
}
@keyframes rotate-hint {
0%, 40%, 100% { transform: rotate(0deg); }
60%, 80% { transform: rotate(90deg); }
}
@media (orientation: portrait) and (max-width: 900px) {
.rotate-overlay { display: flex; }
}
The overlay HTML is included in the Shell Structure above. Drop both in and the deck handles portrait phones gracefully without any per-slide changes.
Why not full mobile responsiveness? Split panels, stat columns, and photo backgrounds all rely on horizontal real estate to communicate. A reflowed mobile version is effectively a different deck — and a worse one. PowerPoint and Google Slides also fail on portrait mobile; the audience already accepts this for the format.
Print / PDF Export
Users should be able to hit Cmd+P → "Save as PDF" and get a clean one-slide-per-page PDF. The screen deck stacks slides absolutely and hides inactive ones via opacity: 0, so a naive print captures only the active slide. The fix: a @media print block that un-stacks every slide, kills animations, hides nav/overlays, and forces landscape page size.
Add this near the bottom of the <style> block:
@media print {
@page {
size: 1600px 1000px;
margin: 0;
}
html, body {
width: 1600px; height: auto;
overflow: visible !important;
background: #fff;
}
.deck {
width: 1600px; height: auto;
position: static !important;
}
.slide {
position: relative !important;
inset: auto !important;
width: 1600px; height: 1000px;
opacity: 1 !important;
pointer-events: auto !important;
page-break-after: always;
break-after: page;
transform: none !important;
}
*, *::before, *::after {
animation: none !important;
animation-duration: 0s !important;
animation-delay: 0s !important;
transition: none !important;
}
.slide .reveal {
opacity: 1 !important;
transform: none !important;
}
.nav, .rotate-overlay { display: none !important; }
* {
-webkit-print-color-adjust: exact !important;
print-color-adjust: exact !important;
}
}
Usage: user opens the deck in a browser, hits Cmd+P (or Ctrl+P), picks "Save as PDF" as the destination, and optionally toggles "Background graphics" on if the print dialog offers it. The default paper size in the dialog doesn't matter much — the @page rule sets the content frame, and browsers scale to fit. For pixel-perfect output, users can pick "Custom" paper size matching 1600 × 1000 in the dialog.
Gotchas:
- Gradients and some photo-panel rasterization can look slightly different from screen (Chrome and Safari differ). If the deck must look identical in print, keep backgrounds solid.
- Slides taller than 1000px will clip. Don't let content overflow the slide bounds on screen and it won't clip in print.
- Firefox honors
@page size: <pixels> less reliably than Chrome/Safari. For critical Firefox users, swap to size: 10in 6.25in (same ratio, physical units).
Step 3: Layout Patterns
Use these as building blocks. Mix and match per slide.
Split Panel (most versatile)
Dark panel left (brand primary color), light panel right (white or cream). Diagonal slice or hard edge between them.
<div class="slide" id="sN" style="display:flex;">
<div class="left-panel"></div>
<div class="right-panel"></div>
</div>
.left-panel {
width: 50%; background: var(--primary);
display: flex; flex-direction: column; justify-content: center;
padding: 72px 64px; position: relative; overflow: hidden;
}
.right-panel {
flex: 1;
display: flex; flex-direction: column; justify-content: center;
padding: 72px 56px 72px 64px; background: var(--cream);
}
Stat Column (credentials, metrics)
Right-side column of large serif numbers/words with small uppercase labels. Works over photo backgrounds.
<div class="stats-col">
<div class="stat">
<div class="stat-n">13+</div>
<div class="stat-l">Years at institution</div>
</div>
</div>
Value Cards (principles, pillars)
Left colored bar accent, title + description. One bar color per value for visual differentiation.
<div class="value-card">
<div class="value-bar" style="background:#2e7d32"></div>
<div>
<div class="value-title">Principle Name</div>
<div class="value-text">Description of the principle.</div>
</div>
</div>
Approach Rows (process, steps)
Large ghost number, vertical rule, then body content. Good for 3-step processes.
<div class="approach">
<div class="approach-num">01</div>
<div class="approach-body">
<div class="approach-title">Step Title</div>
<div class="approach-text">Detail text.</div>
<div class="approach-tags"><span>Tag 1</span><span>Tag 2</span></div>
</div>
</div>
Tool/Status Cards
Name + status badge + description. Status badge variants: live (green), in-progress (amber), planned (neutral).
<div class="tool-card">
<div class="tool-body">
<div class="tool-name">Tool Name</div>
<span class="status status-live">Live</span>
<div class="tool-desc">What it does and who it's for.</div>
</div>
</div>
Photo Background Panel
Embed image as base64, apply gradient overlay so text stays readable.
<div class="photo-panel" style="
position:absolute; top:0; right:0; bottom:0; width:42%;
background: url('data:image/png;base64,...') center/cover no-repeat;
z-index:1;
">
<div style="position:absolute;inset:0;
background:linear-gradient(to right, var(--primary) 0%, rgba(0,0,0,.55) 40%, rgba(0,0,0,.1) 100%);
"></div>
</div>
To embed an image as base64:
import base64
data = 'data:image/png;base64,' + base64.b64encode(open('image.png','rb').read()).decode()
Step 4: Content Slide Sequence
A strong 5-slide deck structure for an institutional introduction:
| Slide | Purpose | Layout |
|---|
| 1 | Who you are + elevator pitch | Full bleed dark + photo + stat column |
| 2 | Values / principles | Split panel + value cards |
| 3 | Approach / framework | Light background + approach rows |
| 4 | Tools / enablement | Dark background + two columns |
| 5 | Close + discussion opener | Split panel + photo background |
Adapt freely. The key is each slide has ONE job and ONE dominant visual idea.
Step 5: Typography Rules
.display {
font-family: var(--serif);
font-size: clamp(52px, 7vw, 88px);
font-weight: 300; line-height: .95;
letter-spacing: -.01em;
}
.heading {
font-family: var(--serif);
font-size: clamp(32px, 4vw, 50px);
font-weight: 400; line-height: 1.1;
}
.label {
font-family: var(--sans);
font-size: 10px; font-weight: 600;
letter-spacing: .2em; text-transform: uppercase;
}
.body {
font-family: var(--sans);
font-size: 13px; font-weight: 300; line-height: 1.65;
}
Critical Pitfalls
NEVER use blanket string/regex replacement on files containing JavaScript
This will corrupt arithmetic operators. Example of what goes wrong:
content = content.replace(' - ', ', ')
Safe approach: Use targeted str_replace with unique, specific strings. Always view the JS section after any automated replacement to verify arithmetic operators are intact.
Always declare const slides before using it
If inserting a new function before the navigation block, check that const slides = document.querySelectorAll('.slide') is still present and comes before slides.forEach(...).
str_replace requires unique strings
Before using str_replace, grep for the target string to confirm it appears exactly once. If it appears multiple times, add more context lines to make it unique.
Base64 images make files large but self-contained
A single high-res PNG can add 150-200KB to the file. This is fine for a presentation deck. Use cover sizing and center center positioning. Always add a gradient overlay div for text legibility.
Emoji in content
Avoid leading emoji in list items and card titles — they read as AI-generated and date quickly. If the user asks to remove emoji, use Python unicode replacement rather than sed:
import re
content = re.sub(r'[\U00002600-\U0001FAFF]+', '', content)
content = content.replace('\ufe0f', '')
Iterative Editing Workflow
When making changes after initial build:
- View the relevant section first —
view tool with line range
- Use
str_replace for surgical edits — never rewrite the whole file for small changes
- Verify JS after any automated text processing — check lines with
cur, dx, +, - operators
- Present fresh file after changes — user may be viewing cached version
When the user says things like "slide 3: change X" — make ONLY that change. Resist the urge to improve adjacent content.
Codex / API Usage Notes
This skill is compatible with Claude API (Codex) usage. Key considerations:
- The complete JS block (navigation + fullscreen) should be treated as a unit — include it verbatim and do not modify arithmetic
- When generating via API, stream the file to disk and open in browser for preview
- The base64 embedding approach works identically in API context
- CSS variables (
--primary, --accent, etc.) make theming programmatic — pass brand colors as parameters and substitute into the template
PPTX Export (PowerPoint)
When the user explicitly asks for a .pptx file, generate a Python script that uses python-pptx to build the deck programmatically. The user runs it with:
uv run --with python-pptx <script>.py
No system-wide installs required — uv handles the dependency inline.
When to use PPTX instead of HTML
- User explicitly asks for
.pptx or "PowerPoint"
- User needs recipients to edit slides in Office
- User says "I need to share this with someone who uses PowerPoint"
How it works
Same content extraction and brand logic as HTML — extract colors, fonts, layout intent from the prompt. Then emit a self-contained .py script instead of an .html file.
PPTX Generation Pattern
from pptx import Presentation
from pptx.util import Inches, Pt
from pptx.dml.color import RGBColor
from pptx.enum.text import PP_ALIGN, MSO_ANCHOR
from pptx.enum.shapes import MSO_SHAPE
PRIMARY = RGBColor(0x1A, 0x1A, 0x2E)
ACCENT = RGBColor(0xE9, 0x45, 0x60)
prs = Presentation()
prs.slide_width = Inches(16)
prs.slide_height = Inches(10)
blank_layout = prs.slide_layouts[6]
def set_slide_bg(slide, color):
fill = slide.background.fill
fill.solid()
fill.fore_color.rgb = color
def add_text_box(slide, left, top, width, height, text,
font_size=18, bold=False, italic=False,
color=RGBColor(0xFF,0xFF,0xFF), font_name="Arial"):
txBox = slide.shapes.add_textbox(left, top, width, height)
tf = txBox.text_frame
tf.word_wrap = True
p = tf.paragraphs[0]
p.text = text
p.font.size = Pt(font_size)
p.font.bold = bold
p.font.italic = italic
p.font.color.rgb = color
p.font.name = font_name
slide1 = prs.slides.add_slide(blank_layout)
set_slide_bg(slide1, PRIMARY)
add_text_box(slide1, Inches(0.8), Inches(3), Inches(10), Inches(3),
"Title Here", font_size=72, font_name="Georgia")
prs.save("output.pptx")
print("Saved: output.pptx")
Mapping HTML layouts to PPTX
| HTML Pattern | PPTX Equivalent |
|---|
| Split panel (dark left / light right) | Rectangle shape as left bg + text boxes on each side |
| Stat columns | Large-font text boxes with small label text boxes below |
| Value cards with colored bars | Thin rectangle shapes as accent bars + adjacent text boxes |
| Approach rows (numbered steps) | Large number text box + title/body text boxes offset right |
| Full-bleed background | set_slide_bg() with brand color |
Important caveats
Warn the user: PPTX output is an approximation of the HTML deck, not a pixel-perfect replica. python-pptx does not support CSS, gradients, clamp() scaling, or web fonts. The generated deck will:
- Use system fonts (Arial, Georgia) as fallbacks for Google Fonts
- Approximate gradients with solid colors
- Skip animations and reveals (static slides)
- Require manual touch-up for precise alignment
Always tell the user: "The .pptx is a solid starting point but may need minor adjustments in PowerPoint — check alignment and font rendering before presenting."
Font mapping
| HTML (Google Font) | PPTX fallback |
|---|
| Cormorant Garamond | Georgia |
| DM Sans | Arial |
| Playfair Display | Georgia |
| Inter | Arial |
| Any serif | Georgia or Times New Roman |
| Any sans-serif | Arial or Calibri |
Quick Reference: Palette Slots
Every deck fills these slots. Populate from the brand guide, then theme programmatically via CSS variables.
--primary Dominant backgrounds and dark panels
--accent Rules, spine bars, CTAs, status dots, hover states
--cream Light slide backgrounds (warmer than pure white)
--stone Dividers, subtle borders (one tint above cream)
--ink Body text on light backgrounds (softer than black)
--muted Labels, metadata, secondary text
--go Positive / live / green status
--warn Review / caution / amber status
--alt Fifth accent for value/pillar differentiation
Fill with real hex codes at the top of <style> and reference everywhere via var(--name).