This commit is contained in:
2026-04-12 01:06:31 +07:00
commit 10d660cbcb
1066 changed files with 228596 additions and 0 deletions

View File

@@ -0,0 +1,650 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Architecture Diagram — Reference Template</title>
<!--
Reference template for the ck:preview skill: CSS Grid architecture layout.
Warm terracotta/sage palette — distinctly different from the teal (mermaid)
and rose (data-table) templates so agents absorb variety, not a single palette.
Key patterns demonstrated:
- Warm non-default palette (terracotta + sage, NOT indigo/violet)
- Depth tiers: hero (input sources), default (mid sections), recessed (callout)
- Asymmetric background atmosphere (off-center gradient mesh)
- Large heading (38px) for typographic contrast
- Section cards with colored accent borders and dot labels
- Vertical flow arrows between sections (inline SVG)
- Horizontal pipeline with step boxes and arrow separators
- Parallel branch within a pipeline
- Color-coded legend, three-column output row
- Staggered fade-in via --i CSS variable (works with interleaved elements)
- Reduced motion respect, responsive single-column fallback
-->
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:wght@400;500;600&family=IBM+Plex+Sans:wght@400;500;600;700&display=swap" rel="stylesheet">
<style>
/* ============ THEME ============ */
:root {
--font-body: 'IBM Plex Sans', system-ui, sans-serif;
--font-mono: 'IBM Plex Mono', 'SF Mono', Consolas, monospace;
/* Light theme — warm terracotta + sage palette */
--bg: #faf7f5;
--surface: #ffffff;
--surface2: #f5f0ec;
--surface-elevated: #fff9f5;
--border: rgba(0, 0, 0, 0.07);
--border-bright: rgba(0, 0, 0, 0.14);
--text: #292017;
--text-dim: #8a7e72;
--accent: #c2410c;
--accent-dim: rgba(194, 65, 12, 0.07);
--green: #4d7c0f;
--green-dim: rgba(77, 124, 15, 0.07);
--orange: #b45309;
--orange-dim: rgba(180, 83, 9, 0.07);
--sage: #65a30d;
--sage-dim: rgba(101, 163, 13, 0.07);
--teal: #0f766e;
--teal-dim: rgba(15, 118, 110, 0.07);
--plum: #9f1239;
--plum-dim: rgba(159, 18, 57, 0.07);
}
@media (prefers-color-scheme: dark) {
:root:not([data-theme="light"]) {
--bg: #1a1412;
--surface: #231d1a;
--surface2: #2e2622;
--surface-elevated: #352d28;
--border: rgba(255, 255, 255, 0.06);
--border-bright: rgba(255, 255, 255, 0.12);
--text: #ede5dd;
--text-dim: #a69889;
--accent: #fb923c;
--accent-dim: rgba(251, 146, 60, 0.12);
--green: #a3e635;
--green-dim: rgba(163, 230, 53, 0.1);
--orange: #fbbf24;
--orange-dim: rgba(251, 191, 36, 0.1);
--sage: #bef264;
--sage-dim: rgba(190, 242, 100, 0.1);
--teal: #5eead4;
--teal-dim: rgba(94, 234, 212, 0.1);
--plum: #fda4af;
--plum-dim: rgba(253, 164, 175, 0.1);
}
}
/* ── Dark (manual toggle override) ── */
[data-theme="dark"] {
--bg: #1a1412;
--surface: #231d1a;
--surface2: #2e2622;
--surface-elevated: #352d28;
--border: rgba(255, 255, 255, 0.06);
--border-bright: rgba(255, 255, 255, 0.12);
--text: #ede5dd;
--text-dim: #a69889;
--accent: #fb923c;
--accent-dim: rgba(251, 146, 60, 0.12);
--green: #a3e635;
--green-dim: rgba(163, 230, 53, 0.1);
--orange: #fbbf24;
--orange-dim: rgba(251, 191, 36, 0.1);
--sage: #bef264;
--sage-dim: rgba(190, 242, 100, 0.1);
--teal: #5eead4;
--teal-dim: rgba(94, 234, 212, 0.1);
--plum: #fda4af;
--plum-dim: rgba(253, 164, 175, 0.1);
}
/* ============ THEME TOGGLE ============ */
.theme-toggle {
position: fixed; top: 16px; right: 16px; z-index: 300;
width: 36px; height: 36px; border-radius: 8px;
border: 1px solid var(--border); background: var(--surface);
color: var(--text-dim); cursor: pointer;
display: flex; align-items: center; justify-content: center;
font-size: 16px; transition: background 0.15s, color 0.15s;
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.06);
}
.theme-toggle:hover { background: var(--surface2); color: var(--text); }
/* ============ RESET + BASE ============ */
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
background: var(--bg);
background-image:
radial-gradient(ellipse at 20% 0%, var(--accent-dim) 0%, transparent 50%),
radial-gradient(ellipse at 80% 100%, var(--sage-dim) 0%, transparent 40%);
color: var(--text);
font-family: var(--font-body);
padding: 40px;
min-height: 100vh;
}
/* ============ ANIMATION ============ */
@keyframes fadeUp {
from { opacity: 0; transform: translateY(12px); }
to { opacity: 1; transform: translateY(0); }
}
.section, .flow-arrow {
animation: fadeUp 0.4s ease-out both;
animation-delay: calc(var(--i, 0) * 0.06s);
}
@media (prefers-reduced-motion: reduce) {
*, *::before, *::after {
animation-duration: 0.01ms !important;
animation-delay: 0ms !important;
transition-duration: 0.01ms !important;
}
}
/* ============ TYPOGRAPHY ============ */
h1 {
font-size: 38px;
font-weight: 700;
letter-spacing: -1px;
margin-bottom: 6px;
text-wrap: balance;
}
.subtitle {
color: var(--text-dim);
font-size: 14px;
margin-bottom: 40px;
font-family: var(--font-mono);
}
/* ============ LAYOUT ============ */
.diagram {
display: grid;
grid-template-columns: 1fr;
gap: 24px;
max-width: 1100px;
margin: 0 auto;
}
/* ============ SECTION CARD ============ */
.section {
background: var(--surface);
border: 1px solid var(--border);
border-radius: 12px;
padding: 20px 24px;
}
.section--hero {
background: var(--surface-elevated);
border-color: color-mix(in srgb, var(--border) 50%, var(--accent) 50%);
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.06);
padding: 28px 32px;
}
.section--recessed {
background: var(--surface2);
box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.04);
}
.section-label {
font-family: var(--font-mono);
font-size: 11px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 1.5px;
margin-bottom: 16px;
display: flex;
align-items: center;
gap: 8px;
}
.section-label .dot {
width: 8px;
height: 8px;
border-radius: 50%;
display: inline-block;
}
/* Color variants */
.section--accent { border-color: var(--accent-dim); }
.section--accent .section-label { color: var(--accent); }
.section--accent .section-label .dot { background: var(--accent); }
.section--green { border-color: var(--green-dim); }
.section--green .section-label { color: var(--green); }
.section--green .section-label .dot { background: var(--green); }
.section--orange { border-color: var(--orange-dim); }
.section--orange .section-label { color: var(--orange); }
.section--orange .section-label .dot { background: var(--orange); }
.section--sage { border-color: var(--sage-dim); }
.section--sage .section-label { color: var(--sage); }
.section--sage .section-label .dot { background: var(--sage); }
.section--teal { border-color: var(--teal-dim); }
.section--teal .section-label { color: var(--teal); }
.section--teal .section-label .dot { background: var(--teal); }
.section--plum { border-color: var(--plum-dim); }
.section--plum .section-label { color: var(--plum); }
.section--plum .section-label .dot { background: var(--plum); }
/* ============ INNER GRID ============ */
.inner-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 12px;
}
.inner-card {
background: var(--surface2);
border: 1px solid var(--border);
border-radius: 8px;
padding: 12px 16px;
}
.inner-card .title {
font-weight: 600;
font-size: 13px;
margin-bottom: 4px;
}
.inner-card .desc {
color: var(--text-dim);
font-size: 12px;
line-height: 1.5;
}
.inner-card code {
font-family: var(--font-mono);
font-size: 11px;
background: var(--accent-dim);
color: var(--accent);
padding: 1px 5px;
border-radius: 3px;
}
/* ============ FLOW ARROW ============ */
.flow-arrow {
display: flex;
justify-content: center;
align-items: center;
gap: 8px;
color: var(--text-dim);
font-family: var(--font-mono);
font-size: 12px;
padding: 4px 0;
}
.flow-arrow svg {
width: 20px;
height: 20px;
fill: none;
stroke: var(--border-bright);
stroke-width: 2;
stroke-linecap: round;
stroke-linejoin: round;
}
/* ============ PIPELINE ============ */
.pipeline {
display: flex;
gap: 0;
align-items: stretch;
overflow-x: auto;
padding-bottom: 4px;
}
.pipeline-step {
background: var(--surface);
border: 1px solid var(--border);
border-radius: 8px;
padding: 10px 12px;
min-width: 120px;
flex-shrink: 0;
text-align: center;
}
.pipeline-step .step-num {
font-family: var(--font-mono);
font-size: 10px;
font-weight: 600;
margin-bottom: 4px;
}
.pipeline-step .step-name {
font-size: 12px;
font-weight: 600;
margin-bottom: 3px;
}
.pipeline-step .step-detail {
font-size: 10px;
color: var(--text-dim);
line-height: 1.4;
}
/* Step color variants */
.pipeline-step--teal { border-color: var(--teal-dim); }
.pipeline-step--teal .step-num { color: var(--teal); }
.pipeline-step--sage { border-color: var(--sage-dim); }
.pipeline-step--sage .step-num { color: var(--sage); }
.pipeline-step--orange { border-color: var(--orange-dim); }
.pipeline-step--orange .step-num { color: var(--orange); }
.pipeline-step--green { border-color: var(--green-dim); }
.pipeline-step--green .step-num { color: var(--green); }
.pipeline-arrow {
display: flex;
align-items: center;
padding: 0 2px;
color: var(--border-bright);
font-size: 16px;
flex-shrink: 0;
}
.pipeline-parallel {
display: flex;
flex-direction: column;
gap: 4px;
}
/* ============ THREE COLUMN ROW ============ */
.three-col {
display: grid;
grid-template-columns: 1fr 1fr 1fr;
gap: 16px;
}
/* ============ LIST ============ */
.node-list {
list-style: none;
font-size: 12px;
line-height: 1.8;
}
.node-list li {
padding-left: 14px;
position: relative;
}
.node-list li::before {
content: '';
color: var(--text-dim);
font-weight: 600;
position: absolute;
left: 0;
}
.node-list code {
font-family: var(--font-mono);
font-size: 11px;
background: var(--accent-dim);
color: var(--accent);
padding: 1px 5px;
border-radius: 3px;
}
/* ============ CALLOUT ============ */
.callout {
background: var(--surface2);
border: 1px solid var(--border);
border-left: 3px solid var(--accent);
border-radius: 0 8px 8px 0;
padding: 14px 18px;
font-size: 13px;
line-height: 1.6;
color: var(--text-dim);
}
.callout strong { color: var(--text); font-weight: 600; }
.callout code {
font-family: var(--font-mono);
font-size: 11px;
background: var(--accent-dim);
color: var(--accent);
padding: 1px 5px;
border-radius: 3px;
}
/* ============ LEGEND ============ */
.legend {
display: flex;
gap: 20px;
flex-wrap: wrap;
}
.legend-item {
display: flex;
align-items: center;
gap: 6px;
font-size: 11px;
color: var(--text-dim);
font-family: var(--font-mono);
}
.legend-swatch {
width: 12px;
height: 12px;
border-radius: 3px;
}
/* ============ SOURCE PILLS ============ */
.sources {
display: flex;
gap: 12px;
flex-wrap: wrap;
justify-content: center;
}
.source {
background: var(--surface2);
border: 1px solid var(--border);
border-radius: 8px;
padding: 10px 18px;
font-family: var(--font-mono);
font-size: 13px;
font-weight: 500;
display: flex;
align-items: center;
gap: 8px;
transition: border-color 0.2s;
}
.source:hover { border-color: var(--border-bright); }
/* ============ RESPONSIVE ============ */
@media (max-width: 768px) {
body { padding: 20px; }
.inner-grid { grid-template-columns: 1fr; }
.three-col { grid-template-columns: 1fr; }
.pipeline { flex-wrap: wrap; gap: 6px; }
.pipeline-arrow { display: none; }
}
</style>
</head>
<body>
<button class="theme-toggle" id="themeToggle" title="Toggle theme" aria-label="Toggle light/dark theme"></button>
<script>
(function() {
var t = document.getElementById('themeToggle');
var s = localStorage.getItem('theme');
var i = s || (matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light');
if (s) document.documentElement.setAttribute('data-theme', i);
t.textContent = i === 'dark' ? '\u2600' : '\u263E';
t.addEventListener('click', function() {
var c = document.documentElement.getAttribute('data-theme')
|| (matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light');
var n = c === 'light' ? 'dark' : 'light';
document.documentElement.setAttribute('data-theme', n);
localStorage.setItem('theme', n);
t.textContent = n === 'dark' ? '\u2600' : '\u263E';
});
})();
</script>
<h1>System Architecture</h1>
<p class="subtitle">reference template &mdash; architecture diagram pattern</p>
<div class="diagram">
<!-- Source pills row — hero depth for the entry point -->
<div class="section section--hero" style="--i:0">
<div class="section-label"><span class="dot" style="background:var(--text-dim)"></span> Input Sources</div>
<div class="sources">
<div class="source"><span>💬</span> Slack</div>
<div class="source"><span>🐙</span> GitHub</div>
<div class="source"><span>📧</span> Email</div>
</div>
</div>
<!-- Flow arrow -->
<div class="flow-arrow" style="--i:1">
<svg viewBox="0 0 20 20"><path d="M10 4 L10 16 M6 12 L10 16 L14 12"/></svg>
incoming events
</div>
<!-- Gateway section with inner grid -->
<div class="section section--accent" style="--i:2">
<div class="section-label"><span class="dot"></span> Gateway Layer</div>
<div class="inner-grid">
<div class="inner-card">
<div class="title">Router</div>
<div class="desc">Routes messages to agents via <code>resolveRoute()</code></div>
</div>
<div class="inner-card">
<div class="title">HTTP Server</div>
<div class="desc">Plugin routes + handlers for webhooks and API</div>
</div>
</div>
</div>
<div class="flow-arrow" style="--i:3">
<svg viewBox="0 0 20 20"><path d="M10 4 L10 16 M6 12 L10 16 L14 12"/></svg>
pipeline entry
</div>
<!-- Pipeline section -->
<div class="section section--green" style="--i:4">
<div class="section-label"><span class="dot"></span> Processing Pipeline</div>
<div class="legend" style="margin-bottom:14px">
<div class="legend-item"><div class="legend-swatch" style="background:var(--teal-dim);border:1px solid var(--teal)"></div>no LLM</div>
<div class="legend-item"><div class="legend-swatch" style="background:var(--sage-dim);border:1px solid var(--sage)"></div>LLM call</div>
<div class="legend-item"><div class="legend-swatch" style="background:var(--orange-dim);border:1px solid var(--orange)"></div>embedding</div>
<div class="legend-item"><div class="legend-swatch" style="background:var(--green-dim);border:1px solid var(--green)"></div>DB write</div>
</div>
<div class="pipeline">
<div class="pipeline-step pipeline-step--teal">
<div class="step-num">STEP 0</div>
<div class="step-name">Pre-filter</div>
<div class="step-detail">Allowlist, bots,<br>dedup check</div>
</div>
<div class="pipeline-arrow"></div>
<div class="pipeline-step pipeline-step--sage">
<div class="step-num">STEP 1</div>
<div class="step-name">Relevance</div>
<div class="step-detail">Cheap LLM<br>boolean check</div>
</div>
<div class="pipeline-arrow"></div>
<div class="pipeline-step pipeline-step--sage">
<div class="step-num">STEP 2</div>
<div class="step-name">Classify</div>
<div class="step-detail">JSON schema<br>validated output</div>
</div>
<div class="pipeline-arrow"></div>
<div class="pipeline-parallel">
<div class="pipeline-step pipeline-step--orange">
<div class="step-num">STEP 3</div>
<div class="step-name">Embed</div>
<div class="step-detail">Vector embedding</div>
</div>
<div class="pipeline-step pipeline-step--teal">
<div class="step-num">STEP 5</div>
<div class="step-name">Enrich</div>
<div class="step-detail">User resolution</div>
</div>
</div>
<div class="pipeline-arrow"></div>
<div class="pipeline-step pipeline-step--green">
<div class="step-num">STEP 4</div>
<div class="step-name">Cluster</div>
<div class="step-detail">Cosine similarity<br>+ INSERT</div>
</div>
</div>
</div>
<div class="flow-arrow" style="--i:5">
<svg viewBox="0 0 20 20"><path d="M10 4 L10 16 M6 12 L10 16 L14 12"/></svg>
stored and queryable
</div>
<!-- Database section -->
<div class="section section--orange" style="--i:6">
<div class="section-label"><span class="dot"></span> Database</div>
<div class="inner-grid">
<div class="inner-card">
<div class="title">feedback_items</div>
<div class="desc">Classification, embedding, cluster assignment, source dedup</div>
</div>
<div class="inner-card">
<div class="title">clusters</div>
<div class="desc">Centroid vectors, trends, ticket links, severity rollup</div>
</div>
</div>
</div>
<div class="flow-arrow" style="--i:7">
<svg viewBox="0 0 20 20"><path d="M10 4 L10 16 M6 12 L10 16 L14 12"/></svg>
consumed by
</div>
<!-- Three column output -->
<div class="three-col">
<div class="section section--sage" style="--i:8">
<div class="section-label"><span class="dot"></span> Agent Tools</div>
<ul class="node-list">
<li><code>search</code> semantic vector search</li>
<li><code>clusters</code> browse and filter</li>
<li><code>stats</code> aggregate metrics</li>
</ul>
</div>
<div class="section section--plum" style="--i:9">
<div class="section-label"><span class="dot"></span> Actions</div>
<ul class="node-list">
<li>Create tickets from clusters</li>
<li>Notify customers on ship</li>
<li>Generate release notes</li>
</ul>
</div>
<div class="section section--teal" style="--i:10">
<div class="section-label"><span class="dot"></span> Dashboard</div>
<ul class="node-list">
<li>Metrics overview</li>
<li>Feedback stream</li>
<li>NL chat interface</li>
</ul>
</div>
</div>
<!-- Callout — recessed depth for secondary info -->
<div class="callout section section--recessed" style="--i:11">
<strong>Multi-tenant</strong> &mdash; Each agent gets an isolated database at
<code>{agentDir}/intelligence/feedback.db</code> with per-agent config overlay
and credential isolation.
</div>
</div>
</body>
</html>

View File

@@ -0,0 +1,590 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Requirements Audit — Reference Template</title>
<!--
Reference template for the ck:preview skill: data tables.
Rose/cranberry palette — distinctly different from terracotta (architecture)
and teal (mermaid) templates so agents absorb variety.
Key patterns demonstrated:
- Rose/cranberry palette (NOT indigo/violet)
- KPI summary cards above the table (visual hook before the data)
- Collapsible <details> section (replaces static callout)
- Depth tiers: elevated KPIs, default table, collapsible for secondary
- Real <table> element with sticky header, alternating rows
- Status indicator badges (match, gap, partial)
- Text wrapping in wide columns, code references in cells
- Summary footer row with aggregate status
- Staggered row animation via --i variable
- Both light and dark themes via prefers-color-scheme
- Responsive horizontal scroll wrapper
-->
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Instrument+Serif&family=JetBrains+Mono:wght@400;500;600&display=swap" rel="stylesheet">
<style>
/* ============ THEME ============ */
:root {
--font-body: 'Instrument Serif', 'Georgia', serif;
--font-mono: 'JetBrains Mono', 'SF Mono', Consolas, monospace;
--font-sans: system-ui, -apple-system, sans-serif;
--bg: #fff5f5;
--surface: #ffffff;
--surface2: #fef0ee;
--surface-elevated: #fff8f7;
--border: rgba(0, 0, 0, 0.07);
--border-bright: rgba(0, 0, 0, 0.14);
--text: #1c1917;
--text-dim: #78716c;
--accent: #be123c;
--accent-dim: rgba(190, 18, 60, 0.06);
--green: #16a34a;
--green-dim: rgba(22, 163, 74, 0.08);
--red: #dc2626;
--red-dim: rgba(220, 38, 38, 0.08);
--orange: #d97706;
--orange-dim: rgba(217, 119, 6, 0.08);
}
@media (prefers-color-scheme: dark) {
:root:not([data-theme="light"]) {
--bg: #1a0a0a;
--surface: #231414;
--surface2: #2e1b1b;
--surface-elevated: #351f1f;
--border: rgba(255, 255, 255, 0.06);
--border-bright: rgba(255, 255, 255, 0.12);
--text: #fde2e2;
--text-dim: #c9a3a3;
--accent: #fb7185;
--accent-dim: rgba(251, 113, 133, 0.12);
--green: #4ade80;
--green-dim: rgba(74, 222, 128, 0.1);
--red: #f87171;
--red-dim: rgba(248, 113, 113, 0.1);
--orange: #fbbf24;
--orange-dim: rgba(251, 191, 36, 0.1);
}
}
/* ── Dark (manual toggle override) ── */
[data-theme="dark"] {
--bg: #1a0a0a;
--surface: #231414;
--surface2: #2e1b1b;
--surface-elevated: #351f1f;
--border: rgba(255, 255, 255, 0.06);
--border-bright: rgba(255, 255, 255, 0.12);
--text: #fde2e2;
--text-dim: #c9a3a3;
--accent: #fb7185;
--accent-dim: rgba(251, 113, 133, 0.12);
--green: #4ade80;
--green-dim: rgba(74, 222, 128, 0.1);
--red: #f87171;
--red-dim: rgba(248, 113, 113, 0.1);
--orange: #fbbf24;
--orange-dim: rgba(251, 191, 36, 0.1);
}
/* ============ THEME TOGGLE ============ */
.theme-toggle {
position: fixed; top: 16px; right: 16px; z-index: 300;
width: 36px; height: 36px; border-radius: 8px;
border: 1px solid var(--border); background: var(--surface);
color: var(--text-dim); cursor: pointer;
display: flex; align-items: center; justify-content: center;
font-size: 16px; transition: background 0.15s, color 0.15s;
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.06);
}
.theme-toggle:hover { background: var(--surface2); color: var(--text); }
/* ============ RESET + BASE ============ */
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
background: var(--bg);
background-image:
radial-gradient(ellipse at 30% 0%, var(--accent-dim) 0%, transparent 50%),
radial-gradient(ellipse at 70% 100%, var(--green-dim) 0%, transparent 40%);
color: var(--text);
font-family: var(--font-sans);
padding: 40px;
min-height: 100vh;
}
/* ============ ANIMATION ============ */
@keyframes fadeUp {
from { opacity: 0; transform: translateY(10px); }
to { opacity: 1; transform: translateY(0); }
}
.animate {
animation: fadeUp 0.35s ease-out both;
animation-delay: calc(var(--i, 0) * 0.04s);
}
@media (prefers-reduced-motion: reduce) {
*, *::before, *::after {
animation-duration: 0.01ms !important;
animation-delay: 0ms !important;
transition-duration: 0.01ms !important;
}
}
/* ============ CONTAINER ============ */
.container {
max-width: 1000px;
margin: 0 auto;
}
/* ============ HEADER ============ */
h1 {
font-family: var(--font-body);
font-size: 32px;
font-weight: 400;
letter-spacing: -0.3px;
margin-bottom: 6px;
}
.subtitle {
color: var(--text-dim);
font-family: var(--font-mono);
font-size: 12px;
margin-bottom: 32px;
}
/* ============ LEGEND ============ */
.legend {
display: flex;
gap: 16px;
flex-wrap: wrap;
margin-bottom: 20px;
}
.legend-item {
display: flex;
align-items: center;
gap: 6px;
font-family: var(--font-mono);
font-size: 11px;
color: var(--text-dim);
}
.legend-swatch {
width: 10px;
height: 10px;
border-radius: 3px;
}
/* ============ TABLE WRAPPER ============ */
.table-wrap {
background: var(--surface);
border: 1px solid var(--border);
border-radius: 12px;
overflow: hidden;
margin-bottom: 24px;
}
.table-scroll {
overflow-x: auto;
-webkit-overflow-scrolling: touch;
}
/* ============ TABLE ============ */
.data-table {
width: 100%;
border-collapse: collapse;
font-size: 13px;
line-height: 1.55;
}
/* Header */
.data-table thead {
position: sticky;
top: 0;
z-index: 2;
}
.data-table th {
background: var(--surface2);
font-family: var(--font-mono);
font-size: 10px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 1.2px;
color: var(--text-dim);
text-align: left;
padding: 14px 16px;
border-bottom: 2px solid var(--border-bright);
white-space: nowrap;
}
/* Cells */
.data-table td {
padding: 14px 16px;
border-bottom: 1px solid var(--border);
vertical-align: top;
}
.data-table tbody tr:last-child td {
border-bottom: none;
}
/* Wide text column */
.data-table .wide {
min-width: 220px;
max-width: 440px;
}
/* Alternating rows */
.data-table tbody tr:nth-child(even) {
background: var(--accent-dim);
}
/* Row hover + animation stagger */
.data-table tbody tr {
transition: background 0.15s ease;
animation: fadeUp 0.35s ease-out both;
animation-delay: calc(var(--i, 0) * 0.04s);
}
.data-table tbody tr:hover {
background: var(--border);
}
/* Code in cells */
.data-table code {
font-family: var(--font-mono);
font-size: 11px;
background: var(--accent-dim);
color: var(--accent);
padding: 1px 5px;
border-radius: 3px;
}
/* Secondary text */
.data-table small {
display: block;
color: var(--text-dim);
font-size: 11px;
margin-top: 3px;
}
/* ============ STATUS BADGES ============ */
.status {
display: inline-flex;
align-items: center;
gap: 5px;
font-family: var(--font-mono);
font-size: 10px;
font-weight: 600;
padding: 3px 10px;
border-radius: 6px;
white-space: nowrap;
letter-spacing: 0.3px;
}
.status--match {
background: var(--green-dim);
color: var(--green);
}
.status--gap {
background: var(--red-dim);
color: var(--red);
}
.status--partial {
background: var(--orange-dim);
color: var(--orange);
}
/* Dot before status text */
.status::before {
content: '';
width: 6px;
height: 6px;
border-radius: 50%;
background: currentColor;
}
/* ============ FOOTER ROW ============ */
.data-table tfoot td {
background: var(--surface2);
font-weight: 600;
font-family: var(--font-mono);
font-size: 12px;
border-top: 2px solid var(--border-bright);
border-bottom: none;
padding: 14px 16px;
}
/* ============ KPI SUMMARY ============ */
.kpi-row {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
gap: 14px;
margin-bottom: 20px;
}
.kpi-card {
background: var(--surface-elevated, var(--surface));
border: 1px solid var(--border);
border-radius: 10px;
padding: 18px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
}
.kpi-card__value {
font-family: var(--font-body);
font-size: 32px;
font-weight: 400;
line-height: 1.1;
font-variant-numeric: tabular-nums;
}
.kpi-card__value--green { color: var(--green); }
.kpi-card__value--red { color: var(--red); }
.kpi-card__value--orange { color: var(--orange); }
.kpi-card__label {
font-family: var(--font-mono);
font-size: 10px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 1.2px;
color: var(--text-dim);
margin-top: 6px;
}
/* ============ COLLAPSIBLE ============ */
details.collapsible {
border: 1px solid var(--border);
border-radius: 10px;
overflow: hidden;
}
details.collapsible summary {
padding: 14px 20px;
background: var(--surface);
font-family: var(--font-mono);
font-size: 12px;
font-weight: 600;
cursor: pointer;
list-style: none;
display: flex;
align-items: center;
gap: 8px;
color: var(--text);
transition: background 0.15s ease;
}
details.collapsible summary:hover {
background: var(--surface-elevated, var(--surface));
}
details.collapsible summary::-webkit-details-marker { display: none; }
details.collapsible summary::before {
content: '▸';
font-size: 11px;
color: var(--text-dim);
transition: transform 0.15s ease;
}
details.collapsible[open] summary::before {
transform: rotate(90deg);
}
details.collapsible .collapsible__body {
padding: 16px 20px;
border-top: 1px solid var(--border);
font-size: 13px;
line-height: 1.6;
color: var(--text-dim);
}
details.collapsible .collapsible__body strong {
color: var(--text);
font-weight: 600;
}
details.collapsible .collapsible__body code {
font-family: var(--font-mono);
font-size: 11px;
background: var(--accent-dim);
color: var(--accent);
padding: 1px 5px;
border-radius: 3px;
}
/* ============ RESPONSIVE ============ */
@media (max-width: 768px) {
body { padding: 16px; }
h1 { font-size: 24px; }
.data-table th,
.data-table td { padding: 10px 12px; }
}
</style>
</head>
<body>
<button class="theme-toggle" id="themeToggle" title="Toggle theme" aria-label="Toggle light/dark theme"></button>
<script>
(function() {
var t = document.getElementById('themeToggle');
var s = localStorage.getItem('theme');
var i = s || (matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light');
if (s) document.documentElement.setAttribute('data-theme', i);
t.textContent = i === 'dark' ? '\u2600' : '\u263E';
t.addEventListener('click', function() {
var c = document.documentElement.getAttribute('data-theme')
|| (matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light');
var n = c === 'light' ? 'dark' : 'light';
document.documentElement.setAttribute('data-theme', n);
localStorage.setItem('theme', n);
t.textContent = n === 'dark' ? '\u2600' : '\u263E';
});
})();
</script>
<div class="container">
<h1 class="animate" style="--i:0">Requirements Audit</h1>
<p class="subtitle animate" style="--i:1">victoria's email vs. implementation plan &mdash; point-by-point review</p>
<div class="kpi-row animate" style="--i:2">
<div class="kpi-card">
<div class="kpi-card__value">14</div>
<div class="kpi-card__label">Items Reviewed</div>
</div>
<div class="kpi-card">
<div class="kpi-card__value kpi-card__value--green">13</div>
<div class="kpi-card__label">Match</div>
</div>
<div class="kpi-card">
<div class="kpi-card__value kpi-card__value--red">1</div>
<div class="kpi-card__label">Gap</div>
</div>
<div class="kpi-card">
<div class="kpi-card__value" style="color:var(--accent)">93%</div>
<div class="kpi-card__label">Coverage</div>
</div>
</div>
<div class="legend animate" style="--i:3">
<div class="legend-item"><div class="legend-swatch" style="background:var(--green)"></div> Match</div>
<div class="legend-item"><div class="legend-swatch" style="background:var(--red)"></div> Gap</div>
<div class="legend-item"><div class="legend-swatch" style="background:var(--orange)"></div> Partial</div>
</div>
<div class="table-wrap animate" style="--i:4">
<div class="table-scroll">
<table class="data-table">
<thead>
<tr>
<th class="wide">Request</th>
<th class="wide">Plan</th>
<th>Status</th>
</tr>
</thead>
<tbody>
<tr style="--i:4">
<td class="wide">Template: <code>yoth-special-edition</code> only</td>
<td class="wide">Plan scopes everything to this template.</td>
<td><span class="status status--match">Match</span></td>
</tr>
<tr style="--i:5">
<td class="wide">EOD tomorrow (Friday), Tuesday AM launch</td>
<td class="wide">Plan header says same.</td>
<td><span class="status status--match">Match</span></td>
</tr>
<tr style="--i:6">
<td class="wide">Update BIS button label and pop up text</td>
<td class="wide">Button text + modal description configurable via settings.</td>
<td><span class="status status--match">Match</span></td>
</tr>
<tr style="--i:7">
<td class="wide">Use Stoq's API to trigger events</td>
<td class="wide">Uses <code>openInlineForm</code>, <code>openModal</code>, <code>removeInlineForm</code>.</td>
<td><span class="status status--match">Match</span></td>
</tr>
<tr style="--i:8">
<td class="wide">Custom button + modal with text settings in buy buttons block</td>
<td class="wide">Schema settings in <code>buy_buttons</code> block.</td>
<td><span class="status status--match">Match</span></td>
</tr>
<tr style="--i:9">
<td class="wide">Default values = current behavior when empty</td>
<td class="wide">Blank settings = Stoq default "Notify Me" behavior.</td>
<td><span class="status status--match">Match</span></td>
</tr>
<tr style="--i:10">
<td class="wide">Only display for OOS variants</td>
<td class="wide">DOM-based sold-out detection.</td>
<td><span class="status status--match">Match</span></td>
</tr>
<tr style="--i:11">
<td class="wide">Exclude products with <code>excludebis</code> tag</td>
<td class="wide">Checked in both PDP and PLP Liquid.</td>
<td><span class="status status--match">Match</span></td>
</tr>
<tr style="--i:12">
<td class="wide"><code>openInlineForm</code> to load Stoq form in modal</td>
<td class="wide">PDP modal uses <code>openInlineForm</code>.</td>
<td><span class="status status--match">Match</span></td>
</tr>
<tr style="--i:13">
<td class="wide">Updated Button Label: "Join the waitlist"</td>
<td class="wide">Pre-populated in template JSON.</td>
<td><span class="status status--match">Match</span></td>
</tr>
<tr style="--i:14">
<td class="wide">Updated Pop Up Text: "Sign up to be notified when we restock this or other embroidered styles."</td>
<td class="wide"><code>bis_modal_description</code> setting.</td>
<td><span class="status status--match">Match</span></td>
</tr>
<tr style="--i:15">
<td class="wide">Theme: Huha 2.0 - Giddy Up Collection D2C Launch</td>
<td class="wide">Clone ID <code>145580556374</code>.</td>
<td><span class="status status--match">Match</span></td>
</tr>
<tr style="--i:16">
<td class="wide">Changes made locally</td>
<td class="wide">Local dev + theme push.</td>
<td><span class="status status--match">Match</span></td>
</tr>
<tr style="--i:17">
<td class="wide">Run <code>stoq:restock-modal:submitted</code> when form is submitted</td>
<td class="wide">Not mentioned in plan.
<small>When using <code>openInlineForm</code> inside a custom modal, unclear if Stoq fires this event automatically or if manual dispatch is needed.</small>
</td>
<td><span class="status status--gap">Gap</span></td>
</tr>
</tbody>
<tfoot>
<tr>
<td>14 items reviewed</td>
<td></td>
<td>13 match &middot; 1 gap</td>
</tr>
</tfoot>
</table>
</div>
</div>
<details class="collapsible animate" style="--i:19">
<summary>Gap Analysis Detail</summary>
<div class="collapsible__body">
<strong>Gap: <code>stoq:restock-modal:submitted</code> event.</strong>
Michael explicitly requests firing this event on form submission. The plan uses Stoq's <code>openInlineForm</code> inside a custom modal, but doesn't address whether Stoq dispatches this event automatically in that context. If other integrations (Klaviyo, analytics, theme JS) listen for it, missing it could silently break the submission pipeline. Recommend adding an explicit <code>dispatchEvent</code> call as a safety net.
</div>
</details>
</div>
</body>
</html>

View File

@@ -0,0 +1,768 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>CI/CD Pipeline — Reference Template</title>
<!--
Reference template for the ck:preview skill: Mermaid diagrams.
Teal/cyan palette — distinctly different from terracotta (architecture)
and rose (data-table) templates so agents absorb variety.
Key patterns demonstrated:
- Teal/cyan palette (NOT indigo/violet)
- Dot-grid background atmosphere (different from radial gradient)
- Large heading (38px) for typographic contrast
- ESM import of Mermaid + @mermaid-js/layout-elk
- Mermaid theme: 'base' + full themeVariables, fontSize: 16px
- CSS overrides: .nodeLabel 16px, .edgeLabel 13px
- look: 'classic' for clean lines
- layout: 'elk' for better node positioning
- Zoom controls with scroll-to-zoom and drag-to-pan
- Both light and dark themes via prefers-color-scheme
- Staggered fade-in, reduced motion respect
-->
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Bricolage+Grotesque:wght@400;500;600;700&family=Fragment+Mono:wght@400&display=swap" rel="stylesheet">
<style>
/* ============ THEME ============ */
:root {
--font-body: 'Bricolage Grotesque', system-ui, sans-serif;
--font-mono: 'Fragment Mono', 'SF Mono', Consolas, monospace;
--bg: #f0fdfa;
--surface: #ffffff;
--border: rgba(0, 0, 0, 0.07);
--text: #134e4a;
--text-dim: #5f8a85;
--primary: #0d9488;
--primary-dim: rgba(13, 148, 136, 0.08);
--secondary: #0369a1;
--tertiary: #d97706;
--danger: #dc2626;
}
@media (prefers-color-scheme: dark) {
:root:not([data-theme="light"]) {
--bg: #042f2e;
--surface: #0a3d3a;
--border: rgba(255, 255, 255, 0.08);
--text: #ccfbf1;
--text-dim: #5eead4;
--primary: #2dd4bf;
--primary-dim: rgba(45, 212, 191, 0.14);
--secondary: #38bdf8;
--tertiary: #fbbf24;
--danger: #f87171;
}
}
/* ── Dark (manual toggle override) ── */
[data-theme="dark"] {
--bg: #042f2e;
--surface: #0a3d3a;
--border: rgba(255, 255, 255, 0.08);
--text: #ccfbf1;
--text-dim: #5eead4;
--primary: #2dd4bf;
--primary-dim: rgba(45, 212, 191, 0.14);
--secondary: #38bdf8;
--tertiary: #fbbf24;
--danger: #f87171;
}
/* ============ THEME TOGGLE ============ */
.theme-toggle {
position: fixed; top: 16px; right: 16px; z-index: 300;
width: 36px; height: 36px; border-radius: 8px;
border: 1px solid var(--border); background: var(--surface);
color: var(--text-dim); cursor: pointer;
display: flex; align-items: center; justify-content: center;
font-size: 16px; transition: background 0.15s, color 0.15s;
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.06);
}
.theme-toggle:hover { background: var(--surface); color: var(--text); }
/* ============ RESET + BASE ============ */
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
background-color: var(--bg);
background-image: radial-gradient(circle, var(--border) 1px, transparent 1px);
background-size: 24px 24px;
color: var(--text);
font-family: var(--font-body);
padding: 40px;
min-height: 100vh;
}
/* ============ ANIMATION ============ */
@keyframes fadeUp {
from { opacity: 0; transform: translateY(12px); }
to { opacity: 1; transform: translateY(0); }
}
.animate {
animation: fadeUp 0.4s ease-out both;
animation-delay: calc(var(--i, 0) * 0.06s);
}
@media (prefers-reduced-motion: reduce) {
*, *::before, *::after {
animation-duration: 0.01ms !important;
animation-delay: 0ms !important;
transition-duration: 0.01ms !important;
}
}
/* ============ LAYOUT ============ */
.container {
max-width: 1000px;
margin: 0 auto;
}
h1 {
font-size: 38px;
font-weight: 700;
letter-spacing: -1px;
margin-bottom: 6px;
text-wrap: balance;
}
.subtitle {
color: var(--text-dim);
font-family: var(--font-mono);
font-size: 12px;
margin-bottom: 32px;
}
.description {
font-size: 14px;
line-height: 1.7;
color: var(--text-dim);
margin-bottom: 24px;
max-width: 700px;
}
.description code {
font-family: var(--font-mono);
font-size: 12px;
background: var(--primary-dim);
color: var(--primary);
padding: 1px 5px;
border-radius: 3px;
}
/* ============ MERMAID CONTAINER ============ */
.mermaid-wrap {
position: relative;
background: var(--surface);
border: 1px solid var(--border);
border-radius: 12px;
padding: 32px 24px;
overflow: auto;
margin-bottom: 24px;
display: flex;
justify-content: center;
align-items: center;
min-height: 400px;
}
.zoom-controls {
position: absolute;
top: 8px;
right: 8px;
display: flex;
gap: 2px;
z-index: 10;
background: var(--surface);
border: 1px solid var(--border);
border-radius: 6px;
padding: 2px;
}
.zoom-controls button {
width: 28px;
height: 28px;
border: none;
background: transparent;
color: var(--text-dim);
font-family: var(--font-mono);
font-size: 14px;
cursor: pointer;
border-radius: 4px;
display: flex;
align-items: center;
justify-content: center;
transition: background 0.15s ease, color 0.15s ease;
}
.zoom-controls button:hover {
background: var(--border);
color: var(--text);
}
.mermaid-wrap { cursor: grab; }
.mermaid-wrap.is-panning { cursor: grabbing; user-select: none; }
/* ============ MULTI-DIAGRAM STRUCTURE ============ */
.diagram-shell {
position: relative;
}
.diagram-shell__hint {
font-family: var(--font-mono);
font-size: 11px;
color: var(--text-dim);
margin-bottom: 8px;
opacity: 0.7;
}
.mermaid-viewport {
position: relative;
overflow: hidden;
width: 100%;
height: 100%;
min-height: 300px;
}
.mermaid-canvas {
position: absolute;
top: 0;
left: 0;
}
.zoom-label {
font-family: var(--font-mono);
font-size: 10px;
color: var(--text-dim);
padding: 0 6px;
white-space: nowrap;
}
/* ============ MERMAID SVG OVERRIDES ============ */
.mermaid .nodeLabel {
font-family: var(--font-body) !important;
font-size: 16px !important;
}
.mermaid .edgeLabel {
font-family: var(--font-mono) !important;
font-size: 13px !important;
}
.mermaid .node rect,
.mermaid .node circle,
.mermaid .node polygon {
stroke-width: 1.5px !important;
}
.mermaid .edge-pattern-solid {
stroke-width: 1.5px !important;
}
/* ============ LEGEND ============ */
.legend {
display: flex;
gap: 20px;
flex-wrap: wrap;
}
.legend-item {
display: flex;
align-items: center;
gap: 6px;
font-family: var(--font-mono);
font-size: 11px;
color: var(--text-dim);
}
.legend-swatch {
width: 12px;
height: 12px;
border-radius: 3px;
}
/* ============ CALLOUT ============ */
.callout {
background: var(--surface);
border: 1px solid var(--border);
border-left: 3px solid var(--primary);
border-radius: 0 10px 10px 0;
padding: 16px 20px;
font-size: 13px;
line-height: 1.6;
color: var(--text-dim);
margin-top: 24px;
}
.callout strong { color: var(--text); }
.callout code {
font-family: var(--font-mono);
font-size: 11px;
background: var(--primary-dim);
color: var(--primary);
padding: 1px 5px;
border-radius: 3px;
}
/* ============ RESPONSIVE ============ */
@media (max-width: 768px) {
body { padding: 16px; }
h1 { font-size: 22px; }
.mermaid-wrap { padding: 16px 12px; }
}
</style>
</head>
<body>
<button class="theme-toggle" id="themeToggle" title="Toggle theme" aria-label="Toggle light/dark theme"></button>
<script>
(function() {
var t = document.getElementById('themeToggle');
var s = localStorage.getItem('theme');
var i = s || (matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light');
if (s) document.documentElement.setAttribute('data-theme', i);
t.textContent = i === 'dark' ? '\u2600' : '\u263E';
t.addEventListener('click', function() {
var c = document.documentElement.getAttribute('data-theme')
|| (matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light');
var n = c === 'light' ? 'dark' : 'light';
document.documentElement.setAttribute('data-theme', n);
localStorage.setItem('theme', n);
t.textContent = n === 'dark' ? '\u2600' : '\u263E';
});
})();
</script>
<div class="container">
<h1 class="animate" style="--i:0">CI/CD Pipeline</h1>
<p class="subtitle animate" style="--i:1">github actions &rarr; staging &rarr; production</p>
<p class="description animate" style="--i:2">
Every push to <code>main</code> triggers the full pipeline. Tests and linting run in parallel,
then the build step produces a Docker image. Staging deploys automatically; production requires
manual approval via a GitHub environment gate.
</p>
<section class="diagram-shell animate" style="--i:3">
<p class="diagram-shell__hint">
Ctrl/Cmd + wheel to zoom. Scroll to pan. Drag to pan when zoomed. Double-click to fit.
</p>
<div class="mermaid-wrap">
<div class="zoom-controls">
<button type="button" data-action="zoom-in" title="Zoom in">+</button>
<button type="button" data-action="zoom-out" title="Zoom out">&minus;</button>
<button type="button" data-action="zoom-fit" title="Smart fit">&#8634;</button>
<button type="button" data-action="zoom-one" title="1:1 zoom">1:1</button>
<button type="button" data-action="zoom-expand" title="Open full size">&#x26F6;</button>
<span class="zoom-label">Loading...</span>
</div>
<div class="mermaid-viewport">
<div class="mermaid mermaid-canvas"></div>
</div>
</div>
<script type="text/plain" class="diagram-source">
graph TD
A[Push to main] --> B{Branch?}
B -->|main| C[Run Tests]
B -->|feature| I[Run Tests]
I --> J[Preview Deploy]
C --> D[Lint + Type Check]
C --> E[Unit Tests]
C --> F[Integration Tests]
D --> G[Build Docker Image]
E --> G
F --> G
G --> H[Deploy to Staging]
H --> K{Smoke Tests Pass?}
K -->|Yes| L[Manual Approval]
K -->|No| M[Alert + Rollback]
L --> N[Deploy to Production]
N --> O[Health Check]
O --> P[Done]
</script>
</section>
<div class="legend animate" style="--i:4">
<div class="legend-item"><div class="legend-swatch" style="background:var(--primary)"></div> Automated step</div>
<div class="legend-item"><div class="legend-swatch" style="background:var(--tertiary)"></div> Decision gate</div>
<div class="legend-item"><div class="legend-swatch" style="background:var(--danger)"></div> Failure path</div>
<div class="legend-item"><div class="legend-swatch" style="background:var(--secondary)"></div> Success</div>
</div>
<div class="callout animate" style="--i:5">
<strong>ELK layout.</strong> The <code>layout: 'elk'</code> engine provides better node positioning
for complex graphs — it requires the separate <code>@mermaid-js/layout-elk</code> package (imported above).
Without it, Mermaid silently falls back to dagre.
</div>
</div>
<script type="module">
import mermaid from 'https://cdn.jsdelivr.net/npm/mermaid@11/dist/mermaid.esm.min.mjs';
import elkLayouts from 'https://cdn.jsdelivr.net/npm/@mermaid-js/layout-elk/dist/mermaid-layout-elk.esm.min.mjs';
const config = {
fitPadding: 28,
minHeight: 360,
maxHeightPx: 960,
maxHeightVh: 0.84,
maxInitialZoom: 1.8,
minZoom: 0.08,
maxZoom: 6.5,
zoomStep: 0.14,
readabilityFloor: 0.58
};
const clamp = (n, lo, hi) => Math.max(lo, Math.min(hi, n));
let activeDrag = null;
addEventListener('mousemove', (e) => activeDrag?.onMove(e));
addEventListener('mouseup', () => {
activeDrag?.onEnd();
activeDrag = null;
});
const isDark = matchMedia('(prefers-color-scheme: dark)').matches;
mermaid.registerLayoutLoaders(elkLayouts);
mermaid.initialize({
startOnLoad: false,
theme: 'base',
look: 'classic',
layout: 'elk',
themeVariables: {
fontFamily: "'Bricolage Grotesque', system-ui, sans-serif",
fontSize: '16px',
primaryColor: isDark ? '#115e59' : '#ccfbf1',
primaryBorderColor: isDark ? '#2dd4bf' : '#0d9488',
primaryTextColor: isDark ? '#ccfbf1' : '#134e4a',
secondaryColor: isDark ? '#0c4a6e' : '#e0f2fe',
secondaryBorderColor: isDark ? '#38bdf8' : '#0369a1',
secondaryTextColor: isDark ? '#ccfbf1' : '#134e4a',
tertiaryColor: isDark ? '#2e2618' : '#fffbeb',
tertiaryBorderColor: isDark ? '#fbbf24' : '#d97706',
tertiaryTextColor: isDark ? '#ccfbf1' : '#134e4a',
lineColor: isDark ? '#5eead4' : '#5f8a85',
noteBkgColor: isDark ? '#115e59' : '#fefce8',
noteTextColor: isDark ? '#ccfbf1' : '#134e4a',
noteBorderColor: isDark ? '#fbbf24' : '#d97706',
}
});
function initDiagram(shell) {
const wrap = shell.querySelector('.mermaid-wrap');
const viewport = shell.querySelector('.mermaid-viewport');
const canvas = shell.querySelector('.mermaid-canvas');
const source = shell.querySelector('.diagram-source');
const label = shell.querySelector('.zoom-label');
if (!wrap || !viewport || !canvas || !source || !label) {
console.error('initDiagram: missing required elements in', shell);
return;
}
let zoom = 1;
let fitMode = 'contain';
let panX = 0;
let panY = 0;
let svgW = 0;
let svgH = 0;
let sx = 0;
let sy = 0;
let spx = 0;
let spy = 0;
let touchDist = 0;
let touchCx = 0;
let touchCy = 0;
function constrainPan() {
const vpW = viewport.clientWidth;
const vpH = viewport.clientHeight;
const rW = svgW * zoom;
const rH = svgH * zoom;
const pad = config.fitPadding;
panX = (rW + pad * 2 <= vpW)
? (vpW - rW) / 2
: clamp(panX, vpW - rW - pad, pad);
panY = (rH + pad * 2 <= vpH)
? (vpH - rH) / 2
: clamp(panY, vpH - rH - pad, pad);
}
function applyTransform() {
const svg = canvas.querySelector('svg');
if (!svg || !svgW) return;
constrainPan();
svg.style.width = (svgW * zoom) + 'px';
svg.style.height = (svgH * zoom) + 'px';
canvas.style.transform = `translate(${panX}px, ${panY}px)`;
label.textContent = Math.round(zoom * 100) + '% \u2014 ' + fitMode;
}
function canPan() {
const rW = svgW * zoom;
const rH = svgH * zoom;
return rW + config.fitPadding * 2 > viewport.clientWidth
|| rH + config.fitPadding * 2 > viewport.clientHeight;
}
function computeSmartFit() {
const vpW = viewport.clientWidth;
const vpH = viewport.clientHeight;
const aW = Math.max(80, vpW - config.fitPadding * 2);
const aH = Math.max(80, vpH - config.fitPadding * 2);
const contain = Math.min(aW / svgW, aH / svgH);
let z = contain;
let mode = 'contain';
if (contain < config.readabilityFloor) {
const chartR = svgH / svgW;
const vpR = vpH / Math.max(vpW, 1);
if (chartR >= vpR) {
z = aW / svgW;
mode = 'width-priority';
} else {
z = aH / svgH;
mode = 'height-priority';
}
}
return { zoom: clamp(z, config.minZoom, config.maxInitialZoom), mode };
}
function fitDiagram() {
if (!svgW) return;
const fit = computeSmartFit();
zoom = fit.zoom;
fitMode = fit.mode;
panX = (viewport.clientWidth - svgW * zoom) / 2;
panY = (viewport.clientHeight - svgH * zoom) / 2;
applyTransform();
}
function setOneToOne() {
zoom = clamp(1, config.minZoom, config.maxZoom);
fitMode = '1:1';
panX = (viewport.clientWidth - svgW * zoom) / 2;
panY = (viewport.clientHeight - svgH * zoom) / 2;
applyTransform();
}
function zoomAround(factor, cx, cy) {
const next = clamp(zoom * factor, config.minZoom, config.maxZoom);
const ratio = next / zoom;
panX = cx - ratio * (cx - panX);
panY = cy - ratio * (cy - panY);
zoom = next;
fitMode = 'custom';
applyTransform();
}
function readSvgNaturalSize(svg) {
let w = 0;
let h = 0;
if (svg.viewBox?.baseVal?.width > 0) {
w = svg.viewBox.baseVal.width;
h = svg.viewBox.baseVal.height;
}
if (!w) {
w = parseFloat(svg.getAttribute('width')) || 0;
h = parseFloat(svg.getAttribute('height')) || 0;
}
if (!w) {
const b = svg.getBBox();
w = b.width;
h = b.height;
}
if (!w) {
const r = svg.getBoundingClientRect();
w = r.width || 1000;
h = r.height || 700;
}
if (!svg.getAttribute('viewBox')) {
svg.setAttribute('viewBox', `0 0 ${w} ${h}`);
}
return { w, h };
}
function setAdaptiveHeight() {
if (!svgW) return;
const usableW = Math.max(280, wrap.getBoundingClientRect().width - 2);
const idealH = (svgH / svgW) * usableW + config.fitPadding * 2;
const maxVp = Math.floor(innerHeight * config.maxHeightVh);
const hardMax = Math.min(config.maxHeightPx, Math.max(config.minHeight + 40, maxVp));
wrap.style.height = Math.round(clamp(idealH, config.minHeight, hardMax)) + 'px';
}
function openInNewTab() {
const svg = canvas.querySelector('svg');
if (!svg) return;
const clone = svg.cloneNode(true);
clone.style.width = '';
clone.style.height = '';
// Use the same isDark value that was used to render the Mermaid theme
// This ensures the background matches the baked-in SVG colors
const bg = isDark ? '#042f2e' : '#f0fdfa';
const html = `<!DOCTYPE html><html lang="en"><head><meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Diagram</title><style>
body{margin:0;min-height:100vh;display:flex;align-items:center;justify-content:center;
background:${bg};padding:40px;box-sizing:border-box}
svg{max-width:100%;max-height:90vh;height:auto}
</style></head><body>${clone.outerHTML}</body></html>`;
open(URL.createObjectURL(new Blob([html], { type: 'text/html' })), '_blank');
}
async function render() {
try {
const code = source.textContent.trim();
if (!code) {
label.textContent = 'Error: Empty source';
return;
}
const id = 'diagram-' + Date.now() + '-' + Math.random().toString(36).slice(2, 8);
const { svg } = await mermaid.render(id, code);
canvas.innerHTML = svg;
const svgNode = canvas.querySelector('svg');
if (!svgNode) {
label.textContent = 'Error: No SVG';
return;
}
const size = readSvgNaturalSize(svgNode);
svgW = size.w;
svgH = size.h;
svgNode.removeAttribute('width');
svgNode.removeAttribute('height');
svgNode.style.maxWidth = 'none';
svgNode.style.display = 'block';
setAdaptiveHeight();
fitDiagram();
} catch (err) {
console.error('Mermaid render failed:', err);
label.textContent = 'Error: ' + (err.message || 'Render failed');
}
}
const actions = {
'zoom-in': () => zoomAround(1 + config.zoomStep, viewport.clientWidth / 2, viewport.clientHeight / 2),
'zoom-out': () => zoomAround(1 / (1 + config.zoomStep), viewport.clientWidth / 2, viewport.clientHeight / 2),
'zoom-fit': fitDiagram,
'zoom-one': setOneToOne,
'zoom-expand': openInNewTab
};
Object.entries(actions).forEach(([action, handler]) => {
wrap.querySelector(`[data-action="${action}"]`)?.addEventListener('click', handler);
});
viewport.addEventListener('dblclick', fitDiagram);
viewport.addEventListener('wheel', (e) => {
if (e.ctrlKey || e.metaKey) {
e.preventDefault();
const rect = viewport.getBoundingClientRect();
const factor = e.deltaY < 0 ? 1 + config.zoomStep : 1 / (1 + config.zoomStep);
zoomAround(factor, e.clientX - rect.left, e.clientY - rect.top);
return;
}
if (canPan()) {
e.preventDefault();
panX -= e.deltaX;
panY -= e.deltaY;
applyTransform();
}
}, { passive: false });
viewport.addEventListener('mousedown', (e) => {
if (e.target.closest('.zoom-controls') || !canPan()) return;
wrap.classList.add('is-panning');
sx = e.clientX;
sy = e.clientY;
spx = panX;
spy = panY;
e.preventDefault();
activeDrag = {
onMove: (ev) => {
panX = spx + (ev.clientX - sx);
panY = spy + (ev.clientY - sy);
applyTransform();
},
onEnd: () => {
wrap.classList.remove('is-panning');
}
};
});
viewport.addEventListener('touchstart', (e) => {
if (e.touches.length === 1) {
sx = e.touches[0].clientX;
sy = e.touches[0].clientY;
spx = panX;
spy = panY;
} else if (e.touches.length === 2) {
const dx = e.touches[0].clientX - e.touches[1].clientX;
const dy = e.touches[0].clientY - e.touches[1].clientY;
touchDist = Math.sqrt(dx * dx + dy * dy);
const r = viewport.getBoundingClientRect();
touchCx = (e.touches[0].clientX + e.touches[1].clientX) / 2 - r.left;
touchCy = (e.touches[0].clientY + e.touches[1].clientY) / 2 - r.top;
}
}, { passive: true });
viewport.addEventListener('touchmove', (e) => {
if (e.touches.length === 1 && canPan()) {
if (touchDist > 0) {
sx = e.touches[0].clientX;
sy = e.touches[0].clientY;
spx = panX;
spy = panY;
touchDist = 0;
}
e.preventDefault();
panX = spx + (e.touches[0].clientX - sx);
panY = spy + (e.touches[0].clientY - sy);
applyTransform();
} else if (e.touches.length === 2 && touchDist > 0) {
e.preventDefault();
const dx = e.touches[0].clientX - e.touches[1].clientX;
const dy = e.touches[0].clientY - e.touches[1].clientY;
const d = Math.sqrt(dx * dx + dy * dy);
zoomAround(d / touchDist, touchCx, touchCy);
touchDist = d;
}
}, { passive: false });
new ResizeObserver(() => {
if (svgW) {
setAdaptiveHeight();
fitDiagram();
}
}).observe(wrap);
render();
}
document.querySelectorAll('.diagram-shell').forEach(initDiagram);
</script>
</body>
</html>

View File

@@ -0,0 +1,968 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>API Gateway Redesign — Reference Slide Deck</title>
<!--
Reference template for the ck:preview skill: slide decks.
Midnight Editorial preset — deep navy, serif display, warm gold accents.
Distinctly different from the terracotta (architecture), teal (mermaid),
and rose (data-table) templates so agents absorb variety.
Key patterns demonstrated:
- All 10 slide types in a cohesive narrative
- SlideEngine JS: keyboard/touch/wheel nav, progress bar, dots, counter, hints
- Cinematic transitions: fade + translateY + scale, staggered child reveals
- Per-slide background variation (gradient direction, accent glow position)
- Decorative SVG accents (corner marks, quote mark, divider)
- Typography scale: 120px display → 48px heading → 22px body → 14px label
- Compositional variety: centered, left-heavy, split, full-bleed
- Mermaid at presentation scale (18px labels, 2px edges, 8 nodes)
- Dark-first with light mode via prefers-color-scheme
- Responsive height breakpoints for projection and small viewports
- Nav chrome with backdrop blur for mixed-background visibility
- Event delegation: Mermaid zoom and table scroll don't trigger slide nav
- prefers-reduced-motion respected
-->
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Instrument+Serif:ital@0;1&family=JetBrains+Mono:wght@400;500;600;700&display=swap" rel="stylesheet">
<style>
/* ============ THEME: Midnight Editorial ============ */
:root {
--font-body: 'Instrument Serif', Georgia, serif;
--font-mono: 'JetBrains Mono', 'SF Mono', Consolas, monospace;
--bg: #0f1729;
--surface: #162040;
--surface2: #1d2b52;
--surface-elevated: #243362;
--border: rgba(200, 180, 140, 0.08);
--border-bright: rgba(200, 180, 140, 0.16);
--text: #e8e4d8;
--text-dim: #9a9484;
--accent: #d4a73a;
--accent-dim: rgba(212, 167, 58, 0.1);
--code-bg: #0a0f1e;
--code-text: #d4d0c4;
--green: #4ade80;
--green-dim: rgba(74, 222, 128, 0.1);
--red: #f87171;
--red-dim: rgba(248, 113, 113, 0.1);
--blue: #60a5fa;
--blue-dim: rgba(96, 165, 250, 0.1);
}
@media (prefers-color-scheme: light) {
:root:not([data-theme="dark"]) {
--bg: #faf8f2;
--surface: #ffffff;
--surface2: #f5f0e6;
--surface-elevated: #fffdf5;
--border: rgba(30, 30, 50, 0.08);
--border-bright: rgba(30, 30, 50, 0.16);
--text: #1a1814;
--text-dim: #7a7468;
--accent: #b8860b;
--accent-dim: rgba(184, 134, 11, 0.08);
--code-bg: #2a2520;
--code-text: #e8e4d8;
--green: #16a34a;
--green-dim: rgba(22, 163, 74, 0.08);
--red: #dc2626;
--red-dim: rgba(220, 38, 38, 0.08);
--blue: #2563eb;
--blue-dim: rgba(37, 99, 235, 0.08);
}
}
/* ── Light (manual toggle override) ── */
[data-theme="light"] {
--bg: #faf8f2;
--surface: #ffffff;
--surface2: #f5f0e6;
--surface-elevated: #fffdf5;
--border: rgba(30, 30, 50, 0.08);
--border-bright: rgba(30, 30, 50, 0.16);
--text: #1a1814;
--text-dim: #7a7468;
--accent: #b8860b;
--accent-dim: rgba(184, 134, 11, 0.08);
--code-bg: #2a2520;
--code-text: #e8e4d8;
--green: #16a34a;
--green-dim: rgba(22, 163, 74, 0.08);
--red: #dc2626;
--red-dim: rgba(220, 38, 38, 0.08);
--blue: #2563eb;
--blue-dim: rgba(37, 99, 235, 0.08);
}
/* ============ THEME TOGGLE ============ */
.theme-toggle {
position: fixed; top: 16px; right: 16px; z-index: 300;
width: 36px; height: 36px; border-radius: 8px;
border: 1px solid var(--border); background: var(--surface);
color: var(--text-dim); cursor: pointer;
display: flex; align-items: center; justify-content: center;
font-size: 16px; transition: background 0.15s, color 0.15s;
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.06);
}
.theme-toggle:hover { background: var(--surface2); color: var(--text); }
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: var(--font-body);
color: var(--text);
background: var(--bg);
overflow: hidden;
}
/* ============ DECK ENGINE ============ */
.deck {
height: 100dvh;
overflow-y: auto;
scroll-snap-type: y mandatory;
scroll-behavior: smooth;
-webkit-overflow-scrolling: touch;
}
.slide {
height: 100dvh;
scroll-snap-align: start;
overflow: hidden;
position: relative;
display: flex;
flex-direction: column;
justify-content: center;
padding: clamp(40px, 6vh, 80px) clamp(40px, 8vw, 120px);
isolation: isolate;
opacity: 0;
transform: translateY(40px) scale(0.98);
transition:
opacity 0.6s cubic-bezier(0.16, 1, 0.3, 1),
transform 0.6s cubic-bezier(0.16, 1, 0.3, 1);
}
.slide.visible {
opacity: 1;
transform: none;
}
.slide .reveal {
opacity: 0;
transform: translateY(20px);
transition:
opacity 0.5s cubic-bezier(0.16, 1, 0.3, 1),
transform 0.5s cubic-bezier(0.16, 1, 0.3, 1);
}
.slide.visible .reveal { opacity: 1; transform: none; }
.slide.visible .reveal:nth-child(1) { transition-delay: 0.1s; }
.slide.visible .reveal:nth-child(2) { transition-delay: 0.2s; }
.slide.visible .reveal:nth-child(3) { transition-delay: 0.3s; }
.slide.visible .reveal:nth-child(4) { transition-delay: 0.4s; }
.slide.visible .reveal:nth-child(5) { transition-delay: 0.5s; }
.slide.visible .reveal:nth-child(6) { transition-delay: 0.6s; }
@media (prefers-reduced-motion: reduce) {
.slide, .slide .reveal {
opacity: 1 !important;
transform: none !important;
transition: none !important;
}
}
/* ============ NAV CHROME ============ */
.deck-progress {
position: fixed; top: 0; left: 0; height: 3px;
background: var(--accent); z-index: 100;
transition: width 0.3s ease; pointer-events: none;
}
.deck-dots {
position: fixed; right: clamp(12px, 2vw, 24px); top: 50%;
transform: translateY(-50%); display: flex; flex-direction: column;
gap: 8px; z-index: 100; padding: 8px;
background: color-mix(in srgb, var(--bg) 60%, transparent 40%);
border-radius: 20px;
backdrop-filter: blur(4px); -webkit-backdrop-filter: blur(4px);
}
.deck-dot {
width: 8px; height: 8px; border-radius: 50%;
background: var(--text-dim); opacity: 0.3; border: none; padding: 0;
cursor: pointer; transition: opacity 0.2s ease, transform 0.2s ease;
}
.deck-dot:hover { opacity: 0.6; }
.deck-dot.active { opacity: 1; transform: scale(1.5); background: var(--accent); }
.deck-counter {
position: fixed; bottom: clamp(12px, 2vh, 24px); right: clamp(12px, 2vw, 24px);
font-family: var(--font-mono); font-size: 12px; color: var(--text-dim);
z-index: 100; font-variant-numeric: tabular-nums;
text-shadow: 0 1px 3px rgba(0, 0, 0, 0.3);
}
.deck-hints {
position: fixed; bottom: clamp(12px, 2vh, 24px); left: 50%;
transform: translateX(-50%); font-family: var(--font-mono);
font-size: 11px; color: var(--text-dim); opacity: 0.6; z-index: 100;
transition: opacity 0.5s ease; white-space: nowrap;
text-shadow: 0 1px 3px rgba(0, 0, 0, 0.3);
}
.deck-hints.faded { opacity: 0; pointer-events: none; }
/* ============ SHARED SLIDE ELEMENTS ============ */
.slide__display {
font-size: clamp(48px, 10vw, 120px);
font-weight: 400;
letter-spacing: -2px;
line-height: 0.95;
text-wrap: balance;
}
.slide__heading {
font-size: clamp(28px, 5vw, 48px);
font-weight: 400;
letter-spacing: -0.5px;
line-height: 1.15;
text-wrap: balance;
}
.slide__body {
font-size: clamp(16px, 2.2vw, 22px);
line-height: 1.6;
color: var(--text-dim);
text-wrap: pretty;
}
.slide__subtitle {
font-family: var(--font-mono);
font-size: clamp(12px, 1.5vw, 18px);
color: var(--text-dim);
letter-spacing: 1px;
text-transform: uppercase;
}
.slide__label {
font-family: var(--font-mono);
font-size: clamp(10px, 1.2vw, 13px);
font-weight: 600;
text-transform: uppercase;
letter-spacing: 1.5px;
color: var(--accent);
margin-bottom: 12px;
}
/* ============ DECORATIVE SVG ============ */
.slide__decor {
position: absolute;
pointer-events: none;
z-index: 0;
}
/* ============ SLIDE TYPE: TITLE ============ */
.slide--title {
justify-content: center;
align-items: center;
text-align: center;
background-image: radial-gradient(ellipse at 50% 30%, var(--accent-dim) 0%, transparent 50%);
}
.slide--title .slide__display { color: var(--accent); }
/* ============ SLIDE TYPE: DIVIDER ============ */
.slide--divider { justify-content: center; }
.slide--divider .slide__number {
font-size: clamp(100px, 22vw, 260px);
font-weight: 200;
line-height: 0.85;
opacity: 0.06;
position: absolute;
top: 50%; left: 50%;
transform: translate(-50%, -55%);
pointer-events: none;
font-variant-numeric: tabular-nums;
color: var(--accent);
}
/* ============ SLIDE TYPE: CONTENT ============ */
.slide--content .slide__inner {
display: grid;
grid-template-columns: 3fr 2fr;
gap: clamp(24px, 4vw, 60px);
align-items: center;
width: 100%;
}
.slide__bullets {
list-style: none;
padding: 0;
}
.slide__bullets li {
padding: 10px 0 10px 24px;
position: relative;
font-size: clamp(16px, 2vw, 22px);
line-height: 1.5;
color: var(--text-dim);
}
.slide__bullets li::before {
content: '';
position: absolute;
left: 0; top: 20px;
width: 6px; height: 6px;
border-radius: 50%;
background: var(--accent);
}
.slide__aside {
display: flex;
align-items: center;
justify-content: center;
min-height: 200px;
}
/* ============ SLIDE TYPE: SPLIT ============ */
.slide--split { padding: 0; }
.slide--split .slide__panels {
display: grid;
grid-template-columns: 3fr 2fr;
height: 100%;
}
.slide--split .slide__panel {
padding: clamp(40px, 6vh, 80px) clamp(32px, 4vw, 60px);
display: flex;
flex-direction: column;
justify-content: center;
}
.slide--split .slide__panel--primary { background: var(--surface); }
.slide--split .slide__panel--secondary {
background: var(--surface2);
display: flex;
flex-direction: column;
gap: 12px;
}
/* ============ SLIDE TYPE: DIAGRAM ============ */
.slide--diagram {
padding: clamp(24px, 4vh, 48px) clamp(24px, 4vw, 60px);
}
.slide--diagram .slide__heading { margin-bottom: clamp(8px, 1.5vh, 20px); }
.mermaid-wrap {
position: relative;
background: var(--surface);
border: 1px solid var(--border);
border-radius: 12px;
padding: 24px;
overflow: auto;
flex: 1;
min-height: 0;
display: flex;
justify-content: center;
align-items: center;
scrollbar-width: thin;
scrollbar-color: var(--border) transparent;
}
.mermaid-wrap::-webkit-scrollbar { width: 6px; height: 6px; }
.mermaid-wrap::-webkit-scrollbar-thumb { background: var(--border); border-radius: 3px; }
.zoom-controls {
position: absolute; top: 8px; right: 8px;
display: flex; gap: 2px; z-index: 10;
background: var(--surface); border: 1px solid var(--border);
border-radius: 6px; padding: 2px;
}
.zoom-controls button {
width: 28px; height: 28px; border: none; background: transparent;
color: var(--text-dim); font-family: var(--font-mono); font-size: 14px;
cursor: pointer; border-radius: 4px;
display: flex; align-items: center; justify-content: center;
transition: background 0.15s, color 0.15s;
}
.zoom-controls button:hover { background: var(--border); color: var(--text); }
.mermaid-wrap { cursor: grab; }
.mermaid-wrap.is-panning { cursor: grabbing; user-select: none; }
.mermaid .nodeLabel { color: var(--text) !important; }
.mermaid .edgeLabel { color: var(--text-dim) !important; background-color: var(--bg) !important; }
.mermaid .edgeLabel rect { fill: var(--bg) !important; }
.slide--diagram .mermaid svg {
width: 100% !important;
height: auto !important;
max-width: 100% !important;
}
.slide--diagram .mermaid .nodeLabel { font-size: 18px !important; }
.slide--diagram .mermaid .edgeLabel { font-family: var(--font-mono) !important; font-size: 14px !important; }
.slide--diagram .mermaid .node rect,
.slide--diagram .mermaid .node circle,
.slide--diagram .mermaid .node polygon { stroke-width: 2px; }
.slide--diagram .mermaid .edge-pattern-solid { stroke-width: 2px; }
/* ============ SLIDE TYPE: DASHBOARD ============ */
.slide--dashboard .slide__kpis {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(clamp(140px, 20vw, 220px), 1fr));
gap: clamp(12px, 2vw, 24px);
}
.slide__kpi {
background: var(--surface);
border: 1px solid var(--border);
border-radius: 12px;
padding: clamp(16px, 3vh, 32px) clamp(16px, 2vw, 24px);
min-width: 0;
overflow: hidden;
}
.slide__kpi-val {
font-size: clamp(36px, 6vw, 64px);
font-weight: 400;
letter-spacing: -1.5px;
line-height: 1.1;
font-variant-numeric: tabular-nums;
white-space: nowrap;
}
.slide__kpi-label {
font-family: var(--font-mono);
font-size: clamp(9px, 1.2vw, 13px);
font-weight: 600;
text-transform: uppercase;
letter-spacing: 1.5px;
color: var(--text-dim);
margin-top: 8px;
}
.slide__kpi-trend {
font-family: var(--font-mono);
font-size: 12px;
margin-top: 4px;
}
/* ============ SLIDE TYPE: TABLE ============ */
.slide--table { padding: clamp(24px, 4vh, 48px) clamp(24px, 4vw, 60px); }
.table-wrap {
background: var(--surface);
border: 1px solid var(--border);
border-radius: 12px;
overflow: hidden;
flex: 1;
min-height: 0;
}
.table-scroll { overflow-x: auto; }
.data-table { width: 100%; border-collapse: collapse; }
.data-table th {
background: var(--surface2);
font-family: var(--font-mono);
font-size: clamp(10px, 1.3vw, 14px);
font-weight: 600;
text-transform: uppercase;
letter-spacing: 1px;
color: var(--text-dim);
text-align: left;
padding: clamp(10px, 1.5vh, 16px) clamp(14px, 2vw, 24px);
border-bottom: 2px solid var(--border-bright);
white-space: nowrap;
}
.data-table td {
padding: clamp(10px, 1.5vh, 16px) clamp(14px, 2vw, 24px);
border-bottom: 1px solid var(--border);
font-size: clamp(14px, 1.8vw, 20px);
vertical-align: top;
}
.data-table tbody tr:last-child td { border-bottom: none; }
.data-table tbody tr:nth-child(even) { background: var(--surface2); }
.data-table tbody tr { transition: background 0.15s; }
.data-table tbody tr:hover { background: var(--accent-dim); }
.data-table code {
font-family: var(--font-mono); font-size: 0.85em;
background: var(--accent-dim); color: var(--accent);
padding: 1px 5px; border-radius: 3px;
}
/* ============ SLIDE TYPE: CODE ============ */
.slide--code { align-items: center; }
.slide__code-block {
background: var(--code-bg);
border: 1px solid var(--border);
border-radius: 12px;
padding: clamp(24px, 4vh, 48px) clamp(24px, 4vw, 48px);
max-width: 900px;
width: 100%;
position: relative;
}
.slide__code-filename {
position: absolute;
top: -12px; left: 24px;
font-family: var(--font-mono);
font-size: 11px; font-weight: 600;
padding: 4px 12px; border-radius: 4px;
background: var(--accent); color: var(--bg);
}
.slide__code-block pre { margin: 0; overflow-x: auto; }
.slide__code-block code {
font-family: var(--font-mono);
font-size: clamp(14px, 1.6vw, 18px);
line-height: 1.7;
color: var(--code-text);
}
.slide__code-block .hl { color: var(--accent); }
.slide__code-block .cm { color: var(--text-dim); }
/* ============ SLIDE TYPE: QUOTE ============ */
.slide--quote {
justify-content: center;
align-items: center;
text-align: center;
padding: clamp(60px, 10vh, 120px) clamp(60px, 12vw, 200px);
}
.slide__quote-mark {
font-size: clamp(80px, 14vw, 180px);
line-height: 0.5;
opacity: 0.06;
font-family: Georgia, serif;
pointer-events: none;
margin-bottom: -20px;
color: var(--accent);
}
.slide--quote blockquote {
font-size: clamp(24px, 4vw, 48px);
font-weight: 400;
line-height: 1.35;
font-style: italic;
}
.slide--quote cite {
font-family: var(--font-mono);
font-size: clamp(11px, 1.4vw, 14px);
font-style: normal;
margin-top: clamp(16px, 3vh, 32px);
display: block;
letter-spacing: 1.5px;
text-transform: uppercase;
color: var(--text-dim);
}
/* ============ SLIDE TYPE: FULL-BLEED ============ */
.slide--bleed {
padding: 0;
justify-content: flex-end;
}
.slide__bg {
position: absolute; inset: 0;
background-size: cover; background-position: center;
z-index: 0;
}
.slide__bg--gradient {
background: linear-gradient(135deg, #1a0f3c 0%, #0f1729 40%, #162040 100%);
}
.slide__scrim {
position: absolute; inset: 0;
background: linear-gradient(to top, rgba(0,0,0,0.75) 0%, rgba(0,0,0,0.2) 40%, transparent 100%);
z-index: 1;
}
.slide--bleed .slide__content {
position: relative; z-index: 2;
padding: clamp(40px, 6vh, 80px) clamp(40px, 8vw, 120px);
color: #ffffff;
}
.slide--bleed .slide__heading { color: #ffffff; }
.slide--bleed .slide__subtitle { color: rgba(255,255,255,0.7); }
/* ============ RESPONSIVE ============ */
@media (max-height: 700px) {
.slide { padding: clamp(24px, 4vh, 40px) clamp(32px, 6vw, 80px); }
.slide__display { font-size: clamp(36px, 8vw, 72px); }
.slide--divider .slide__number { font-size: clamp(80px, 16vw, 160px); }
}
@media (max-height: 600px) {
.slide__decor { display: none; }
.slide--quote { padding: clamp(32px, 6vh, 60px) clamp(40px, 8vw, 100px); }
.slide__quote-mark { display: none; }
}
@media (max-height: 500px) {
.slide { padding: clamp(16px, 3vh, 24px) clamp(24px, 5vw, 48px); }
.deck-dots { display: none; }
.slide__display { font-size: clamp(28px, 7vw, 48px); }
}
@media (max-width: 768px) {
.slide--content .slide__inner { grid-template-columns: 1fr; }
.slide--content .slide__aside { display: none; }
.slide--split .slide__panels { grid-template-columns: 1fr; }
.slide--dashboard .slide__kpis { grid-template-columns: repeat(2, 1fr); }
}
</style>
</head>
<body>
<button class="theme-toggle" id="themeToggle" title="Toggle theme" aria-label="Toggle light/dark theme"></button>
<script>
(function() {
var t = document.getElementById('themeToggle');
var s = localStorage.getItem('theme');
var i = s || (matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light');
if (s) document.documentElement.setAttribute('data-theme', i);
t.textContent = i === 'dark' ? '\u2600' : '\u263E';
t.addEventListener('click', function() {
var c = document.documentElement.getAttribute('data-theme')
|| (matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light');
var n = c === 'light' ? 'dark' : 'light';
document.documentElement.setAttribute('data-theme', n);
localStorage.setItem('theme', n);
t.textContent = n === 'dark' ? '\u2600' : '\u263E';
});
})();
</script>
<div class="deck">
<!-- SLIDE 1: TITLE -->
<section class="slide slide--title">
<svg class="slide__decor" style="top:0;right:0;" width="120" height="120" viewBox="0 0 120 120">
<line x1="120" y1="0" x2="120" y2="40" stroke="var(--accent)" stroke-width="2" opacity="0.15"/>
<line x1="80" y1="0" x2="120" y2="0" stroke="var(--accent)" stroke-width="2" opacity="0.15"/>
</svg>
<svg class="slide__decor" style="bottom:0;left:0;" width="120" height="120" viewBox="0 0 120 120">
<line x1="0" y1="80" x2="0" y2="120" stroke="var(--accent)" stroke-width="2" opacity="0.15"/>
<line x1="0" y1="120" x2="40" y2="120" stroke="var(--accent)" stroke-width="2" opacity="0.15"/>
</svg>
<div class="reveal">
<p class="slide__subtitle" style="margin-bottom:clamp(16px,2vh,32px);">Engineering Review &mdash; Q1 2026</p>
</div>
<h1 class="slide__display reveal">API Gateway Redesign</h1>
<div class="reveal">
<p class="slide__subtitle" style="margin-top:clamp(16px,2vh,32px);">From monolith proxy to edge-native routing</p>
</div>
</section>
<!-- SLIDE 2: SECTION DIVIDER -->
<section class="slide slide--divider" style="background-image:radial-gradient(ellipse at 80% 60%, var(--accent-dim) 0%, transparent 40%);">
<span class="slide__number">01</span>
<div>
<h2 class="slide__heading reveal">The Problem</h2>
<p class="slide__subtitle reveal" style="margin-top:12px;">Why the current gateway can't scale</p>
</div>
</section>
<!-- SLIDE 3: CONTENT (left-heavy asymmetric) -->
<section class="slide slide--content" style="background-image:radial-gradient(ellipse at 20% 80%, var(--accent-dim) 0%, transparent 45%);">
<div class="slide__inner">
<div>
<p class="slide__label reveal">Current State</p>
<h2 class="slide__heading reveal">Single Point of Failure</h2>
<ul class="slide__bullets">
<li class="reveal">All traffic routes through one Node.js process</li>
<li class="reveal">Rate limiting is per-instance, not distributed</li>
<li class="reveal">Auth validation adds 40ms per request</li>
<li class="reveal">No circuit breaking &mdash; cascade failures hit everything</li>
</ul>
</div>
<div class="slide__aside reveal">
<svg viewBox="0 0 160 160" width="160" height="160">
<circle cx="80" cy="80" r="60" fill="none" stroke="var(--red)" stroke-width="2" opacity="0.3"/>
<circle cx="80" cy="80" r="40" fill="none" stroke="var(--red)" stroke-width="1.5" opacity="0.2" stroke-dasharray="4 4"/>
<circle cx="80" cy="80" r="8" fill="var(--red)" opacity="0.4"/>
<text x="80" y="130" text-anchor="middle" font-family="var(--font-mono)" font-size="11" fill="var(--text-dim)">SINGLE PROCESS</text>
</svg>
</div>
</div>
</section>
<!-- SLIDE 4: SPLIT (before/after) -->
<section class="slide slide--split">
<div class="slide__panels">
<div class="slide__panel slide__panel--primary">
<p class="slide__label reveal" style="color:var(--red);">Before</p>
<h2 class="slide__heading reveal" style="font-size:clamp(22px,3.5vw,36px);">Monolith Proxy</h2>
<ul class="slide__bullets" style="margin-top:16px;">
<li class="reveal">Express.js middleware chain</li>
<li class="reveal">In-memory rate limit counters</li>
<li class="reveal">Synchronous JWT validation</li>
<li class="reveal">Manual upstream health checks</li>
</ul>
</div>
<div class="slide__panel slide__panel--secondary">
<p class="slide__label reveal" style="color:var(--green);">After</p>
<h2 class="slide__heading reveal" style="font-size:clamp(22px,3.5vw,36px);">Edge-Native</h2>
<ul class="slide__bullets" style="margin-top:16px;">
<li class="reveal">Cloudflare Workers at the edge</li>
<li class="reveal">Durable Objects for distributed state</li>
<li class="reveal">Async JWT with key caching</li>
<li class="reveal">Automatic circuit breakers</li>
</ul>
</div>
</div>
</section>
<!-- SLIDE 5: SECTION DIVIDER -->
<section class="slide slide--divider" style="background-image:radial-gradient(ellipse at 30% 40%, var(--accent-dim) 0%, transparent 40%);">
<span class="slide__number">02</span>
<div>
<h2 class="slide__heading reveal">Architecture</h2>
<p class="slide__subtitle reveal" style="margin-top:12px;">How the new system works</p>
</div>
</section>
<!-- SLIDE 6: DIAGRAM -->
<section class="slide slide--diagram">
<h2 class="slide__heading reveal">Request Flow</h2>
<div class="mermaid-wrap reveal">
<div class="zoom-controls">
<button onclick="zoomDiagram(this,1.2)" title="Zoom in">+</button>
<button onclick="zoomDiagram(this,0.8)" title="Zoom out">&minus;</button>
<button onclick="resetZoom(this)" title="Reset">&#8634;</button>
<button onclick="openDiagramFullscreen(this)" title="Open full size in new tab">&#x26F6;</button>
</div>
<pre class="mermaid">
graph LR
Client["Client"] --> Edge["Edge Worker"]
Edge --> Auth["Auth Cache"]
Edge --> RL["Rate Limiter<br/>Durable Object"]
Edge --> Router["Route Resolver"]
Router --> API["API Service"]
Router --> Static["Static Assets"]
API --> DB["Database"]
classDef primary fill:#d4a73a22,stroke:#d4a73a,stroke-width:2px
classDef secondary fill:#60a5fa22,stroke:#60a5fa,stroke-width:2px
classDef storage fill:#4ade8022,stroke:#4ade80,stroke-width:2px
class Client,Edge primary
class Auth,RL,Router secondary
class API,Static,DB storage
</pre>
</div>
</section>
<!-- SLIDE 7: DASHBOARD -->
<section class="slide slide--dashboard" style="background-image:radial-gradient(ellipse at 70% 30%, var(--accent-dim) 0%, transparent 40%);">
<h2 class="slide__heading reveal">Performance Impact</h2>
<div class="slide__kpis">
<div class="slide__kpi reveal">
<div class="slide__kpi-val" style="color:var(--accent);">12ms</div>
<div class="slide__kpi-label">P99 Latency</div>
<div class="slide__kpi-trend" style="color:var(--green);">&darr; from 142ms</div>
</div>
<div class="slide__kpi reveal">
<div class="slide__kpi-val" style="color:var(--green);">99.97%</div>
<div class="slide__kpi-label">Uptime</div>
<div class="slide__kpi-trend" style="color:var(--green);">&uarr; from 99.2%</div>
</div>
<div class="slide__kpi reveal">
<div class="slide__kpi-val" style="color:var(--blue);">340</div>
<div class="slide__kpi-label">Edge Locations</div>
<div class="slide__kpi-trend" style="color:var(--text-dim);">global coverage</div>
</div>
<div class="slide__kpi reveal">
<div class="slide__kpi-val" style="color:var(--accent);">$0.02</div>
<div class="slide__kpi-label">Per 10K Requests</div>
<div class="slide__kpi-trend" style="color:var(--green);">&darr; 68% cost reduction</div>
</div>
</div>
</section>
<!-- SLIDE 8: TABLE -->
<section class="slide slide--table">
<h2 class="slide__heading reveal">Migration Phases</h2>
<div class="table-wrap reveal" style="flex:1; min-height:0; margin-top:clamp(8px,1.5vh,20px);">
<div class="table-scroll">
<table class="data-table">
<thead><tr><th>Phase</th><th>Scope</th><th>Timeline</th><th>Risk</th></tr></thead>
<tbody>
<tr><td>1. Shadow mode</td><td>Mirror traffic to edge, compare responses</td><td>Week 1&ndash;2</td><td style="color:var(--green);">Low</td></tr>
<tr><td>2. Canary rollout</td><td>5% traffic to edge, monitor errors</td><td>Week 3</td><td style="color:var(--green);">Low</td></tr>
<tr><td>3. Gradual shift</td><td>25% &rarr; 50% &rarr; 75% traffic</td><td>Week 4&ndash;5</td><td style="color:var(--accent);">Medium</td></tr>
<tr><td>4. Full cutover</td><td>100% traffic, decommission old proxy</td><td>Week 6</td><td style="color:var(--accent);">Medium</td></tr>
<tr><td>5. Cleanup</td><td>Remove feature flags, archive old code</td><td>Week 7</td><td style="color:var(--green);">Low</td></tr>
</tbody>
</table>
</div>
</div>
</section>
<!-- SLIDE 9: CODE -->
<section class="slide slide--code" style="background-image:radial-gradient(ellipse at 50% 80%, var(--accent-dim) 0%, transparent 40%);">
<h2 class="slide__heading reveal" style="text-align:center;">Edge Worker Entry Point</h2>
<div class="slide__code-block reveal" style="margin-top:clamp(12px,2vh,24px);">
<span class="slide__code-filename">gateway.ts</span>
<pre><code><span class="hl">export default</span> {
<span class="hl">async fetch</span>(req: Request, env: Env) {
<span class="cm">// Auth check with edge-cached keys</span>
const identity = <span class="hl">await</span> verifyAuth(req, env);
<span class="cm">// Distributed rate limiting</span>
const limit = env.RATE_LIMITER.get(identity.id);
<span class="hl">if</span> (<span class="hl">await</span> limit.check()) <span class="hl">return</span> tooMany();
<span class="cm">// Route to upstream</span>
<span class="hl">return</span> route(req, env.SERVICES);
}
};</code></pre>
</div>
</section>
<!-- SLIDE 10: QUOTE -->
<section class="slide slide--quote" style="background-image:radial-gradient(ellipse at 50% 50%, var(--accent-dim) 0%, transparent 35%);">
<div class="slide__quote-mark reveal">&ldquo;</div>
<blockquote class="reveal">
The fastest request is the one that never leaves the edge.
</blockquote>
<cite class="reveal">&mdash; Edge Computing Principle</cite>
</section>
<!-- SLIDE 11: FULL-BLEED -->
<section class="slide slide--bleed">
<div class="slide__bg slide__bg--gradient"></div>
<div class="slide__scrim"></div>
<div class="slide__content">
<p class="slide__label reveal" style="color:rgba(255,255,255,0.6);">Next Steps</p>
<h2 class="slide__heading reveal">Ship Shadow Mode This Week</h2>
<p class="slide__subtitle reveal" style="color:rgba(255,255,255,0.6); margin-top:12px;">Full cutover targeted for end of Q1</p>
</div>
</section>
</div><!-- /deck -->
<script type="module">
import mermaid from 'https://cdn.jsdelivr.net/npm/mermaid@11/dist/mermaid.esm.min.mjs';
const isDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
mermaid.initialize({
startOnLoad: true,
theme: 'base',
look: 'classic',
themeVariables: {
primaryColor: isDark ? '#1d2b52' : '#fef3e0',
primaryBorderColor: isDark ? '#d4a73a' : '#b8860b',
primaryTextColor: isDark ? '#e8e4d8' : '#1a1814',
secondaryColor: isDark ? '#162040' : '#eff6ff',
secondaryBorderColor: isDark ? '#60a5fa' : '#2563eb',
secondaryTextColor: isDark ? '#e8e4d8' : '#1a1814',
tertiaryColor: isDark ? '#0f2620' : '#f0fdf4',
tertiaryBorderColor: isDark ? '#4ade80' : '#16a34a',
tertiaryTextColor: isDark ? '#e8e4d8' : '#1a1814',
lineColor: isDark ? '#9a9484' : '#7a7468',
fontSize: '18px',
fontFamily: 'var(--font-body)',
noteBkgColor: isDark ? '#1d2b52' : '#fef3e0',
noteTextColor: isDark ? '#e8e4d8' : '#1a1814',
noteBorderColor: isDark ? '#d4a73a' : '#b8860b',
}
});
function autoFit() {
document.querySelectorAll('.mermaid svg').forEach(function(svg) {
svg.removeAttribute('height');
svg.style.width = '100%';
svg.style.maxWidth = '100%';
svg.style.height = 'auto';
svg.parentElement.style.width = '100%';
});
document.querySelectorAll('.slide__kpi-val').forEach(function(el) {
if (el.scrollWidth > el.clientWidth) {
var s = el.clientWidth / el.scrollWidth;
el.style.transform = 'scale(' + s + ')';
el.style.transformOrigin = 'left top';
}
});
document.querySelectorAll('.slide--quote blockquote').forEach(function(el) {
var len = el.textContent.trim().length;
if (len > 100) {
var scale = Math.max(0.5, 100 / len);
var fs = parseFloat(getComputedStyle(el).fontSize);
el.style.fontSize = Math.max(16, Math.round(fs * scale)) + 'px';
}
});
}
mermaid.run().then(function() {
autoFit();
new SlideEngine();
});
</script>
<script>
// Mermaid zoom controls + click-to-expand
var INITIAL_ZOOM=1;
function zoomDiagram(b,f){var w=b.closest('.mermaid-wrap');var t=w.querySelector('.mermaid');var c=parseFloat(t.dataset.zoom||INITIAL_ZOOM);var n=Math.min(Math.max(c*f,0.5),5);t.dataset.zoom=n;t.style.zoom=n;}
function resetZoom(b){var w=b.closest('.mermaid-wrap');var t=w.querySelector('.mermaid');t.dataset.zoom=INITIAL_ZOOM;t.style.zoom=INITIAL_ZOOM;}
function openDiagramFullscreen(b){openMermaidInNewTab(b.closest('.mermaid-wrap'));}
function openMermaidInNewTab(w){var svg=w.querySelector('.mermaid svg');if(!svg)return;var clone=svg.cloneNode(true);clone.style.zoom='';clone.style.transform='';var styles=getComputedStyle(document.documentElement);var bg=styles.getPropertyValue('--bg').trim()||'#ffffff';var html='<!DOCTYPE html><html lang="en"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>Diagram</title><style>body{margin:0;min-height:100vh;display:flex;align-items:center;justify-content:center;background:'+bg+';padding:40px;box-sizing:border-box}svg{max-width:100%;max-height:90vh;height:auto}</style></head><body>'+clone.outerHTML+'</body></html>';window.open(URL.createObjectURL(new Blob([html],{type:'text/html'})),'_blank');}
document.querySelectorAll('.mermaid-wrap').forEach(function(w){w.addEventListener('wheel',function(e){if(!e.ctrlKey&&!e.metaKey)return;e.preventDefault();var t=w.querySelector('.mermaid');var c=parseFloat(t.dataset.zoom||INITIAL_ZOOM);var f=e.deltaY<0?1.1:0.9;var n=Math.min(Math.max(c*f,0.5),5);t.dataset.zoom=n;t.style.zoom=n;},{passive:false});var sX,sY,sL,sT,sTime,didPan;w.addEventListener('mousedown',function(e){if(e.target.closest('.zoom-controls'))return;w.classList.add('is-panning');sX=e.clientX;sY=e.clientY;sL=w.scrollLeft;sT=w.scrollTop;sTime=Date.now();didPan=false;});window.addEventListener('mousemove',function(e){if(!w.classList.contains('is-panning'))return;var dx=e.clientX-sX,dy=e.clientY-sY;if(Math.abs(dx)>5||Math.abs(dy)>5)didPan=true;w.scrollLeft=sL-dx;w.scrollTop=sT-dy;});window.addEventListener('mouseup',function(){if(!w.classList.contains('is-panning'))return;w.classList.remove('is-panning');if(!didPan&&Date.now()-sTime<300)openMermaidInNewTab(w);});});
// SlideEngine
function SlideEngine(){
this.deck=document.querySelector('.deck');
this.slides=[].slice.call(document.querySelectorAll('.slide'));
this.current=0;
this.total=this.slides.length;
this.buildChrome();
this.bindEvents();
this.observe();
this.update();
}
SlideEngine.prototype.buildChrome=function(){
var bar=document.createElement('div');bar.className='deck-progress';document.body.appendChild(bar);this.bar=bar;
var dots=document.createElement('div');dots.className='deck-dots';var self=this;
this.slides.forEach(function(_,i){var d=document.createElement('button');d.className='deck-dot';d.title='Slide '+(i+1);d.onclick=function(){self.goTo(i);};dots.appendChild(d);});
document.body.appendChild(dots);this.dots=[].slice.call(dots.children);
var ctr=document.createElement('div');ctr.className='deck-counter';document.body.appendChild(ctr);this.counter=ctr;
var hints=document.createElement('div');hints.className='deck-hints';hints.textContent='\u2190 \u2192 or scroll to navigate';document.body.appendChild(hints);this.hints=hints;
this.hintTimer=setTimeout(function(){hints.classList.add('faded');},4000);
};
SlideEngine.prototype.bindEvents=function(){
var self=this;
document.addEventListener('keydown',function(e){
if(e.target.closest('.mermaid-wrap,.table-scroll,.code-scroll,input,textarea,[contenteditable]'))return;
if(['ArrowDown','ArrowRight',' ','PageDown'].indexOf(e.key)>-1){e.preventDefault();self.next();}
else if(['ArrowUp','ArrowLeft','PageUp'].indexOf(e.key)>-1){e.preventDefault();self.prev();}
else if(e.key==='Home'){e.preventDefault();self.goTo(0);}
else if(e.key==='End'){e.preventDefault();self.goTo(self.total-1);}
self.fadeHints();
});
var tY;
this.deck.addEventListener('touchstart',function(e){tY=e.touches[0].clientY;},{passive:true});
this.deck.addEventListener('touchend',function(e){var dy=tY-e.changedTouches[0].clientY;if(Math.abs(dy)>50){dy>0?self.next():self.prev();}});
};
SlideEngine.prototype.observe=function(){
var self=this;
var obs=new IntersectionObserver(function(entries){entries.forEach(function(entry){if(entry.isIntersecting){entry.target.classList.add('visible');self.current=self.slides.indexOf(entry.target);self.update();}});},{threshold:0.5});
this.slides.forEach(function(s){obs.observe(s);});
};
SlideEngine.prototype.goTo=function(i){this.slides[Math.max(0,Math.min(i,this.total-1))].scrollIntoView({behavior:'smooth'});};
SlideEngine.prototype.next=function(){if(this.current<this.total-1)this.goTo(this.current+1);};
SlideEngine.prototype.prev=function(){if(this.current>0)this.goTo(this.current-1);};
SlideEngine.prototype.update=function(){
this.bar.style.width=((this.current+1)/this.total*100)+'%';
var c=this.current;this.dots.forEach(function(d,i){d.classList.toggle('active',i===c);});
this.counter.textContent=(this.current+1)+' / '+this.total;
};
SlideEngine.prototype.fadeHints=function(){clearTimeout(this.hintTimer);this.hints.classList.add('faded');};
</script>
</body>
</html>