Skip to content

Instantly share code, notes, and snippets.

@daneuchar
Last active March 30, 2026 15:18
Show Gist options
  • Select an option

  • Save daneuchar/07c27feaf36ac02e40120d19b6e2072b to your computer and use it in GitHub Desktop.

Select an option

Save daneuchar/07c27feaf36ac02e40120d19b6e2072b to your computer and use it in GitHub Desktop.
workflwo
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Feature Development Workflow</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&family=Space+Grotesk:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
<style>
*, *::before, *::after { margin: 0; padding: 0; box-sizing: border-box; }
:root {
--bg-deepest: #050810;
--bg-primary: #0a0f1c;
--bg-elevated: #111827;
--bg-surface: #1e293b;
--text-primary: #e2e8f0;
--text-secondary:#94a3b8;
--text-tertiary: #64748b;
--text-ghost: #334155;
--accent-teal: #64ffda;
--accent-purple: #a78bfa;
--accent-blue: #60a5fa;
--accent-amber: #fbbf24;
--accent-rose: #fb7185;
--accent-green: #34d399;
--border-subtle: rgba(148,163,184,0.08);
--border-accent: rgba(100,255,218,0.25);
}
html, body {
width: 100%; height: 100%;
overflow: hidden;
background: var(--bg-deepest);
color: var(--text-primary);
font-family: 'Inter', -apple-system, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
/* ── Ambient Background ── */
.ambient-bg {
position: fixed; inset: 0; z-index: 0;
background: linear-gradient(135deg, #050810 0%, #0a0f1c 25%, #0d1520 50%, #10172a 75%, #050810 100%);
background-size: 400% 400%;
animation: gradientShift 25s ease-in-out infinite;
}
@keyframes gradientShift {
0% { background-position: 0% 0%; }
25% { background-position: 100% 0%; }
50% { background-position: 100% 100%; }
75% { background-position: 0% 100%; }
100% { background-position: 0% 0%; }
}
.grid-overlay {
position: fixed; inset: 0; z-index: 1; pointer-events: none;
background-image:
linear-gradient(rgba(100,255,218,0.02) 1px, transparent 1px),
linear-gradient(90deg, rgba(100,255,218,0.02) 1px, transparent 1px);
background-size: 80px 80px;
}
.radial-glow {
position: fixed; inset: 0; z-index: 1; pointer-events: none;
background: radial-gradient(ellipse 800px 600px at 30% 40%, rgba(100,255,218,0.04), transparent 70%),
radial-gradient(ellipse 600px 500px at 70% 60%, rgba(167,139,250,0.03), transparent 70%);
animation: glowDrift 20s ease-in-out infinite alternate;
}
@keyframes glowDrift {
0% { opacity: 1; transform: translate(0, 0); }
100% { opacity: 0.7; transform: translate(40px, -30px); }
}
.noise-overlay {
position: fixed; inset: 0; z-index: 2; pointer-events: none;
opacity: 0.03;
background-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 256 256' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.85' numOctaves='4' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E");
}
/* ── Particles ── */
.particles { position: fixed; inset: 0; z-index: 2; pointer-events: none; overflow: hidden; }
.particle {
position: absolute; border-radius: 50%;
background: radial-gradient(circle, rgba(100,255,218,0.5), transparent 70%);
animation: particleFloat linear infinite;
}
@keyframes particleFloat {
0% { transform: translateY(100vh) scale(0); opacity: 0; }
10% { opacity: 0.8; transform: translateY(90vh) scale(1); }
90% { opacity: 0.3; }
100% { transform: translateY(-5vh) scale(0.3); opacity: 0; }
}
/* ── Progress Bar ── */
.progress-bar {
position: fixed; top: 0; left: 0; height: 3px; z-index: 100;
background: linear-gradient(90deg, var(--accent-teal), var(--accent-purple));
box-shadow: 0 0 12px rgba(100,255,218,0.4);
transition: width 0.7s cubic-bezier(0.16, 1, 0.3, 1);
width: 0%;
}
/* ── Slide Counter ── */
.slide-counter {
position: fixed; bottom: 2rem; right: 2.5rem; z-index: 100;
font-family: 'JetBrains Mono', monospace; font-size: 0.8rem;
color: var(--text-tertiary); letter-spacing: 0.15em;
opacity: 0.6;
}
/* ── Nav Hint ── */
.nav-hint {
position: fixed; bottom: 2rem; left: 50%; transform: translateX(-50%); z-index: 100;
font-size: 0.75rem; color: var(--text-ghost); letter-spacing: 0.1em;
animation: hintPulse 3s ease-in-out infinite;
}
@keyframes hintPulse { 0%,100% { opacity: 0.3; } 50% { opacity: 0.7; } }
/* ── Slides Container ── */
.slides-container { position: fixed; inset: 0; z-index: 10; }
.slide {
position: absolute; inset: 0;
display: flex; flex-direction: column;
justify-content: center; align-items: center;
padding: 4rem 8rem;
opacity: 0; pointer-events: none;
will-change: transform, opacity, clip-path;
}
.slide.active { opacity: 1; pointer-events: all; }
/* ── Typography ── */
h1 { font-family: 'Space Grotesk', sans-serif; font-size: clamp(2.5rem, 5vw, 4.2rem); font-weight: 700; letter-spacing: -0.03em; line-height: 1.15; }
h2 { font-family: 'Space Grotesk', sans-serif; font-size: clamp(1.8rem, 3.5vw, 2.8rem); font-weight: 600; letter-spacing: -0.02em; line-height: 1.2; }
h3 { font-family: 'Space Grotesk', sans-serif; font-size: clamp(1.3rem, 2.5vw, 1.8rem); font-weight: 500; letter-spacing: -0.01em; }
p, li { font-size: clamp(1rem, 1.5vw, 1.25rem); line-height: 1.75; color: var(--text-secondary); }
.mono { font-family: 'JetBrains Mono', monospace; }
.uppercase { text-transform: uppercase; letter-spacing: 0.2em; font-size: 0.75rem; font-weight: 500; }
/* ── Accent Text ── */
.text-teal { color: var(--accent-teal); }
.text-purple { color: var(--accent-purple); }
.text-blue { color: var(--accent-blue); }
.text-amber { color: var(--accent-amber); }
.text-rose { color: var(--accent-rose); }
.text-green { color: var(--accent-green); }
.glow-teal { text-shadow: 0 0 20px rgba(100,255,218,0.3), 0 0 60px rgba(100,255,218,0.1); }
.glow-purple { text-shadow: 0 0 20px rgba(167,139,250,0.3), 0 0 60px rgba(167,139,250,0.1); }
.glow-blue { text-shadow: 0 0 20px rgba(96,165,250,0.3), 0 0 60px rgba(96,165,250,0.1); }
.glow-amber { text-shadow: 0 0 20px rgba(251,191,36,0.3), 0 0 60px rgba(251,191,36,0.1); }
.glow-green { text-shadow: 0 0 20px rgba(52,211,153,0.3), 0 0 60px rgba(52,211,153,0.1); }
/* ── Layout Utilities ── */
.slide-content { max-width: 1100px; width: 100%; }
.flex-row { display: flex; gap: 2.5rem; align-items: flex-start; }
.flex-col { display: flex; flex-direction: column; gap: 1rem; }
.flex-1 { flex: 1; }
.text-center { text-align: center; }
.mb-1 { margin-bottom: 0.5rem; } .mb-2 { margin-bottom: 1rem; } .mb-3 { margin-bottom: 1.5rem; } .mb-4 { margin-bottom: 2rem; } .mb-6 { margin-bottom: 3rem; }
.mt-2 { margin-top: 1rem; } .mt-4 { margin-top: 2rem; }
/* ── Glow Divider ── */
.glow-divider {
height: 2px; border: none; margin: 2rem 0;
background: linear-gradient(90deg, transparent, var(--accent-teal), transparent);
box-shadow: 0 0 15px rgba(100,255,218,0.2);
}
/* ── Cards ── */
.card {
background: rgba(17,24,39,0.6); border: 1px solid var(--border-subtle);
border-radius: 16px; padding: 2rem; backdrop-filter: blur(10px);
transition: border-color 0.4s ease, box-shadow 0.4s ease;
}
.card:hover { border-color: var(--border-accent); box-shadow: 0 0 30px rgba(100,255,218,0.06); }
/* ── Phase Badge ── */
.phase-badge {
display: inline-flex; align-items: center; gap: 0.6rem;
padding: 0.4rem 1rem; border-radius: 100px; font-size: 0.8rem;
font-weight: 600; letter-spacing: 0.1em; text-transform: uppercase;
border: 1px solid; margin-bottom: 1.5rem;
}
.phase-badge.teal { color: var(--accent-teal); border-color: rgba(100,255,218,0.3); background: rgba(100,255,218,0.06); }
.phase-badge.purple { color: var(--accent-purple); border-color: rgba(167,139,250,0.3); background: rgba(167,139,250,0.06); }
.phase-badge.blue { color: var(--accent-blue); border-color: rgba(96,165,250,0.3); background: rgba(96,165,250,0.06); }
.phase-badge.amber { color: var(--accent-amber); border-color: rgba(251,191,36,0.3); background: rgba(251,191,36,0.06); }
.phase-badge.green { color: var(--accent-green); border-color: rgba(52,211,153,0.3); background: rgba(52,211,153,0.06); }
/* ── Bullet List ── */
.bullet-list { list-style: none; display: flex; flex-direction: column; gap: 0.9rem; }
.bullet-list li {
position: relative; padding-left: 1.6rem; line-height: 1.65;
}
.bullet-list li::before {
content: ''; position: absolute; left: 0; top: 0.65em;
width: 6px; height: 6px; border-radius: 50%;
background: var(--accent-teal);
box-shadow: 0 0 8px rgba(100,255,218,0.4);
}
.bullet-list.purple li::before { background: var(--accent-purple); box-shadow: 0 0 8px rgba(167,139,250,0.4); }
.bullet-list.blue li::before { background: var(--accent-blue); box-shadow: 0 0 8px rgba(96,165,250,0.4); }
.bullet-list.amber li::before { background: var(--accent-amber); box-shadow: 0 0 8px rgba(251,191,36,0.4); }
.bullet-list.green li::before { background: var(--accent-green); box-shadow: 0 0 8px rgba(52,211,153,0.4); }
/* ── Pipeline ── */
.pipeline { display: flex; align-items: center; gap: 0; justify-content: center; flex-wrap: wrap; }
.pipeline-node {
display: flex; flex-direction: column; align-items: center; justify-content: center;
width: 140px; height: 90px; border-radius: 14px; text-align: center;
font-size: 0.8rem; font-weight: 600; letter-spacing: 0.03em;
border: 1px solid; position: relative;
}
.pipeline-arrow {
width: 40px; height: 2px; position: relative; flex-shrink: 0;
}
.pipeline-arrow::after {
content: ''; position: absolute; right: -1px; top: 50%; transform: translateY(-50%);
border: 5px solid transparent; border-left: 7px solid;
}
/* ── Table ── */
.data-table { width: 100%; border-collapse: collapse; font-size: 0.95rem; }
.data-table th {
text-align: left; padding: 0.8rem 1rem; font-weight: 600; font-size: 0.8rem;
text-transform: uppercase; letter-spacing: 0.1em; color: var(--text-tertiary);
border-bottom: 1px solid var(--border-subtle);
}
.data-table td {
padding: 0.75rem 1rem; color: var(--text-secondary);
border-bottom: 1px solid rgba(148,163,184,0.04);
}
.data-table tr { transition: background 0.3s ease; }
.data-table tbody tr:hover { background: rgba(100,255,218,0.02); }
/* ── Big Number ── */
.big-number {
font-family: 'Space Grotesk', sans-serif; font-size: 8rem; font-weight: 700;
line-height: 1; letter-spacing: -0.05em; opacity: 0.1;
position: absolute; right: 4rem; top: 3rem;
}
/* ── Flowchart SVG ── */
.flowchart { width: 100%; max-width: 900px; margin: 0 auto; }
.flowchart svg { width: 100%; height: auto; }
.flow-line {
fill: none; stroke-width: 2; stroke-linecap: round;
}
.flow-node-rect {
rx: 12; ry: 12; stroke-width: 1.5;
}
.flow-node-text {
font-family: 'Inter', sans-serif; font-size: 13px; font-weight: 500;
fill: var(--text-primary); text-anchor: middle; dominant-baseline: central;
}
/* ── Stat Blocks ── */
.stat-grid { display: grid; grid-template-columns: repeat(4, 1fr); gap: 1.2rem; }
.stat-block {
background: rgba(17,24,39,0.5); border: 1px solid var(--border-subtle);
border-radius: 14px; padding: 1.5rem; text-align: center;
}
.stat-value { font-family: 'Space Grotesk', sans-serif; font-size: 2rem; font-weight: 700; }
.stat-label { font-size: 0.8rem; color: var(--text-tertiary); margin-top: 0.3rem; }
/* ── Title Chars ── */
.char-wrap { display: inline-block; overflow: hidden; }
.char-inner { display: inline-block; }
/* ── Markdown Render ── */
.md-render { font-size: 0.78rem; line-height: 1.75; color: var(--text-secondary); }
.md-render h1 { font-size: 1.15rem; font-weight: 700; color: var(--text-primary); margin: 0 0 0.5rem 0; }
.md-render h2 { font-size: 0.95rem; font-weight: 600; color: var(--accent-purple); margin: 1.2rem 0 0.5rem 0; }
.md-render h3 { font-size: 0.82rem; font-weight: 600; color: var(--text-primary); margin: 1rem 0 0.4rem 0; }
.md-render hr { border: none; border-top: 1px solid rgba(255,255,255,0.06); margin: 1rem 0; }
.md-render p { margin: 0.4rem 0; }
.md-render strong { color: var(--text-primary); font-weight: 600; }
.md-render em { color: var(--text-tertiary); font-style: italic; }
.md-render code { font-family: 'JetBrains Mono', monospace; font-size: 0.72rem; background: rgba(167,139,250,0.08); color: var(--accent-purple); padding: 0.15em 0.4em; border-radius: 4px; }
.md-render pre { background: rgba(10,12,20,0.6); border: 1px solid rgba(255,255,255,0.06); border-radius: 8px; padding: 0.8rem 1rem; margin: 0.6rem 0; overflow-x: auto; }
.md-render pre code { background: none; padding: 0; color: var(--accent-teal); font-size: 0.72rem; }
.md-render blockquote { border-left: 3px solid var(--accent-purple); padding: 0.5rem 1rem; margin: 0.6rem 0; background: rgba(167,139,250,0.04); border-radius: 0 8px 8px 0; color: var(--text-tertiary); font-style: italic; }
.md-render ul { padding-left: 1.2rem; margin: 0.4rem 0; }
.md-render li { margin: 0.2rem 0; }
.md-render li::marker { color: var(--accent-purple); }
.md-render table { width: 100%; border-collapse: collapse; margin: 0.6rem 0; font-size: 0.74rem; }
.md-render th { text-align: left; padding: 0.5rem 0.8rem; border-bottom: 1px solid rgba(255,255,255,0.1); color: var(--text-ghost); text-transform: uppercase; letter-spacing: 0.08em; font-size: 0.68rem; font-weight: 500; }
.md-render td { padding: 0.45rem 0.8rem; border-bottom: 1px solid rgba(255,255,255,0.04); }
/* ── File Tree ── */
.file-tree {
font-family: 'JetBrains Mono', monospace; font-size: 0.82rem; line-height: 2;
color: var(--text-tertiary); user-select: none;
}
.file-tree .tree-line { white-space: nowrap; display: flex; align-items: center; gap: 0.4rem; }
.file-tree .tree-indent { color: var(--text-ghost); flex-shrink: 0; }
.file-tree .tree-icon { flex-shrink: 0; font-size: 0.9rem; }
.file-tree .tree-name { color: var(--text-secondary); }
.file-tree .tree-name.folder { color: var(--text-tertiary); }
.file-tree .tree-name.orchestrator {
color: var(--accent-teal); font-weight: 600;
text-shadow: 0 0 12px rgba(100,255,218,0.3);
}
.file-tree .tree-name.module {
color: var(--accent-purple);
}
.file-tree .tree-tag {
font-size: 0.6rem; padding: 0.1rem 0.45rem; border-radius: 4px;
font-weight: 600; letter-spacing: 0.08em; text-transform: uppercase;
margin-left: 0.5rem; vertical-align: middle;
}
.file-tree .tree-tag.orch { color: var(--accent-teal); background: rgba(100,255,218,0.08); border: 1px solid rgba(100,255,218,0.2); }
.file-tree .tree-tag.ctx { color: var(--accent-purple); background: rgba(167,139,250,0.08); border: 1px solid rgba(167,139,250,0.2); }
/* ── Orchestrator Hub ── */
.orch-hub {
position: relative; display: flex; flex-direction: column;
align-items: center; justify-content: center; gap: 0.6rem;
}
.orch-node {
width: 160px; padding: 1rem; border-radius: 16px; text-align: center;
border: 1.5px solid rgba(100,255,218,0.35);
background: rgba(100,255,218,0.05);
box-shadow: 0 0 30px rgba(100,255,218,0.08), inset 0 0 20px rgba(100,255,218,0.03);
position: relative; z-index: 2;
}
.orch-node .orch-label { font-family: 'JetBrains Mono', monospace; font-size: 0.8rem; color: var(--accent-teal); font-weight: 600; }
.orch-node .orch-sub { font-size: 0.7rem; color: var(--text-tertiary); margin-top: 0.2rem; }
.orch-pulse {
position: absolute; inset: -8px; border-radius: 20px; z-index: 1;
border: 1px solid rgba(100,255,218,0.15);
animation: orchPulse 3s ease-in-out infinite;
}
@keyframes orchPulse {
0%, 100% { opacity: 0.3; transform: scale(1); }
50% { opacity: 0.8; transform: scale(1.06); }
}
/* ── Connection Lines (CSS animated) ── */
.conn-line {
position: absolute; height: 1.5px;
background: linear-gradient(90deg, rgba(167,139,250,0.4), rgba(100,255,218,0.5));
transform-origin: left center;
z-index: 0;
}
.conn-line::after {
content: ''; position: absolute; right: -2px; top: 50%; transform: translateY(-50%);
width: 5px; height: 5px; border-radius: 50%;
background: var(--accent-teal);
box-shadow: 0 0 8px rgba(100,255,218,0.6);
}
.conn-dot-travel {
position: absolute; width: 4px; height: 4px; border-radius: 50%;
background: var(--accent-teal);
box-shadow: 0 0 10px rgba(100,255,218,0.8), 0 0 20px rgba(100,255,218,0.3);
z-index: 3;
}
/* ── Reveal Items ── */
.reveal-item { opacity: 0; transform: translateY(30px); }
/* ── Responsive ── */
@media (max-width: 900px) {
.slide { padding: 3rem 2rem; }
.flex-row { flex-direction: column; }
.stat-grid { grid-template-columns: repeat(2, 1fr); }
.pipeline { flex-direction: column; }
.pipeline-arrow { width: 2px; height: 30px; }
.big-number { font-size: 5rem; right: 1.5rem; top: 1.5rem; }
}
</style>
</head>
<body>
<div class="ambient-bg"></div>
<div class="grid-overlay"></div>
<div class="radial-glow"></div>
<div class="noise-overlay"></div>
<div class="particles" id="particles"></div>
<div class="progress-bar" id="progressBar"></div>
<div class="slide-counter" id="slideCounter"></div>
<div class="nav-hint" id="navHint">ARROW KEYS TO NAVIGATE</div>
<div class="slides-container">
<!-- ═══════ SLIDE 0: Title ═══════ -->
<div class="slide active" data-index="0">
<div class="slide-content text-center">
<p class="uppercase text-teal reveal-item mb-4" style="font-size:0.85rem;">Thoughtworks UBS Engineering Process</p>
<h1 class="mb-3">
<span class="title-text">AI Assisted Development</span><br>
<span class="title-text text-teal glow-teal">Workflow</span>
</h1>
<hr class="glow-divider reveal-item" style="max-width:200px;margin:1.5rem auto;">
<p class="reveal-item" style="max-width:600px;margin:0 auto;">A structured, five-phase delivery process that ensures consistent quality while maintaining velocity as the team scales.</p>
<div class="mt-4 reveal-item">
<span class="mono text-ghost" style="font-size:0.85rem;">Daniel Euchar, Thoughtworks &mdash; 2026</span>
</div>
</div>
</div>
<!-- ═══════ SLIDE 1: Overview Pipeline ═══════ -->
<div class="slide" data-index="1">
<div class="slide-content text-center">
<p class="uppercase text-teal reveal-item mb-3">End-to-End Overview</p>
<h2 class="reveal-item mb-6">Five Phases, One Feedback Loop</h2>
<div class="pipeline reveal-item">
<div class="pipeline-node" style="background:rgba(10,15,28,0.8);border-color:rgba(148,163,184,0.2);color:var(--text-secondary);">
<span style="font-size:1.4rem;margin-bottom:0.2rem;">0</span>
<span style="font-size:0.7rem;opacity:0.7;">AI Bootstrap</span>
<span style="font-size:0.6rem;color:var(--text-ghost);">ONE-TIME</span>
</div>
<div class="pipeline-arrow" style="background:var(--text-ghost);">
<span style="position:absolute;right:-1px;top:50%;transform:translateY(-50%);border:5px solid transparent;border-left:7px solid var(--text-ghost);"></span>
</div>
<div class="pipeline-node" style="background:rgba(99,102,241,0.1);border-color:rgba(99,102,241,0.3);color:var(--accent-purple);">
<span style="font-size:1.4rem;margin-bottom:0.2rem;">1</span>
<span style="font-size:0.7rem;">Planning</span>
</div>
<div class="pipeline-arrow" style="background:var(--accent-purple);opacity:0.4;">
<span style="position:absolute;right:-1px;top:50%;transform:translateY(-50%);border:5px solid transparent;border-left:7px solid var(--accent-purple);opacity:0.4;"></span>
</div>
<div class="pipeline-node" style="background:rgba(251,191,36,0.08);border-color:rgba(251,191,36,0.3);color:var(--accent-amber);">
<span style="font-size:1.4rem;margin-bottom:0.2rem;">2</span>
<span style="font-size:0.7rem;">Build & Test</span>
</div>
<div class="pipeline-arrow" style="background:var(--accent-amber);opacity:0.4;">
<span style="position:absolute;right:-1px;top:50%;transform:translateY(-50%);border:5px solid transparent;border-left:7px solid var(--accent-amber);opacity:0.4;"></span>
</div>
<div class="pipeline-node" style="background:rgba(96,165,250,0.08);border-color:rgba(96,165,250,0.3);color:var(--accent-blue);">
<span style="font-size:1.4rem;margin-bottom:0.2rem;">3</span>
<span style="font-size:0.7rem;">Iteration</span>
</div>
<div class="pipeline-arrow" style="background:var(--accent-blue);opacity:0.4;">
<span style="position:absolute;right:-1px;top:50%;transform:translateY(-50%);border:5px solid transparent;border-left:7px solid var(--accent-blue);opacity:0.4;"></span>
</div>
<div class="pipeline-node" style="background:rgba(52,211,153,0.08);border-color:rgba(52,211,153,0.3);color:var(--accent-green);">
<span style="font-size:1.4rem;margin-bottom:0.2rem;">4</span>
<span style="font-size:0.7rem;">Deploy &</span>
<span style="font-size:0.7rem;">Validate</span>
</div>
</div>
<div class="mt-4 reveal-item" style="display:flex;justify-content:center;align-items:center;gap:0.6rem;">
<svg width="20" height="20" viewBox="0 0 20 20" fill="none"><path d="M15 10C15 12.7614 12.7614 15 10 15C7.23858 15 5 12.7614 5 10C5 7.23858 7.23858 5 10 5" stroke="var(--accent-teal)" stroke-width="1.5" stroke-linecap="round"/><path d="M10 2V5L12 3" stroke="var(--accent-teal)" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/></svg>
<span class="text-teal" style="font-size:0.9rem;">Learnings feeds back into documentation</span>
</div>
</div>
</div>
<!-- ═══════ SLIDE 2: Phase 0 ═══════ -->
<div class="slide" data-index="2">
<div class="big-number text-ghost">0</div>
<div class="slide-content">
<div class="phase-badge teal">Phase 0 &mdash; One-Time Setup</div>
<h2 class="reveal-item mb-2">AI Knowledge Bootstrap</h2>
<p class="reveal-item mb-4" style="max-width:700px;">Give AI tools deep, structured understanding of the codebase so every future cycle starts with accurate context &mdash; not guesswork.</p>
<div class="flex-row">
<div class="flex-1">
<h3 class="reveal-item text-teal mb-3" style="font-size:1.1rem;">What Happens</h3>
<ul class="bullet-list">
<li class="reveal-item">AI scans entire codebase and generates structured module documentation</li>
<li class="reveal-item">Developer domain experts review and enrich every doc &mdash; AI captures <em>what</em>, developers add the <em>why</em></li>
<li class="reveal-item">Tool instruction files configured (<span class="mono text-teal" style="font-size:0.9em;">claude.md</span>, Copilot instructions)</li>
<li class="reveal-item">Enforcement rules set &mdash; AI must consult docs and conventions before any plan</li>
</ul>
</div>
<div class="flex-1">
<div class="card reveal-item">
<h3 class="mb-2" style="font-size:0.95rem;color:var(--text-primary);">Module Doc Template</h3>
<table class="data-table" style="font-size:0.85rem;">
<tbody>
<tr><td class="text-teal" style="width:45%;">Overview & API</td><td class="text-ghost">AI-generated</td></tr>
<tr><td class="text-teal">Dependencies</td><td class="text-ghost">AI-generated</td></tr>
<tr><td class="text-purple">Architecture & Decisions</td><td class="text-ghost">Dev-authored</td></tr>
<tr><td class="text-purple">Tribal Knowledge</td><td class="text-ghost">Dev-authored</td></tr>
<tr><td class="text-amber">Constraints & Rules</td><td class="text-ghost">Both</td></tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
<!-- ═══════ SLIDE 3: Phase 0 Enforcement ═══════ -->
<div class="slide" data-index="3">
<div class="big-number text-ghost">0</div>
<div class="slide-content">
<div class="phase-badge teal">Phase 0 &mdash; Orchestration</div>
<h2 class="reveal-item mb-2">Instruction Files as Orchestrators</h2>
<p class="reveal-item mb-3" style="max-width:750px;">Instruction files sit at the root and gather context from every module doc before the AI can plan or write code.</p>
<!-- File Tree + Orchestrator Diagram -->
<div class="flex-row reveal-item" style="align-items:stretch;gap:1.5rem;">
<!-- LEFT: File Tree -->
<div class="card" style="flex:1.1;padding:1.5rem 1.8rem;">
<h3 style="font-size:0.8rem;color:var(--text-ghost);margin-bottom:0.8rem;letter-spacing:0.15em;text-transform:uppercase;">Project Structure</h3>
<div class="file-tree" id="fileTree">
<div class="tree-line reveal-item"><span class="tree-icon" style="color:var(--accent-amber);">&#128193;</span> <span class="tree-name folder">project/</span></div>
<div class="tree-line reveal-item"><span class="tree-indent">├──</span> <span class="tree-icon" style="color:var(--accent-amber);">&#128193;</span> <span class="tree-name folder">.github/</span></div>
<div class="tree-line reveal-item" data-orch="2"><span class="tree-indent">│&nbsp;&nbsp; └──</span> <span class="tree-icon">&#128220;</span> <span class="tree-name orchestrator">copilot-instructions.md</span> <span class="tree-tag orch">orchestrator</span></div>
<div class="tree-line reveal-item"><span class="tree-indent">├──</span> <span class="tree-icon" style="color:var(--accent-amber);">&#128193;</span> <span class="tree-name folder">src/</span></div>
<div class="tree-line reveal-item"><span class="tree-indent">│&nbsp;&nbsp; ├──</span> <span class="tree-icon" style="color:var(--accent-amber);">&#128193;</span> <span class="tree-name folder">auth/</span></div>
<div class="tree-line reveal-item" data-module="1"><span class="tree-indent">│&nbsp;&nbsp; │&nbsp;&nbsp; └──</span> <span class="tree-icon">&#128196;</span> <span class="tree-name module">module.md</span> <span class="tree-tag ctx">context</span></div>
<div class="tree-line reveal-item"><span class="tree-indent">│&nbsp;&nbsp; ├──</span> <span class="tree-icon" style="color:var(--accent-amber);">&#128193;</span> <span class="tree-name folder">payments/</span></div>
<div class="tree-line reveal-item" data-module="2"><span class="tree-indent">│&nbsp;&nbsp; │&nbsp;&nbsp; └──</span> <span class="tree-icon">&#128196;</span> <span class="tree-name module">module.md</span> <span class="tree-tag ctx">context</span></div>
<div class="tree-line reveal-item"><span class="tree-indent">│&nbsp;&nbsp; ├──</span> <span class="tree-icon" style="color:var(--accent-amber);">&#128193;</span> <span class="tree-name folder">api/</span></div>
<div class="tree-line reveal-item" data-module="3"><span class="tree-indent">│&nbsp;&nbsp; │&nbsp;&nbsp; └──</span> <span class="tree-icon">&#128196;</span> <span class="tree-name module">module.md</span> <span class="tree-tag ctx">context</span></div>
<div class="tree-line reveal-item"><span class="tree-indent">│&nbsp;&nbsp; └──</span> <span class="tree-icon" style="color:var(--accent-amber);">&#128193;</span> <span class="tree-name folder">database/</span></div>
<div class="tree-line reveal-item" data-module="4"><span class="tree-indent">│&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; └──</span> <span class="tree-icon">&#128196;</span> <span class="tree-name module">module.md</span> <span class="tree-tag ctx">context</span></div>
<div class="tree-line reveal-item"><span class="tree-indent">└──</span> <span class="tree-icon">&#128220;</span> <span class="tree-name" style="color:var(--text-ghost);">CONVENTIONS.md</span></div>
</div>
</div>
<!-- RIGHT: Orchestration Flow -->
<div style="flex:0.9;display:flex;flex-direction:column;gap:1.2rem;">
<!-- Hub Diagram -->
<div class="card reveal-item" style="flex:1;display:flex;align-items:center;justify-content:center;position:relative;overflow:hidden;min-height:220px;">
<!-- SVG Connection Diagram -->
<svg viewBox="0 0 520 260" fill="none" xmlns="http://www.w3.org/2000/svg" style="width:100%;height:100%;position:absolute;inset:0;padding:10px;">
<!-- Module nodes (left side) -->
<rect class="flow-node-rect" x="10" y="12" width="160" height="42" rx="8" fill="rgba(167,139,250,0.06)" stroke="rgba(167,139,250,0.25)"/>
<text class="flow-node-text" x="90" y="38" fill="var(--accent-purple)" font-size="9" text-anchor="middle" dominant-baseline="middle">auth/module.md</text>
<rect class="flow-node-rect" x="10" y="66" width="160" height="42" rx="8" fill="rgba(167,139,250,0.06)" stroke="rgba(167,139,250,0.25)"/>
<text class="flow-node-text" x="90" y="92" fill="var(--accent-purple)" font-size="9" text-anchor="middle" dominant-baseline="middle">payments/module.md</text>
<rect class="flow-node-rect" x="10" y="120" width="160" height="42" rx="8" fill="rgba(167,139,250,0.06)" stroke="rgba(167,139,250,0.25)"/>
<text class="flow-node-text" x="90" y="146" fill="var(--accent-purple)" font-size="9" text-anchor="middle" dominant-baseline="middle">api/module.md</text>
<rect class="flow-node-rect" x="10" y="174" width="160" height="42" rx="8" fill="rgba(167,139,250,0.06)" stroke="rgba(167,139,250,0.25)"/>
<text class="flow-node-text" x="90" y="200" fill="var(--accent-purple)" font-size="9" text-anchor="middle" dominant-baseline="middle">database/module.md</text>
<!-- Connection lines from modules to orchestrator -->
<line class="flow-line" x1="170" y1="33" x2="300" y2="100" stroke="rgba(167,139,250,0.2)"/>
<line class="flow-line" x1="170" y1="87" x2="300" y2="110" stroke="rgba(167,139,250,0.2)"/>
<line class="flow-line" x1="170" y1="141" x2="300" y2="122" stroke="rgba(167,139,250,0.2)"/>
<line class="flow-line" x1="170" y1="195" x2="300" y2="134" stroke="rgba(167,139,250,0.2)"/>
<!-- Central Orchestrator node -->
<rect rx="14" ry="14" x="300" y="76" width="200" height="82" fill="rgba(100,255,218,0.06)" stroke="rgba(100,255,218,0.35)" stroke-width="1.5"/>
<!-- Pulse ring -->
<rect rx="18" ry="18" x="293" y="69" width="214" height="96" fill="none" stroke="rgba(100,255,218,0.1)" stroke-width="1" stroke-dasharray="4 4">
<animate attributeName="opacity" values="0.3;0.8;0.3" dur="3s" repeatCount="indefinite"/>
</rect>
<text class="flow-node-text" x="400" y="110" fill="var(--accent-teal)" font-size="13" font-weight="600" text-anchor="middle" dominant-baseline="middle">claude.md</text>
<text class="flow-node-text" x="400" y="132" fill="var(--text-ghost)" font-size="9" text-anchor="middle" dominant-baseline="middle">gathers context &#x2192; plans</text>
<!-- Arrow from orchestrator to output -->
<line class="flow-line" x1="400" y1="158" x2="400" y2="194" stroke="rgba(100,255,218,0.25)"/>
<rect class="flow-node-rect" x="325" y="194" width="150" height="40" rx="8" fill="rgba(52,211,153,0.06)" stroke="rgba(52,211,153,0.25)"/>
<text class="flow-node-text" x="400" y="218" fill="var(--accent-green)" font-size="9" text-anchor="middle" dominant-baseline="middle">Structured Plan</text>
<!-- Animated dots traveling along lines -->
<circle r="3" fill="var(--accent-teal)" opacity="0.8">
<animate attributeName="cx" values="170;300" dur="2s" repeatCount="indefinite"/>
<animate attributeName="cy" values="33;100" dur="2s" repeatCount="indefinite"/>
<animate attributeName="opacity" values="0;1;1;0" dur="2s" repeatCount="indefinite"/>
</circle>
<circle r="3" fill="var(--accent-teal)" opacity="0.8">
<animate attributeName="cx" values="170;300" dur="2.3s" repeatCount="indefinite" begin="0.4s"/>
<animate attributeName="cy" values="87;110" dur="2.3s" repeatCount="indefinite" begin="0.4s"/>
<animate attributeName="opacity" values="0;1;1;0" dur="2.3s" repeatCount="indefinite" begin="0.4s"/>
</circle>
<circle r="3" fill="var(--accent-teal)" opacity="0.8">
<animate attributeName="cx" values="170;300" dur="2.6s" repeatCount="indefinite" begin="0.8s"/>
<animate attributeName="cy" values="141;122" dur="2.6s" repeatCount="indefinite" begin="0.8s"/>
<animate attributeName="opacity" values="0;1;1;0" dur="2.6s" repeatCount="indefinite" begin="0.8s"/>
</circle>
<circle r="3" fill="var(--accent-teal)" opacity="0.8">
<animate attributeName="cx" values="170;300" dur="2.1s" repeatCount="indefinite" begin="1.2s"/>
<animate attributeName="cy" values="195;134" dur="2.1s" repeatCount="indefinite" begin="1.2s"/>
<animate attributeName="opacity" values="0;1;1;0" dur="2.1s" repeatCount="indefinite" begin="1.2s"/>
</circle>
<!-- Downward pulse dot -->
<circle r="2.5" fill="var(--accent-green)" opacity="0.8">
<animate attributeName="cx" values="400;400" dur="1.5s" repeatCount="indefinite" begin="2s"/>
<animate attributeName="cy" values="158;194" dur="1.5s" repeatCount="indefinite" begin="2s"/>
<animate attributeName="opacity" values="0;1;1;0" dur="1.5s" repeatCount="indefinite" begin="2s"/>
</circle>
</svg>
</div>
<!-- How it works -->
<div class="card reveal-item" style="padding:1.2rem 1.5rem;">
<h3 class="text-teal mb-2" style="font-size:0.85rem;">How the Orchestrator Works</h3>
<ul class="bullet-list" style="font-size:0.82rem;gap:0.5rem;">
<li class="reveal-item">AI reads instruction files <em>first</em> &mdash; they define rules & conventions</li>
<li class="reveal-item">Instruction files reference module docs &mdash; AI loads relevant context</li>
<li class="reveal-item">AI outputs a <strong class="text-teal">structured plan</strong> following the architecture and constraints</li>
<li class="reveal-item">Developer validates the Plan against their own knowledge</li>
</ul>
</div>
</div>
</div>
</div>
</div>
<!-- ═══════ SLIDE 4: Phase 1 ═══════ -->
<div class="slide" data-index="4">
<div class="big-number" style="color:rgba(167,139,250,0.08);">1</div>
<div class="slide-content">
<div class="phase-badge purple">Phase 1</div>
<h2 class="reveal-item mb-2">Planning</h2>
<p class="reveal-item mb-4" style="max-width:700px;">Align on scope, break work into safe chunks, and confirm AI tools have context &mdash; before writing a single line of code.</p>
<div class="flex-row">
<div class="flex-1">
<h3 class="reveal-item text-purple mb-3" style="font-size:1.1rem;">Key Activities</h3>
<ul class="bullet-list purple">
<li class="reveal-item"><strong>AI consults module docs first</strong> &mdash; surfaces constraints before developer plan</li>
<li class="reveal-item"><strong>Time-boxed spikes</strong> for unknowns &mdash; prototyping the riskiest part</li>
<li class="reveal-item"><strong>Decompose into sub-tasks</strong> &mdash; each having logical boundaries and clear success criteria</li>
<li class="reveal-item"><strong>Estimate with ranges</strong> &mdash; "1-4 days, most likely 2"</li>
</ul>
</div>
<div class="flex-1">
<div class="card reveal-item">
<h3 class="mb-3" style="font-size:0.95rem;color:var(--text-primary);">Planning Discipline</h3>
<table class="data-table" style="font-size:0.85rem;">
<thead><tr><th>Rule</th><th>Why</th></tr></thead>
<tbody>
<tr><td>Plan only first 2-3 tasks in detail</td><td class="text-ghost">Refine as understanding grows</td></tr>
<tr><td>Re-evaluate after each sub-task</td><td class="text-ghost">Built into loop, not optional</td></tr>
<tr><td>Feature flags for user-facing changes</td><td class="text-ghost">Decouple deploy from release</td></tr>
<tr><td>Short-lived feature branches</td><td class="text-ghost">Merge within days, not weeks</td></tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
<!-- ═══════ SLIDE 4b: Sample PRD ═══════ -->
<div class="slide" data-index="5">
<div class="big-number" style="color:rgba(167,139,250,0.08);">1</div>
<div class="slide-content">
<div class="phase-badge purple">Phase 1 &mdash; Example</div>
<h2 class="reveal-item mb-3">Sample Planning PRD</h2>
<!-- Markdown rendered in a contained doc box -->
<div class="reveal-item" style="max-width:960px;max-height:72vh;overflow-y:auto;margin:0 auto;background:rgba(15,18,28,0.7);border:1px solid rgba(167,139,250,0.15);border-radius:14px;padding:2.2rem 2.8rem;box-shadow:0 4px 30px rgba(0,0,0,0.3);backdrop-filter:blur(8px);scrollbar-width:thin;scrollbar-color:rgba(167,139,250,0.25) transparent;">
<!-- Title bar -->
<div style="display:flex;align-items:center;gap:0.6rem;margin-bottom:1.5rem;padding-bottom:1rem;border-bottom:1px solid rgba(255,255,255,0.06);">
<div style="display:flex;gap:6px;">
<span style="width:10px;height:10px;border-radius:50%;background:#ff5f57;"></span>
<span style="width:10px;height:10px;border-radius:50%;background:#febc2e;"></span>
<span style="width:10px;height:10px;border-radius:50%;background:#28c840;"></span>
</div>
<span style="font-family:'JetBrains Mono',monospace;font-size:0.75rem;color:var(--text-ghost);margin-left:0.5rem;">samplePrd.md</span>
</div>
<!-- Rendered markdown content -->
<div class="md-render">
<h1>Lineage Radius Feature &mdash; Product Requirements Document</h1>
<hr>
<h2>Overview</h2>
<p>Add a <strong>Radius control</strong> to the data product lineage canvas that allows users to dynamically adjust the depth of lineage visualization by re-fetching data with different radius values.</p>
<hr>
<h2>Scope</h2>
<table>
<tbody>
<tr><td><strong>In Scope</strong></td><td>Data product lineage (port-level view) in <code>FlowCanvas.tsx</code></td></tr>
<tr><td><strong>Out of Scope</strong></td><td>Column-level lineage in <code>ColumnLineageCanvas.tsx</code></td></tr>
</tbody>
</table>
<hr>
<h2>User Story</h2>
<blockquote>As a data product user, I want to control the depth of lineage visualization so that I can explore relationships at different levels without being overwhelmed by too much information.</blockquote>
<hr>
<h2>Acceptance Criteria</h2>
<ul>
<li>&#9744; Radius control appears in the ReactFlow Controls panel</li>
<li>&#9744; Clicking control allows user to select radius (1&ndash;5)</li>
<li>&#9744; Changing radius triggers an API call with the new radius parameter</li>
<li>&#9744; Graph updates with new nodes and recalculated edges</li>
<li>&#9744; Loading state displays during re-fetch</li>
<li>&#9744; Control is only visible for data product lineage (not column lineage)</li>
<li>&#9744; Default radius is <code>2</code></li>
<li>&#9744; All existing tests pass</li>
<li>&#9744; New unit tests cover radius functionality</li>
</ul>
<hr>
<h2>Implementation Tasks</h2>
<h3>1. API Enhancement</h3>
<p><strong>File:</strong> <code>src/features/lineage/api/index.ts</code></p>
<p>Update the <code>getLineage</code> endpoint to support an optional <code>radius</code> query parameter.</p>
<ul>
<li>Add <code>radius?: number</code> to the <code>QueryParams</code> type</li>
<li>Append <code>radius</code> to the URL query string when provided</li>
<li>Default behavior remains unchanged if <code>radius</code> is omitted</li>
</ul>
<h3>2. Radius Control Component</h3>
<p><strong>File:</strong> <code>src/features/lineage/components/RadiusControlButton.tsx</code> <em>(new)</em></p>
<ul>
<li>Extend ReactFlow&rsquo;s <code>ControlButton</code></li>
<li>Display the current radius value</li>
<li>Support radius options: <code>1, 2, 3, 4, 5</code></li>
<li>Use a dropdown or increment/decrement UI pattern</li>
</ul>
<h3>3. Integrate Control into Canvas</h3>
<p><strong>File:</strong> <code>src/features/lineage/components/CustomReactFlow.tsx</code></p>
<ul>
<li>Accept <code>radius</code> and <code>onRadiusChange</code> as props</li>
<li>Render control inside existing <code>&lt;Controls&gt;</code> component</li>
<li>Only render when lineage data is available</li>
</ul>
<h3>4. State Management in FlowCanvas</h3>
<p><strong>File:</strong> <code>src/features/lineage/components/FlowCanvas.tsx</code></p>
<ul>
<li>Add <code>radius</code> state with default value of <code>2</code></li>
<li>Add <code>onRadiusChange</code> callback prop</li>
<li>Trigger the callback when radius changes</li>
</ul>
<h3>5. Parent Component Integration</h3>
<p><strong>File:</strong> <code>src/features/lineage/components/Lineage.tsx</code></p>
<ul>
<li>Add <code>radius</code> state variable</li>
<li>Pass <code>radius</code> to <code>useGetLineageQuery</code> hook</li>
<li>Implement <code>handleRadiusChange</code> callback</li>
</ul>
<h3>6. Constants &amp; Configuration</h3>
<p><strong>File:</strong> <code>src/features/lineage/constants.ts</code></p>
<pre><code>DEFAULT_RADIUS = 2
RADIUS_OPTIONS = [1, 2, 3, 4, 5]
MIN_RADIUS = 1
MAX_RADIUS = 5</code></pre>
<h3>7. Testing</h3>
<table>
<thead><tr><th>File</th><th>Type</th><th>Details</th></tr></thead>
<tbody>
<tr><td><code>RadiusControlButton.test.tsx</code></td><td>New</td><td>Radius display, change interactions, callback</td></tr>
<tr><td><code>FlowCanvas.test.tsx</code></td><td>Update</td><td>Radius prop propagation, change handler</td></tr>
<tr><td><code>Lineage.test.tsx</code></td><td>Update</td><td>Radius state management, API re-fetch</td></tr>
</tbody>
</table>
<hr>
<h2>Technical Considerations</h2>
<ul>
<li><code>radius</code> parameter controls how many hops/levels to fetch</li>
<li>Show a loading indicator during re-fetch</li>
<li>Optionally debounce rapid radius changes</li>
<li>Cache previous radius results via RTK Query</li>
</ul>
<hr>
<h2>Dependencies</h2>
<table>
<thead><tr><th>Dependency</th><th>Status</th></tr></thead>
<tbody>
<tr><td><code>@xyflow/react</code> (ReactFlow)</td><td>Existing</td></tr>
<tr><td>RTK Query</td><td>Existing</td></tr>
<tr><td>Lineage API with <code>radius</code> support</td><td style="color:var(--accent-amber);">Required from backend</td></tr>
</tbody>
</table>
<hr>
<h2>Estimated Complexity</h2>
<p><strong>Medium</strong> &mdash; Requires an API update, a new component, state coordination across multiple layers, and test coverage.</p>
</div>
</div>
</div>
</div>
<!-- ═══════ SLIDE 5: Phase 2 ═══════ -->
<div class="slide" data-index="6">
<div class="big-number" style="color:rgba(251,191,36,0.08);">2</div>
<div class="slide-content">
<div class="phase-badge amber">Phase 2</div>
<h2 class="reveal-item mb-2">Build & Test Loop</h2>
<p class="reveal-item mb-4" style="max-width:700px;">Each sub-task follows a tight cycle. Automated gates run in parallel. AI assists but developer makes the final call.</p>
<div class="flowchart reveal-item mb-4">
<svg viewBox="0 0 900 180" fill="none" xmlns="http://www.w3.org/2000/svg">
<!-- Implement -->
<rect class="flow-node-rect" x="0" y="55" width="120" height="60" fill="rgba(251,191,36,0.1)" stroke="rgba(251,191,36,0.35)"/>
<text class="flow-node-text" x="60" y="85" fill="var(--accent-amber)" font-size="12">Implement</text>
<line class="flow-line" x1="120" y1="85" x2="170" y2="85" stroke="rgba(251,191,36,0.25)"/>
<!-- Auto Gates Group -->
<rect rx="14" ry="14" x="170" y="5" width="380" height="170" fill="rgba(20,184,166,0.03)" stroke="rgba(20,184,166,0.15)" stroke-dasharray="6 4"/>
<text x="185" y="25" fill="var(--text-ghost)" font-size="10" font-family="Inter">AUTOMATED GATES (PARALLEL)</text>
<rect class="flow-node-rect" x="185" y="40" width="100" height="44" fill="rgba(20,184,166,0.08)" stroke="rgba(20,184,166,0.25)"/>
<text class="flow-node-text" x="235" y="62" fill="var(--accent-green)" font-size="10">Linting</text>
<rect class="flow-node-rect" x="295" y="40" width="100" height="44" fill="rgba(20,184,166,0.08)" stroke="rgba(20,184,166,0.25)"/>
<text class="flow-node-text" x="345" y="62" fill="var(--accent-green)" font-size="10">Unit Tests</text>
<rect class="flow-node-rect" x="185" y="95" width="100" height="44" fill="rgba(20,184,166,0.08)" stroke="rgba(20,184,166,0.25)"/>
<text class="flow-node-text" x="235" y="117" fill="var(--accent-green)" font-size="10">SAST Scan</text>
<rect class="flow-node-rect" x="295" y="95" width="100" height="44" fill="rgba(20,184,166,0.08)" stroke="rgba(20,184,166,0.25)"/>
<text class="flow-node-text" x="345" y="117" fill="var(--accent-green)" font-size="10">Perf Check</text>
<!-- Arrow connecting group to next -->
<line class="flow-line" x1="415" y1="85" x2="550" y2="85" stroke="rgba(20,184,166,0.25)"/>
<!-- AI Review -->
<rect class="flow-node-rect" x="560" y="55" width="120" height="60" fill="rgba(167,139,250,0.1)" stroke="rgba(167,139,250,0.35)"/>
<text class="flow-node-text" x="620" y="80" fill="var(--accent-purple)" font-size="11">AI Review</text>
<text class="flow-node-text" x="620" y="95" fill="var(--text-ghost)" font-size="9">assistant, not gate</text>
<line class="flow-line" x1="680" y1="85" x2="720" y2="85" stroke="rgba(167,139,250,0.25)"/>
<!-- Human Review -->
<rect class="flow-node-rect" x="720" y="55" width="130" height="60" fill="rgba(236,72,153,0.08)" stroke="rgba(236,72,153,0.3)"/>
<text class="flow-node-text" x="785" y="80" fill="var(--accent-rose)" font-size="11">Human Review</text>
<text class="flow-node-text" x="785" y="95" fill="var(--text-ghost)" font-size="9">final authority</text>
</svg>
</div>
<div class="flex-row">
<div class="flex-1 card reveal-item" style="text-align:center;">
<span class="text-amber" style="font-size:2rem;font-weight:700;">SLA</span>
<p style="font-size:0.85rem;margin-top:0.4rem;">Standard: <strong class="text-teal">2 hrs</strong><br>Hotfix: <strong class="text-amber">30 min</strong></p>
</div>
<div class="flex-1 card reveal-item" style="text-align:center;">
<span class="text-purple" style="font-size:2rem;font-weight:700;">Async</span>
<p style="font-size:0.85rem;margin-top:0.4rem;">Devs move to next task<br>while review proceeds</p>
</div>
<div class="flex-1 card reveal-item" style="text-align:center;">
<span class="text-green" style="font-size:2rem;font-weight:700;">Risk-Tiered</span>
<p style="font-size:0.85rem;margin-top:0.4rem;">Docs fix ≠ payment code<br>Different gates per risk</p>
</div>
</div>
</div>
</div>
<!-- ═══════ SLIDE 6: Phase 2 Quality Gates ═══════ -->
<div class="slide" data-index="7">
<div class="big-number" style="color:rgba(251,191,36,0.08);">2</div>
<div class="slide-content">
<div class="phase-badge amber">Phase 2 &mdash; Quality Gates</div>
<h2 class="reveal-item mb-4">Risk-Based Review Tiers</h2>
<div class="card reveal-item">
<table class="data-table">
<thead>
<tr><th>Change Type</th><th>Required Gates</th></tr>
</thead>
<tbody>
<tr class="reveal-item"><td><strong>Documentation / config</strong></td><td>Linting + 1 reviewer</td></tr>
<tr class="reveal-item"><td><strong>Standard feature work</strong></td><td>All automated gates + AI review + 1 reviewer</td></tr>
<tr class="reveal-item"><td><strong class="text-rose">Security-sensitive paths</strong> <span class="text-ghost" style="font-size:0.8em;">(auth, payments, PII)</span></td><td>All gates + security reviewer + DAST on staging</td></tr>
<tr class="reveal-item"><td><strong class="text-amber">Database migrations</strong></td><td>All gates + DBA review + backward-compat check</td></tr>
<tr class="reveal-item"><td><strong class="text-purple">New external dependencies</strong></td><td>All gates + architecture review + license check</td></tr>
</tbody>
</table>
</div>
<div class="mt-4 card reveal-item" style="border-color:rgba(167,139,250,0.15);">
<div class="flex-row" style="align-items:center;">
<div style="font-size:2rem;">&#x1f916;</div>
<div>
<h3 style="font-size:0.95rem;">AI Review: Assistant, Not Authority</h3>
<p style="font-size:0.85rem;">AI flags issues and reduces human review scope. AI approval alone is <strong class="text-rose">never sufficient</strong>. Human reviewer must independently verify security, business logic, and architecture fit.</p>
</div>
</div>
</div>
</div>
</div>
<!-- ═══════ SLIDE 7: Phase 3 ═══════ -->
<div class="slide" data-index="8">
<div class="big-number" style="color:rgba(96,165,250,0.08);">3</div>
<div class="slide-content">
<div class="phase-badge blue">Phase 3</div>
<h2 class="reveal-item mb-2">Iteration & Completion Control</h2>
<p class="reveal-item mb-4" style="max-width:700px;">Each checkpoint is an opportunity to adjust course. Work loops through chunks and sub-tasks until the feature is complete.</p>
<div class="flex-row">
<div class="flex-1">
<h3 class="reveal-item text-blue mb-3" style="font-size:1.1rem;">Iteration Rules</h3>
<ul class="bullet-list blue">
<li class="reveal-item"><strong>Atomic commits</strong> &mdash; each one can be understood and reverted independently</li>
<li class="reveal-item"><strong>1-2 day sub-tasks</strong> &mdash; taking longer is a signal to re-decompose</li>
<li class="reveal-item"><strong>Mandatory re-evaluation</strong> after each sub-task completion</li>
<li class="reveal-item"><strong>Daily rebase</strong> against main &mdash; catch drift early</li>
<li class="reveal-item"><strong>Branch age limit</strong> &mdash; 3 days = warning, 5 days = escalation</li>
</ul>
</div>
<div class="flex-1">
<div class="card reveal-item" style="border-color:rgba(96,165,250,0.15);">
<h3 class="text-blue mb-3" style="font-size:0.95rem;">After Each Sub-Task, Ask:</h3>
<ul class="bullet-list blue" style="font-size:0.9rem;">
<li class="reveal-item">Has anything changed the original plan?</li>
<li class="reveal-item">Are remaining estimates still valid?</li>
<li class="reveal-item">Should priorities shift based on learnings?</li>
</ul>
<hr class="glow-divider" style="margin:1rem 0;background:linear-gradient(90deg,transparent,var(--accent-blue),transparent);box-shadow:0 0 12px rgba(96,165,250,0.2);">
<p class="reveal-item" style="font-size:0.85rem;color:var(--text-tertiary);">On feature completion &rarr; AI refreshes module docs &rarr; human reviews diff</p>
</div>
</div>
</div>
</div>
</div>
<!-- ═══════ SLIDE 8: Phase 4 ═══════ -->
<div class="slide" data-index="9">
<div class="big-number" style="color:rgba(52,211,153,0.08);">4</div>
<div class="slide-content">
<div class="phase-badge green">Phase 4</div>
<h2 class="reveal-item mb-2">Deploy, Validate &amp; Learn</h2>
<p class="reveal-item mb-4" style="max-width:700px;">Ship safely with progressive delivery. Deployment is the <strong class="text-green">beginning</strong> of validation, not the end.</p>
<!-- Pipeline flowchart -->
<div class="flowchart reveal-item mb-4">
<svg viewBox="0 0 760 100" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect class="flow-node-rect" x="0" y="20" width="110" height="60" rx="6" fill="rgba(96,165,250,0.08)" stroke="rgba(96,165,250,0.3)"/>
<text class="flow-node-text" x="55" y="44" fill="var(--accent-blue)" font-size="10" text-anchor="middle">Merge to</text>
<text class="flow-node-text" x="55" y="58" fill="var(--accent-blue)" font-size="10" text-anchor="middle">main</text>
<line class="flow-line" x1="110" y1="50" x2="145" y2="50" stroke="rgba(96,165,250,0.25)"/>
<rect class="flow-node-rect" x="145" y="20" width="110" height="60" rx="6" fill="rgba(6,182,212,0.08)" stroke="rgba(6,182,212,0.3)"/>
<text class="flow-node-text" x="200" y="53" fill="#06b6d4" font-size="10" text-anchor="middle">CI/CD</text>
<line class="flow-line" x1="255" y1="50" x2="290" y2="50" stroke="rgba(6,182,212,0.25)"/>
<rect class="flow-node-rect" x="290" y="20" width="110" height="60" rx="6" fill="rgba(6,182,212,0.08)" stroke="rgba(6,182,212,0.3)"/>
<text class="flow-node-text" x="345" y="44" fill="#06b6d4" font-size="10" text-anchor="middle">Integration</text>
<text class="flow-node-text" x="345" y="58" fill="#06b6d4" font-size="10" text-anchor="middle">Tests</text>
<line class="flow-line" x1="400" y1="50" x2="435" y2="50" stroke="rgba(167,139,250,0.25)"/>
<rect class="flow-node-rect" x="435" y="20" width="110" height="60" rx="6" fill="rgba(167,139,250,0.08)" stroke="rgba(167,139,250,0.3)"/>
<text class="flow-node-text" x="490" y="44" fill="var(--accent-purple)" font-size="10" text-anchor="middle">E2E</text>
<text class="flow-node-text" x="490" y="58" fill="var(--accent-purple)" font-size="10" text-anchor="middle">Automation</text>
<line class="flow-line" x1="545" y1="50" x2="580" y2="50" stroke="rgba(52,211,153,0.25)"/>
<rect class="flow-node-rect" x="580" y="20" width="110" height="60" rx="6" fill="rgba(52,211,153,0.08)" stroke="rgba(52,211,153,0.3)"/>
<text class="flow-node-text" x="635" y="44" fill="var(--accent-green)" font-size="10" text-anchor="middle">Full</text>
<text class="flow-node-text" x="635" y="58" fill="var(--accent-green)" font-size="10" text-anchor="middle">Rollout</text>
<line class="flow-line" x1="690" y1="50" x2="710" y2="50" stroke="rgba(52,211,153,0.25)"/>
<rect class="flow-node-rect" x="710" y="20" width="50" height="60" rx="6" fill="rgba(52,211,153,0.12)" stroke="rgba(52,211,153,0.4)"/>
<text class="flow-node-text" x="735" y="53" fill="var(--accent-green)" font-size="10" text-anchor="middle">&#10003;</text>
</svg>
</div>
<!-- Three column detail cards -->
<div class="flex-row" style="gap:1.2rem;">
<div class="flex-1 card reveal-item">
<h3 class="text-amber mb-2" style="font-size:0.95rem;">Auto-Rollback</h3>
<p style="font-size:0.85rem;margin-bottom:0.8rem;">If error rate or latency spikes post-deploy, <strong class="text-rose">automatic rollback</strong> triggers.</p>
<ul style="font-size:0.8rem;list-style:none;padding:0;margin:0;display:flex;flex-direction:column;gap:0.3rem;">
<li style="color:var(--text-tertiary);"><span style="color:var(--accent-amber);">&#9679;</span> Error rate threshold exceeded</li>
<li style="color:var(--text-tertiary);"><span style="color:var(--accent-amber);">&#9679;</span> P99 latency spike detected</li>
<li style="color:var(--text-tertiary);"><span style="color:var(--accent-amber);">&#9679;</span> Team alerted via PagerDuty</li>
</ul>
</div>
<div class="flex-1 card reveal-item">
<h3 class="text-teal mb-2" style="font-size:0.95rem;">Post-Deploy Validation</h3>
<p style="font-size:0.85rem;margin-bottom:0.8rem;">Production observation period confirms the release is healthy.</p>
<ul style="font-size:0.8rem;list-style:none;padding:0;margin:0;display:flex;flex-direction:column;gap:0.3rem;">
<li style="color:var(--text-tertiary);"><span style="color:var(--accent-teal);">&#9679;</span> SLO compliance checked</li>
<li style="color:var(--text-tertiary);"><span style="color:var(--accent-teal);">&#9679;</span> Dashboards reviewed 24-48h</li>
<li style="color:var(--text-tertiary);"><span style="color:var(--accent-teal);">&#9679;</span> Learnings feed next cycle</li>
</ul>
</div>
</div>
</div>
</div>
<!-- ═══════ SLIDE 9: Rollback Strategy ═══════ -->
<div class="slide" data-index="10">
<div class="big-number" style="color:rgba(52,211,153,0.08);">4</div>
<div class="slide-content">
<div class="phase-badge green">Phase 4 &mdash; Safety Net</div>
<h2 class="reveal-item mb-4">Always Have a Way Back</h2>
<div class="card reveal-item mb-4">
<table class="data-table">
<thead>
<tr><th>Rollback Type</th><th>When</th><th>Time Target</th></tr>
</thead>
<tbody>
<tr class="reveal-item"><td><strong class="text-teal">Feature flag disable</strong></td><td>User-facing issues, business logic errors</td><td class="mono text-green">Seconds</td></tr>
<tr class="reveal-item"><td><strong class="text-amber">Deployment rollback</strong></td><td>Infrastructure issues, perf degradation</td><td class="mono text-amber">&lt; 15 min</td></tr>
<tr class="reveal-item"><td><strong class="text-purple">Database rollback</strong></td><td>Schema migration caused data issues</td><td class="mono text-purple">Per runbook</td></tr>
<tr class="reveal-item"><td><strong class="text-rose">Hotfix</strong></td><td>Bug requires new code, not just reverting</td><td class="mono text-rose">Hours</td></tr>
</tbody>
</table>
</div>
<div class="card reveal-item" style="border-color:rgba(100,255,218,0.15);">
<h3 class="text-teal mb-2" style="font-size:0.95rem;">The Feedback Loop</h3>
<ul class="bullet-list" style="font-size:0.9rem;">
<li class="reveal-item"><strong>Post-deploy review within one week</strong> &mdash; what performed as expected? what surprised us?</li>
<li class="reveal-item"><strong>Production telemetry feeds planning</strong> &mdash; real data informs next cycle's priorities</li>
<li class="reveal-item"><strong>Team that ships owns production health</strong> &mdash; direct incentive for reliable code</li>
<li class="reveal-item"><strong>SLO breaches auto-prioritize reliability</strong> &mdash; health degrades = features pause</li>
</ul>
</div>
</div>
</div>
<!-- ═══════ SLIDE 11: Closing ═══════ -->
<div class="slide" data-index="12">
<div class="slide-content text-center">
<p class="uppercase text-teal reveal-item mb-4">Summary</p>
<h2 class="reveal-item mb-3">Not a Pipeline.<br><span class="text-teal glow-teal">A Feedback System.</span></h2>
<hr class="glow-divider reveal-item" style="max-width:200px;margin:1.5rem auto;">
<div class="reveal-item" style="max-width:650px;margin:0 auto;">
<p class="mb-4">Development insights feed back into planning. Every deployment teaches us something. The process evolves through retrospectives.</p>
</div>
<div class="reveal-item" style="max-width:800px;margin:0 auto;">
<div class="card" style="border-color:rgba(100,255,218,0.2);padding:1.5rem 2rem;">
<div style="display:flex;align-items:center;gap:1rem;margin-bottom:1rem;">
<div style="width:40px;height:40px;border-radius:10px;background:rgba(100,255,218,0.08);border:1px solid rgba(100,255,218,0.2);display:flex;align-items:center;justify-content:center;">
<span style="font-size:1.2rem;">&#9881;</span>
</div>
<div>
<h3 class="text-teal" style="font-size:1.05rem;margin:0;">Prompt-Ops Framework</h3>
<p style="font-size:0.75rem;color:var(--text-ghost);margin:0;">Centralized prompt distribution across teams</p>
</div>
</div>
<p style="font-size:0.85rem;margin-bottom:1rem;">Reusable, version-controlled prompts packaged as a shared framework &mdash; so every team leverages battle-tested AI workflows without reinventing them.</p>
<div style="display:flex;gap:1rem;">
<div style="flex:1;padding:0.8rem;border-radius:8px;background:rgba(100,255,218,0.04);border:1px solid rgba(100,255,218,0.1);">
<div style="font-size:0.7rem;color:var(--accent-teal);text-transform:uppercase;letter-spacing:0.1em;margin-bottom:0.4rem;">Distribute</div>
<p style="font-size:0.78rem;color:var(--text-tertiary);margin:0;">Spec templates, planning prompts, review checklists &mdash; shared across all product teams</p>
</div>
<div style="flex:1;padding:0.8rem;border-radius:8px;background:rgba(167,139,250,0.04);border:1px solid rgba(167,139,250,0.1);">
<div style="font-size:0.7rem;color:var(--accent-purple);text-transform:uppercase;letter-spacing:0.1em;margin-bottom:0.4rem;">Standardize</div>
<p style="font-size:0.78rem;color:var(--text-tertiary);margin:0;">Consistent AI interactions &mdash; same quality gates, same constraint summaries, same output format</p>
</div>
<div style="flex:1;padding:0.8rem;border-radius:8px;background:rgba(251,191,36,0.04);border:1px solid rgba(251,191,36,0.1);">
<div style="font-size:0.7rem;color:var(--accent-amber);text-transform:uppercase;letter-spacing:0.1em;margin-bottom:0.4rem;">Evolve</div>
<p style="font-size:0.78rem;color:var(--text-tertiary);margin:0;">Version-controlled prompts improve over time &mdash; teams contribute learnings back to the framework</p>
</div>
</div>
</div>
</div>
<div class="mt-4 reveal-item">
<span class="mono text-ghost" style="font-size:0.8rem;">UBS &mdash; Feature Development Workflow</span>
</div>
</div>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/animejs/dist/bundles/anime.umd.min.js"></script>
<script>
(function() {
'use strict';
// ── anime.js v4 imports ──
const { animate, createTimeline, stagger } = anime;
// ── State ──
const slides = document.querySelectorAll('.slide');
const totalSlides = slides.length;
let current = 0;
let isAnimating = false;
// ── Particles ──
function initParticles() {
const container = document.getElementById('particles');
for (let i = 0; i < 25; i++) {
const p = document.createElement('div');
p.className = 'particle';
p.style.left = Math.random() * 100 + '%';
p.style.animationDuration = (10 + Math.random() * 15) + 's';
p.style.animationDelay = (Math.random() * 12) + 's';
const size = 2 + Math.random() * 4;
p.style.width = size + 'px';
p.style.height = size + 'px';
container.appendChild(p);
}
}
initParticles();
// ── Progress & Counter ──
function updateUI() {
const pct = totalSlides > 1 ? (current / (totalSlides - 1)) * 100 : 0;
document.getElementById('progressBar').style.width = pct + '%';
document.getElementById('slideCounter').textContent =
String(current + 1).padStart(2, '0') + ' / ' + String(totalSlides).padStart(2, '0');
// hide nav hint after first navigation
if (current > 0) document.getElementById('navHint').style.display = 'none';
}
updateUI();
// ── Animate Slide In ──
function animateSlideIn(slide) {
// Reset all reveal items
const items = slide.querySelectorAll('.reveal-item');
items.forEach(el => { el.style.opacity = '0'; el.style.transform = 'translateY(30px)'; });
// Reveal items staggered
if (items.length > 0) {
animate(items, {
opacity: [0, 1],
translateY: [30, 0],
duration: 700,
ease: 'outExpo',
delay: stagger(90, { start: 250 }),
});
}
// Animate SVG flow lines (draw effect)
const flowLines = slide.querySelectorAll('.flow-line');
flowLines.forEach((line, i) => {
let length;
try { length = line.getTotalLength(); } catch(e) { length = 200; }
line.style.strokeDasharray = length;
line.style.strokeDashoffset = length;
animate(line, {
strokeDashoffset: [length, 0],
duration: 700,
ease: 'inOutCubic',
delay: 500 + i * 150,
});
});
// Animate SVG rects (scale in)
const flowRects = slide.querySelectorAll('.flow-node-rect');
if (flowRects.length > 0) {
flowRects.forEach(r => { r.style.opacity = '0'; });
animate(flowRects, {
opacity: [0, 1],
duration: 500,
ease: 'outCubic',
delay: stagger(80, { start: 300 }),
});
}
// Animate SVG text
const flowTexts = slide.querySelectorAll('.flow-node-text');
if (flowTexts.length > 0) {
flowTexts.forEach(t => { t.style.opacity = '0'; });
animate(flowTexts, {
opacity: [0, 1],
duration: 400,
ease: 'outCubic',
delay: stagger(60, { start: 500 }),
});
}
// Animate pipeline nodes
const pipelineNodes = slide.querySelectorAll('.pipeline-node');
if (pipelineNodes.length > 0) {
pipelineNodes.forEach(n => { n.style.opacity = '0'; n.style.transform = 'scale(0.8)'; });
animate(pipelineNodes, {
opacity: [0, 1],
scale: [0.8, 1],
duration: 600,
ease: 'outBack',
delay: stagger(120, { start: 400 }),
});
}
// Animate pipeline arrows
const pipeArrows = slide.querySelectorAll('.pipeline-arrow');
if (pipeArrows.length > 0) {
pipeArrows.forEach(a => { a.style.opacity = '0'; a.style.transform = 'scaleX(0)'; });
animate(pipeArrows, {
opacity: [0, 1],
scaleX: [0, 1],
duration: 400,
ease: 'outCubic',
delay: stagger(120, { start: 600 }),
});
}
// Animate stat blocks
const statBlocks = slide.querySelectorAll('.stat-block');
if (statBlocks.length > 0) {
statBlocks.forEach(s => { s.style.opacity = '0'; s.style.transform = 'translateY(20px) scale(0.95)'; });
animate(statBlocks, {
opacity: [0, 1],
translateY: [20, 0],
scale: [0.95, 1],
duration: 600,
ease: 'outBack',
delay: stagger(80, { start: 400 }),
});
}
// Animate table rows
const tableRows = slide.querySelectorAll('.data-table tbody tr.reveal-item');
if (tableRows.length > 0) {
tableRows.forEach(r => { r.style.opacity = '0'; r.style.transform = 'translateX(-20px)'; });
animate(tableRows, {
opacity: [0, 1],
translateX: [-20, 0],
duration: 500,
ease: 'outCubic',
delay: stagger(100, { start: 500 }),
});
}
}
// ── Transition Between Slides ──
async function goTo(index) {
if (index < 0 || index >= totalSlides || index === current || isAnimating) return;
isAnimating = true;
const direction = index > current ? 1 : -1;
const outSlide = slides[current];
const inSlide = slides[index];
// Prep incoming slide: hide reveal items BEFORE making it visible
const revealItems = inSlide.querySelectorAll('.reveal-item');
revealItems.forEach(el => { el.style.opacity = '0'; el.style.transform = 'translateY(30px)'; });
const flowRects = inSlide.querySelectorAll('.flow-node-rect');
flowRects.forEach(r => { r.style.opacity = '0'; });
const flowTexts = inSlide.querySelectorAll('.flow-node-text');
flowTexts.forEach(t => { t.style.opacity = '0'; });
const pipelineNodes = inSlide.querySelectorAll('.pipeline-node');
pipelineNodes.forEach(n => { n.style.opacity = '0'; n.style.transform = 'scale(0.8)'; });
const pipeArrows = inSlide.querySelectorAll('.pipeline-arrow');
pipeArrows.forEach(a => { a.style.opacity = '0'; a.style.transform = 'scaleX(0)'; });
const statBlocks = inSlide.querySelectorAll('.stat-block');
statBlocks.forEach(s => { s.style.opacity = '0'; s.style.transform = 'translateY(20px) scale(0.95)'; });
const tableRows = inSlide.querySelectorAll('.data-table tbody tr.reveal-item');
tableRows.forEach(r => { r.style.opacity = '0'; r.style.transform = 'translateX(-20px)'; });
// Set incoming slide initial state
const clipFrom = direction > 0
? 'polygon(100% 0, 100% 0, 100% 100%, 100% 100%)'
: 'polygon(0 0, 0 0, 0 100%, 0 100%)';
const clipTo = 'polygon(0 0, 100% 0, 100% 100%, 0 100%)';
inSlide.classList.add('active');
inSlide.style.opacity = '0';
inSlide.style.clipPath = clipFrom;
// Animate outgoing slide
const outAnim = animate(outSlide, {
opacity: [1, 0],
filter: ['blur(0px)', 'blur(6px)'],
scale: [1, 0.97],
duration: 500,
ease: 'inCubic',
});
// Small delay, then animate incoming
await new Promise(r => setTimeout(r, 200));
inSlide.style.filter = 'blur(4px)';
const inAnim = animate(inSlide, {
opacity: [0, 1],
clipPath: [clipFrom, clipTo],
filter: ['blur(4px)', 'blur(0px)'],
scale: [1.02, 1],
duration: 700,
ease: 'outExpo',
});
// After outgoing finishes
await new Promise(r => setTimeout(r, 400));
outSlide.classList.remove('active');
outSlide.style.opacity = '0';
outSlide.style.filter = '';
outSlide.style.transform = '';
outSlide.style.clipPath = '';
// Update state
current = index;
updateUI();
// Animate content of new slide (items already prepped above)
animateSlideIn(inSlide);
// Wait for animations to settle
await new Promise(r => setTimeout(r, 600));
inSlide.style.clipPath = '';
inSlide.style.filter = '';
inSlide.style.transform = '';
isAnimating = false;
}
function next() { goTo(current + 1); }
function prev() { goTo(current - 1); }
// ── Keyboard Navigation ──
document.addEventListener('keydown', function(e) {
if (isAnimating) return;
switch(e.key) {
case 'ArrowRight': case 'ArrowDown': case ' ': case 'PageDown':
e.preventDefault(); next(); break;
case 'ArrowLeft': case 'ArrowUp': case 'PageUp':
e.preventDefault(); prev(); break;
case 'Home': e.preventDefault(); goTo(0); break;
case 'End': e.preventDefault(); goTo(totalSlides - 1); break;
}
if (e.key >= '1' && e.key <= '9') {
const idx = parseInt(e.key) - 1;
if (idx < totalSlides) goTo(idx);
}
});
// ── Touch / Swipe ──
let touchStartX = 0, touchStartY = 0;
document.addEventListener('touchstart', function(e) {
touchStartX = e.touches[0].clientX;
touchStartY = e.touches[0].clientY;
}, { passive: true });
document.addEventListener('touchend', function(e) {
const dx = e.changedTouches[0].clientX - touchStartX;
const dy = e.changedTouches[0].clientY - touchStartY;
if (Math.abs(dx) < 50 && Math.abs(dy) < 50) return;
if (Math.abs(dx) > Math.abs(dy)) {
if (dx < 0) next(); else prev();
} else {
if (dy < 0) next(); else prev();
}
}, { passive: true });
// ── Initial animation for first slide ──
requestAnimationFrame(function() {
animateSlideIn(slides[0]);
});
})();
</script>
</body>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment