070959e133
landing-page-staging / Deploy landing page to staging (push) Has been skipped
landing-page-ci / Validate landing page (push) Failing after 4s
visual-baseline / Capture visual baselines (push) Has been cancelled
bake-plugin-previews / Bake plugin previews (push) Has been cancelled
969 lines
41 KiB
HTML
969 lines
41 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="en">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||
<title>Defend a CS Capstone like a Top Thesis Advisor - Student coursework</title>
|
||
<style>
|
||
/* === FONTS === */
|
||
@import url('https://fonts.googleapis.com/css2?family=Bodoni+Moda:ital,wght@0,400;0,700;1,400&family=DM+Sans:wght@400;500;700&display=swap');
|
||
|
||
/* ===========================================
|
||
DESIGN TOKENS — Notebook Tabs theme
|
||
Cream paper card on dark ground, colorful
|
||
index tabs on the right edge, binder holes
|
||
on the left. Bodoni Moda + DM Sans.
|
||
All values authored at 1920×1080 stage size.
|
||
=========================================== */
|
||
:root {
|
||
/* Colors */
|
||
--bg-outer: #2d2d2d; /* dark desk behind the notebook */
|
||
--bg-page: #f8f6f1; /* cream paper */
|
||
--text-primary: #1a1a1a;
|
||
--text-secondary: #6b6358;
|
||
--rule: #d8d2c4; /* hairline rules on paper */
|
||
--tab-1: #98d4bb; /* Mint */
|
||
--tab-2: #c7b8ea; /* Lavender */
|
||
--tab-3: #f4b8c5; /* Pink */
|
||
--tab-4: #a8d8ea; /* Sky */
|
||
--tab-5: #ffe6a7; /* Cream */
|
||
|
||
/* Stage */
|
||
--stage-bg: var(--bg-outer);
|
||
--slide-bg: var(--bg-outer);
|
||
|
||
/* Typography */
|
||
--font-display: 'Bodoni Moda', 'Didot', serif;
|
||
--font-body: 'DM Sans', 'Helvetica Neue', sans-serif;
|
||
--title-size: 150px;
|
||
--h2-size: 84px;
|
||
--subtitle-size: 30px;
|
||
--body-size: 26px;
|
||
--label-size: 17px;
|
||
|
||
/* Paper geometry (inside the 1920×1080 stage) */
|
||
--paper-top: 56px;
|
||
--paper-bottom: 56px;
|
||
--paper-left: 120px;
|
||
--paper-right: 150px; /* leaves room for the tabs */
|
||
--paper-pad-x: 110px;
|
||
--paper-pad-y: 78px;
|
||
|
||
/* Motion */
|
||
--ease-page: cubic-bezier(0.16, 1, 0.3, 1);
|
||
--duration-normal: 0.7s;
|
||
}
|
||
|
||
/* === BASE === */
|
||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||
|
||
/* ===========================================
|
||
FIXED 16:9 STAGE: MANDATORY BASE STYLES
|
||
(full viewport-base.css, inlined)
|
||
Slides are authored at 1920×1080 and scaled as a whole.
|
||
=========================================== */
|
||
html,
|
||
body {
|
||
width: 100%;
|
||
height: 100%;
|
||
margin: 0;
|
||
overflow: hidden;
|
||
background: var(--stage-bg, #000);
|
||
}
|
||
.deck-viewport {
|
||
position: fixed;
|
||
inset: 0;
|
||
overflow: hidden;
|
||
background: var(--stage-bg, #000);
|
||
}
|
||
.deck-stage {
|
||
position: absolute;
|
||
left: 0;
|
||
top: 0;
|
||
width: 1920px;
|
||
height: 1080px;
|
||
overflow: hidden;
|
||
transform-origin: 0 0;
|
||
background: var(--slide-bg, #fff);
|
||
}
|
||
.slide {
|
||
position: absolute;
|
||
inset: 0;
|
||
width: 1920px;
|
||
height: 1080px;
|
||
overflow: hidden;
|
||
display: block;
|
||
visibility: hidden;
|
||
opacity: 0;
|
||
pointer-events: none;
|
||
background: var(--slide-bg, #fff);
|
||
}
|
||
.slide.active,
|
||
.slide.visible {
|
||
visibility: visible;
|
||
opacity: 1;
|
||
pointer-events: auto;
|
||
z-index: 1;
|
||
}
|
||
img, video, canvas, svg {
|
||
max-width: 100%;
|
||
max-height: 100%;
|
||
}
|
||
.deck-controls {
|
||
position: fixed;
|
||
left: 50%;
|
||
bottom: 22px;
|
||
transform: translateX(-50%);
|
||
z-index: 1000;
|
||
}
|
||
@media print {
|
||
html, body { width: 1920px; height: auto; overflow: visible; background: #fff; }
|
||
.deck-viewport { position: static; overflow: visible; background: #fff; }
|
||
.deck-stage { position: static; width: auto; height: auto; transform: none !important; background: none; }
|
||
.slide {
|
||
position: relative; display: block !important;
|
||
visibility: visible !important; opacity: 1 !important; pointer-events: auto !important;
|
||
width: 1920px; height: 1080px;
|
||
break-after: page; page-break-after: always;
|
||
}
|
||
.slide:last-child { break-after: auto; page-break-after: auto; }
|
||
.deck-controls { display: none !important; }
|
||
}
|
||
@media (prefers-reduced-motion: reduce) {
|
||
*, *::before, *::after {
|
||
animation-duration: 0.01ms !important;
|
||
transition-duration: 0.2s !important;
|
||
}
|
||
/* no movement: reveals fade only, chart bars render at full height */
|
||
.reveal { transform: none !important; }
|
||
.bar { transform: none !important; }
|
||
}
|
||
|
||
/* ===========================================
|
||
THE NOTEBOOK — paper card, binder holes, tabs
|
||
=========================================== */
|
||
.paper {
|
||
position: absolute;
|
||
top: var(--paper-top);
|
||
bottom: var(--paper-bottom);
|
||
left: var(--paper-left);
|
||
right: var(--paper-right);
|
||
background: var(--bg-page);
|
||
border-radius: 8px;
|
||
box-shadow:
|
||
0 2px 4px rgba(0, 0, 0, 0.25),
|
||
0 18px 50px rgba(0, 0, 0, 0.45),
|
||
inset 0 0 0 1px rgba(255, 255, 255, 0.35);
|
||
/* faint paper grain via layered gradients */
|
||
background-image:
|
||
linear-gradient(rgba(26, 26, 26, 0.018) 1px, transparent 1px),
|
||
linear-gradient(180deg, var(--bg-page), var(--bg-page));
|
||
background-size: 100% 44px, 100% 100%;
|
||
z-index: 1; /* paper sits above the tucked tab roots */
|
||
}
|
||
|
||
/* Binder holes — punched circles on the left margin */
|
||
.holes {
|
||
position: absolute;
|
||
left: 38px;
|
||
top: 90px;
|
||
bottom: 90px;
|
||
width: 26px;
|
||
display: flex;
|
||
flex-direction: column;
|
||
justify-content: space-between;
|
||
z-index: 3;
|
||
}
|
||
.holes span {
|
||
width: 26px;
|
||
height: 26px;
|
||
border-radius: 50%;
|
||
background: var(--bg-outer);
|
||
box-shadow:
|
||
inset 0 2px 4px rgba(0, 0, 0, 0.7),
|
||
0 1px 0 rgba(255, 255, 255, 0.55);
|
||
}
|
||
|
||
/* Margin rule echoing a real notebook's red line */
|
||
.margin-rule {
|
||
position: absolute;
|
||
left: 92px;
|
||
top: 0;
|
||
bottom: 0;
|
||
width: 1px;
|
||
background: var(--tab-3);
|
||
opacity: 0.55;
|
||
z-index: 2;
|
||
}
|
||
|
||
/* Index tabs — vertical labels protruding past the right paper edge.
|
||
Positioned against the stage so 12px of each tab root tucks UNDER
|
||
the paper (.paper has z-index: 1, tabs have z-index: 0). */
|
||
.tabs {
|
||
position: absolute;
|
||
right: calc(var(--paper-right) - 44px); /* 44px sticks out past the paper edge */
|
||
top: calc(var(--paper-top) + 60px);
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 18px;
|
||
z-index: 0; /* tucked behind the paper edge */
|
||
}
|
||
.tab {
|
||
writing-mode: vertical-rl;
|
||
font-family: var(--font-body);
|
||
font-size: var(--label-size);
|
||
font-weight: 700;
|
||
letter-spacing: 0.3em;
|
||
text-transform: uppercase;
|
||
color: rgba(26, 26, 26, 0.78);
|
||
width: 56px;
|
||
height: 158px;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
border-radius: 0 10px 10px 0;
|
||
box-shadow: 2px 2px 6px rgba(0, 0, 0, 0.3);
|
||
opacity: 0.82;
|
||
transform: translateX(-6px);
|
||
transition: transform 0.45s var(--ease-page), opacity 0.45s var(--ease-page);
|
||
}
|
||
.tab-1 { background: var(--tab-1); }
|
||
.tab-2 { background: var(--tab-2); }
|
||
.tab-3 { background: var(--tab-3); }
|
||
.tab-4 { background: var(--tab-4); }
|
||
.tab-5 { background: var(--tab-5); }
|
||
.tab.on { opacity: 1; transform: translateX(0); box-shadow: 4px 4px 10px rgba(0, 0, 0, 0.35); }
|
||
|
||
/* Content area inside the paper */
|
||
.page {
|
||
position: absolute;
|
||
inset: 0;
|
||
padding: var(--paper-pad-y) var(--paper-pad-x) var(--paper-pad-y) 150px;
|
||
display: flex;
|
||
flex-direction: column;
|
||
z-index: 1;
|
||
}
|
||
|
||
/* === PAGE CHROME — running head + baseline === */
|
||
.runhead {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: baseline;
|
||
font-family: var(--font-body);
|
||
font-size: var(--label-size);
|
||
letter-spacing: 0.26em;
|
||
text-transform: uppercase;
|
||
color: var(--text-secondary);
|
||
border-bottom: 1px solid var(--rule);
|
||
padding-bottom: 18px;
|
||
margin-bottom: 40px;
|
||
}
|
||
.runhead .no { font-weight: 700; color: var(--text-primary); }
|
||
.baseline {
|
||
margin-top: auto;
|
||
display: flex;
|
||
justify-content: space-between;
|
||
font-family: var(--font-body);
|
||
font-size: 15px;
|
||
letter-spacing: 0.24em;
|
||
text-transform: uppercase;
|
||
color: var(--text-secondary);
|
||
border-top: 1px solid var(--rule);
|
||
padding-top: 16px;
|
||
}
|
||
|
||
/* === TYPOGRAPHY === */
|
||
.display {
|
||
font-family: var(--font-display);
|
||
font-weight: 700;
|
||
font-size: var(--title-size);
|
||
line-height: 0.98;
|
||
letter-spacing: -0.01em;
|
||
color: var(--text-primary);
|
||
}
|
||
.h2 {
|
||
font-family: var(--font-display);
|
||
font-weight: 700;
|
||
font-size: var(--h2-size);
|
||
line-height: 1.04;
|
||
color: var(--text-primary);
|
||
}
|
||
.kicker {
|
||
font-family: var(--font-body);
|
||
font-size: var(--label-size);
|
||
font-weight: 700;
|
||
letter-spacing: 0.3em;
|
||
text-transform: uppercase;
|
||
color: var(--text-secondary);
|
||
margin-bottom: 22px;
|
||
}
|
||
.kicker .chip {
|
||
display: inline-block;
|
||
width: 30px;
|
||
height: 12px;
|
||
border-radius: 3px;
|
||
margin-right: 12px;
|
||
vertical-align: baseline;
|
||
}
|
||
.lede {
|
||
font-family: var(--font-body);
|
||
font-size: var(--subtitle-size);
|
||
line-height: 1.5;
|
||
color: var(--text-secondary);
|
||
max-width: 980px;
|
||
}
|
||
|
||
/* === ENTRANCE ANIMATION === */
|
||
.reveal {
|
||
opacity: 0;
|
||
transform: translateY(28px);
|
||
transition: opacity var(--duration-normal) var(--ease-page),
|
||
transform var(--duration-normal) var(--ease-page);
|
||
}
|
||
.slide.visible .reveal { opacity: 1; transform: translateY(0); }
|
||
.reveal:nth-child(1), .reveal.d1 { transition-delay: 0.08s; }
|
||
.reveal:nth-child(2), .reveal.d2 { transition-delay: 0.18s; }
|
||
.reveal:nth-child(3), .reveal.d3 { transition-delay: 0.28s; }
|
||
.reveal:nth-child(4), .reveal.d4 { transition-delay: 0.38s; }
|
||
.reveal:nth-child(5), .reveal.d5 { transition-delay: 0.48s; }
|
||
.reveal:nth-child(6), .reveal.d6 { transition-delay: 0.58s; }
|
||
|
||
/* === COVER === */
|
||
.cover-meta {
|
||
display: flex;
|
||
gap: 26px;
|
||
align-items: center;
|
||
font-family: var(--font-body);
|
||
font-size: 20px;
|
||
letter-spacing: 0.2em;
|
||
text-transform: uppercase;
|
||
color: var(--text-secondary);
|
||
margin-top: 44px;
|
||
}
|
||
.cover-swatches { display: flex; gap: 10px; }
|
||
.cover-swatches i {
|
||
width: 44px; height: 18px; border-radius: 4px; display: inline-block;
|
||
box-shadow: 0 1px 2px rgba(0,0,0,0.18);
|
||
}
|
||
.cover-edition {
|
||
font-family: var(--font-display);
|
||
font-style: italic;
|
||
font-size: 34px;
|
||
color: var(--text-secondary);
|
||
margin-top: 26px;
|
||
}
|
||
|
||
/* === CONTENTS (index page) === */
|
||
.toc { display: flex; flex-direction: column; margin-top: 8px; }
|
||
.toc-row {
|
||
display: flex;
|
||
align-items: baseline;
|
||
gap: 26px;
|
||
padding: 26px 0;
|
||
border-bottom: 1px dashed var(--rule);
|
||
font-family: var(--font-body);
|
||
}
|
||
.toc-row:last-child { border-bottom: none; }
|
||
.toc-chip { width: 38px; height: 16px; border-radius: 4px; flex: none; align-self: center; }
|
||
.toc-no {
|
||
font-family: var(--font-display);
|
||
font-size: 30px;
|
||
font-weight: 700;
|
||
color: var(--text-secondary);
|
||
width: 64px;
|
||
flex: none;
|
||
}
|
||
.toc-title { font-family: var(--font-display); font-size: 40px; font-weight: 400; color: var(--text-primary); }
|
||
.toc-dots { flex: 1; border-bottom: 2px dotted var(--rule); transform: translateY(-9px); }
|
||
.toc-pg { font-size: 22px; letter-spacing: 0.18em; color: var(--text-secondary); }
|
||
|
||
/* === SECTION DIVIDER === */
|
||
.divider-no {
|
||
font-family: var(--font-display);
|
||
font-weight: 400;
|
||
font-size: 430px;
|
||
line-height: 0.85;
|
||
color: transparent;
|
||
-webkit-text-stroke: 2px var(--text-primary);
|
||
}
|
||
.divider-wrap { display: flex; align-items: center; gap: 90px; flex: 1; }
|
||
.divider-side .h2 { margin-bottom: 24px; }
|
||
.divider-bar { width: 120px; height: 16px; border-radius: 4px; margin-bottom: 34px; }
|
||
|
||
/* === BULLETS === */
|
||
.points { display: flex; flex-direction: column; gap: 30px; margin-top: 10px; }
|
||
.point { display: flex; gap: 30px; align-items: flex-start; max-width: 1180px; }
|
||
.point .pin {
|
||
flex: none;
|
||
width: 52px; height: 52px;
|
||
border-radius: 50%;
|
||
display: flex; align-items: center; justify-content: center;
|
||
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.18);
|
||
}
|
||
.point .pin svg { width: 26px; height: 26px; }
|
||
.point h3 { font-family: var(--font-display); font-size: 36px; font-weight: 700; color: var(--text-primary); margin-bottom: 8px; }
|
||
.point p { font-family: var(--font-body); font-size: var(--body-size); line-height: 1.5; color: var(--text-secondary); }
|
||
|
||
/* === QUOTE === */
|
||
.quote-mark {
|
||
font-family: var(--font-display);
|
||
font-size: 220px;
|
||
line-height: 0.6;
|
||
color: var(--tab-2);
|
||
margin-bottom: 12px;
|
||
}
|
||
.quote-text {
|
||
font-family: var(--font-display);
|
||
font-style: italic;
|
||
font-weight: 400;
|
||
font-size: 66px;
|
||
line-height: 1.25;
|
||
color: var(--text-primary);
|
||
max-width: 1280px;
|
||
}
|
||
.quote-attr {
|
||
font-family: var(--font-body);
|
||
font-size: 22px;
|
||
letter-spacing: 0.22em;
|
||
text-transform: uppercase;
|
||
color: var(--text-secondary);
|
||
margin-top: 44px;
|
||
}
|
||
|
||
/* === BIG STAT === */
|
||
.stat-wrap { display: flex; align-items: center; gap: 90px; flex: 1; }
|
||
.stat-num {
|
||
font-family: var(--font-display);
|
||
font-weight: 700;
|
||
font-size: 300px;
|
||
line-height: 0.9;
|
||
color: var(--text-primary);
|
||
}
|
||
.stat-num .unit {
|
||
display: block;
|
||
font-size: 120px;
|
||
font-style: italic;
|
||
font-weight: 400;
|
||
line-height: 1;
|
||
margin-top: 18px;
|
||
color: var(--text-secondary);
|
||
}
|
||
.stat-side { max-width: 560px; border-left: 4px solid var(--tab-1); padding-left: 44px; }
|
||
.stat-side h3 { font-family: var(--font-display); font-size: 40px; font-weight: 700; margin-bottom: 16px; color: var(--text-primary); }
|
||
.stat-side p { font-family: var(--font-body); font-size: var(--body-size); line-height: 1.55; color: var(--text-secondary); }
|
||
|
||
/* === BAR CHART (pure CSS) === */
|
||
.chart {
|
||
display: flex;
|
||
align-items: flex-end;
|
||
gap: 70px;
|
||
height: 480px;
|
||
margin-top: 30px;
|
||
padding: 0 30px;
|
||
border-bottom: 2px solid var(--text-primary);
|
||
}
|
||
.bar-group { display: flex; flex-direction: column; align-items: center; height: 100%; justify-content: flex-end; flex: 1; }
|
||
.bar {
|
||
width: 130px;
|
||
border-radius: 8px 8px 0 0;
|
||
position: relative;
|
||
box-shadow: inset 0 -3px 0 rgba(0, 0, 0, 0.1);
|
||
transform: scaleY(0);
|
||
transform-origin: bottom;
|
||
transition: transform 0.9s var(--ease-page);
|
||
}
|
||
.slide.visible .bar { transform: scaleY(1); }
|
||
.slide.visible .bar.b2 { transition-delay: 0.15s; }
|
||
.slide.visible .bar.b3 { transition-delay: 0.3s; }
|
||
.slide.visible .bar.b4 { transition-delay: 0.45s; }
|
||
.bar-val {
|
||
position: absolute;
|
||
top: -44px;
|
||
left: 50%;
|
||
transform: translateX(-50%);
|
||
font-family: var(--font-display);
|
||
font-weight: 700;
|
||
font-size: 30px;
|
||
color: var(--text-primary);
|
||
}
|
||
.bar-label {
|
||
font-family: var(--font-body);
|
||
font-size: 20px;
|
||
letter-spacing: 0.1em;
|
||
text-transform: uppercase;
|
||
color: var(--text-secondary);
|
||
margin-top: 18px;
|
||
}
|
||
|
||
/* === COMPARISON === */
|
||
.compare { display: grid; grid-template-columns: 1fr 1fr; gap: 50px; margin-top: 16px; flex: 1; align-content: start; }
|
||
.col {
|
||
border: 1px solid var(--rule);
|
||
border-radius: 10px;
|
||
padding: 44px 48px;
|
||
background: rgba(255, 255, 255, 0.45);
|
||
}
|
||
.col.hot { border: 2px solid var(--text-primary); box-shadow: 8px 8px 0 var(--tab-4); }
|
||
.col h3 {
|
||
font-family: var(--font-display);
|
||
font-size: 38px;
|
||
font-weight: 700;
|
||
color: var(--text-primary);
|
||
margin-bottom: 26px;
|
||
padding-bottom: 16px;
|
||
border-bottom: 1px solid var(--rule);
|
||
}
|
||
.col ul { list-style: none; }
|
||
.col li {
|
||
font-family: var(--font-body);
|
||
font-size: 25px;
|
||
line-height: 1.45;
|
||
color: var(--text-secondary);
|
||
padding: 10px 0 10px 34px;
|
||
position: relative;
|
||
}
|
||
.col li::before {
|
||
content: '';
|
||
position: absolute;
|
||
left: 0;
|
||
top: 22px;
|
||
width: 14px;
|
||
height: 14px;
|
||
border-radius: 3px;
|
||
background: var(--rule);
|
||
}
|
||
.col.hot li::before { background: var(--tab-4); }
|
||
.col.hot li { color: var(--text-primary); }
|
||
|
||
/* === PRINCIPLE GRID 2×2 === */
|
||
.grid4 { display: grid; grid-template-columns: 1fr 1fr; gap: 36px; margin-top: 12px; flex: 1; align-content: start; }
|
||
.gcard {
|
||
border: 1px solid var(--rule);
|
||
border-radius: 10px;
|
||
padding: 36px 40px;
|
||
background: rgba(255, 255, 255, 0.45);
|
||
position: relative;
|
||
overflow: hidden;
|
||
}
|
||
.gcard::before {
|
||
content: '';
|
||
position: absolute;
|
||
left: 0; top: 0; bottom: 0;
|
||
width: 12px;
|
||
background: var(--edge, var(--tab-1));
|
||
}
|
||
.gcard h3 { font-family: var(--font-display); font-size: 32px; font-weight: 700; color: var(--text-primary); margin-bottom: 10px; }
|
||
.gcard p { font-family: var(--font-body); font-size: 23px; line-height: 1.5; color: var(--text-secondary); }
|
||
.gcard .gno {
|
||
font-family: var(--font-display);
|
||
font-style: italic;
|
||
font-size: 26px;
|
||
color: var(--text-secondary);
|
||
margin-bottom: 10px;
|
||
}
|
||
|
||
/* === CLOSING === */
|
||
.close-wrap { flex: 1; display: flex; flex-direction: column; justify-content: center; }
|
||
.close-wrap .display em { font-style: italic; font-weight: 400; }
|
||
.close-links { display: flex; gap: 56px; margin-top: 60px; }
|
||
.lk {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
gap: 14px;
|
||
font-family: var(--font-body);
|
||
font-size: 22px;
|
||
letter-spacing: 0.16em;
|
||
text-transform: uppercase;
|
||
color: var(--text-primary);
|
||
border-bottom: 3px solid var(--tab-3);
|
||
padding-bottom: 8px;
|
||
}
|
||
.lk svg { width: 26px; height: 26px; }
|
||
|
||
/* === DECK CONTROLS (outside the scaled stage) === */
|
||
.deck-controls {
|
||
font-family: var(--font-body);
|
||
font-size: 14px;
|
||
letter-spacing: 0.22em;
|
||
color: rgba(248, 246, 241, 0.55);
|
||
background: rgba(26, 26, 26, 0.55);
|
||
border: 1px solid rgba(248, 246, 241, 0.18);
|
||
padding: 8px 18px;
|
||
border-radius: 999px;
|
||
user-select: none;
|
||
}
|
||
.deck-controls .cur { color: var(--tab-1); font-weight: 700; }
|
||
</style>
|
||
<style id="od-no-pager">.deck-controls,#counter,.deck-counter{display:none!important}</style>
|
||
</head>
|
||
<body>
|
||
<div class="deck-viewport">
|
||
<main class="deck-stage" id="deckStage" aria-label="Slide deck">
|
||
|
||
<!-- === SLIDE 1: COVER (Intro) === -->
|
||
<section class="slide active visible">
|
||
<div class="paper">
|
||
<div class="holes"><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span></div>
|
||
<div class="margin-rule"></div>
|
||
<div class="page">
|
||
<div class="runhead"><span class="no">No. 01</span><span>CS Capstone Defense · Computer Science Dept.</span></div>
|
||
<div class="close-wrap">
|
||
<p class="kicker reveal d1"><span class="chip" style="background: var(--tab-1);"></span>A private, on-device predictive keyboard</p>
|
||
<h1 class="display reveal d2">On-Device<br>Predictive <em style="font-weight:400;">Keyboard</em></h1>
|
||
<p class="cover-edition reveal d3">TypeAhead — thesis defense, Computer Science Department</p>
|
||
<div class="cover-meta reveal d4">
|
||
<span>Jordan Ames</span><span>·</span><span>Advisor: Prof. Elena Cho</span><span>·</span>
|
||
<span class="cover-swatches"><i style="background:var(--tab-1)"></i><i style="background:var(--tab-2)"></i><i style="background:var(--tab-3)"></i><i style="background:var(--tab-4)"></i><i style="background:var(--tab-5)"></i></span>
|
||
</div>
|
||
</div>
|
||
<div class="baseline"><span>Cover</span><span>Defense date: May 14, 2026</span></div>
|
||
</div>
|
||
</div>
|
||
<div class="tabs" aria-hidden="true">
|
||
<span class="tab tab-1 on">Intro</span><span class="tab tab-2">Problem</span><span class="tab tab-3">Evidence</span><span class="tab tab-4">Findings</span><span class="tab tab-5">Defense</span>
|
||
</div>
|
||
</section>
|
||
|
||
<!-- === SLIDE 2: CONTENTS (Intro) === -->
|
||
<section class="slide">
|
||
<div class="paper">
|
||
<div class="holes"><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span></div>
|
||
<div class="margin-rule"></div>
|
||
<div class="page">
|
||
<div class="runhead"><span class="no">No. 02</span><span>Contents</span></div>
|
||
<h2 class="h2 reveal d1" style="margin-bottom: 30px;">Defense roadmap</h2>
|
||
<div class="toc">
|
||
<div class="toc-row reveal d2"><span class="toc-chip" style="background:var(--tab-2)"></span><span class="toc-no">i.</span><span class="toc-title">The problem — cloud keyboards leak keystrokes</span><span class="toc-dots"></span><span class="toc-pg">PG 03</span></div>
|
||
<div class="toc-row reveal d3"><span class="toc-chip" style="background:var(--tab-3)"></span><span class="toc-no">ii.</span><span class="toc-title">The evidence — accuracy, latency, privacy</span><span class="toc-dots"></span><span class="toc-pg">PG 06</span></div>
|
||
<div class="toc-row reveal d4"><span class="toc-chip" style="background:var(--tab-4)"></span><span class="toc-no">iii.</span><span class="toc-title">The contribution — four things that are new</span><span class="toc-dots"></span><span class="toc-pg">PG 08</span></div>
|
||
<div class="toc-row reveal d5"><span class="toc-chip" style="background:var(--tab-5)"></span><span class="toc-no">iv.</span><span class="toc-title">Limitations & committee Q&A</span><span class="toc-dots"></span><span class="toc-pg">PG 10</span></div>
|
||
</div>
|
||
<div class="baseline"><span>Contents</span><span>02 / 10</span></div>
|
||
</div>
|
||
</div>
|
||
<div class="tabs" aria-hidden="true">
|
||
<span class="tab tab-1 on">Intro</span><span class="tab tab-2">Problem</span><span class="tab tab-3">Evidence</span><span class="tab tab-4">Findings</span><span class="tab tab-5">Defense</span>
|
||
</div>
|
||
</section>
|
||
|
||
<!-- === SLIDE 3: SECTION DIVIDER (Craft) === -->
|
||
<section class="slide">
|
||
<div class="paper">
|
||
<div class="holes"><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span></div>
|
||
<div class="margin-rule"></div>
|
||
<div class="page">
|
||
<div class="runhead"><span class="no">No. 03</span><span>Section i</span></div>
|
||
<div class="divider-wrap">
|
||
<div class="divider-no reveal d1">01</div>
|
||
<div class="divider-side">
|
||
<div class="divider-bar reveal d2" style="background: var(--tab-2);"></div>
|
||
<h2 class="h2 reveal d3">The problem —<br>keystrokes leave the phone</h2>
|
||
<p class="lede reveal d4" style="margin-top: 22px; max-width: 660px;">Every major mobile keyboard (Gboard, SwiftKey) ships raw keystrokes to cloud servers for next-word prediction — a privacy liability class-action suits have already targeted.</p>
|
||
</div>
|
||
</div>
|
||
<div class="baseline"><span>The problem</span><span>03 / 10</span></div>
|
||
</div>
|
||
</div>
|
||
<div class="tabs" aria-hidden="true">
|
||
<span class="tab tab-1">Intro</span><span class="tab tab-2 on">Problem</span><span class="tab tab-3">Evidence</span><span class="tab tab-4">Findings</span><span class="tab tab-5">Defense</span>
|
||
</div>
|
||
</section>
|
||
|
||
<!-- === SLIDE 4: BULLETS (Craft) === -->
|
||
<section class="slide">
|
||
<div class="paper">
|
||
<div class="holes"><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span></div>
|
||
<div class="margin-rule"></div>
|
||
<div class="page">
|
||
<div class="runhead"><span class="no">No. 04</span><span>Section i · The problem</span></div>
|
||
<p class="kicker reveal d1"><span class="chip" style="background: var(--tab-2);"></span>Three design principles</p>
|
||
<h2 class="h2 reveal d2" style="font-size: 64px; margin-bottom: 34px;">TypeAhead: on-device,<br>federated, adaptive</h2>
|
||
<div class="points">
|
||
<div class="point reveal d3">
|
||
<span class="pin" style="background: var(--tab-1);"><svg viewBox="0 0 24 24" fill="none" stroke="#1a1a1a" stroke-width="2.5"><path d="M4 19.5A2.5 2.5 0 0 1 6.5 17H20M4 19.5A2.5 2.5 0 0 0 6.5 22H20V2H6.5A2.5 2.5 0 0 0 4 4.5v15z"/></svg></span>
|
||
<div><h3>On-device inference</h3><p>A 2.1M-parameter transformer runs entirely on-device via Core ML / NNAPI. No keystroke ever leaves the phone — verified by packet-capture across 40 hours of typing.</p></div>
|
||
</div>
|
||
<div class="point reveal d4">
|
||
<span class="pin" style="background: var(--tab-2);"><svg viewBox="0 0 24 24" fill="none" stroke="#1a1a1a" stroke-width="2.5"><path d="M12 19l7-7 3 3-7 7-3-3zM18 13l-1.5-7.5L2 2l3.5 14.5L13 18l5-5zM2 2l7.586 7.586"/><circle cx="11" cy="11" r="2"/></svg></span>
|
||
<div><h3>Federated fine-tuning</h3><p>Personalization trains locally overnight and syncs only encrypted gradient deltas — the same class of protocol behind Gboard's federated learning, rebuilt at capstone scale.</p></div>
|
||
</div>
|
||
<div class="point reveal d5">
|
||
<span class="pin" style="background: var(--tab-3);"><svg viewBox="0 0 24 24" fill="none" stroke="#1a1a1a" stroke-width="2.5"><circle cx="12" cy="12" r="10"/><path d="M12 6v6l4 2"/></svg></span>
|
||
<div><h3>Adaptive vocabulary</h3><p>A per-user trie re-ranks candidates against typing history, so names, slang, and jargon surface within roughly 200 keystrokes without retraining the base model.</p></div>
|
||
</div>
|
||
</div>
|
||
<div class="baseline"><span>The problem</span><span>04 / 10</span></div>
|
||
</div>
|
||
</div>
|
||
<div class="tabs" aria-hidden="true">
|
||
<span class="tab tab-1">Intro</span><span class="tab tab-2 on">Problem</span><span class="tab tab-3">Evidence</span><span class="tab tab-4">Findings</span><span class="tab tab-5">Defense</span>
|
||
</div>
|
||
</section>
|
||
|
||
<!-- === SLIDE 5: QUOTE (Craft) === -->
|
||
<section class="slide">
|
||
<div class="paper">
|
||
<div class="holes"><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span></div>
|
||
<div class="margin-rule"></div>
|
||
<div class="page">
|
||
<div class="runhead"><span class="no">No. 05</span><span>Section i · The problem</span></div>
|
||
<div class="close-wrap">
|
||
<div class="quote-mark reveal d1">“</div>
|
||
<p class="quote-text reveal d2">The question isn't whether an on-device model can predict text — it's whether it can do it as well as the cloud, without the privacy trade-off.</p>
|
||
<p class="quote-attr reveal d3">— Prof. Elena Cho, thesis advisor</p>
|
||
</div>
|
||
<div class="baseline"><span>Quote</span><span>05 / 10</span></div>
|
||
</div>
|
||
</div>
|
||
<div class="tabs" aria-hidden="true">
|
||
<span class="tab tab-1">Intro</span><span class="tab tab-2 on">Problem</span><span class="tab tab-3">Evidence</span><span class="tab tab-4">Findings</span><span class="tab tab-5">Defense</span>
|
||
</div>
|
||
</section>
|
||
|
||
<!-- === SLIDE 6: BIG STAT (Numbers) === -->
|
||
<section class="slide">
|
||
<div class="paper">
|
||
<div class="holes"><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span></div>
|
||
<div class="margin-rule"></div>
|
||
<div class="page">
|
||
<div class="runhead"><span class="no">No. 06</span><span>Section ii · The evidence</span></div>
|
||
<div class="stat-wrap">
|
||
<div class="stat-num reveal d1">94.2<span class="unit">% top-3 hit rate</span></div>
|
||
<div class="stat-side reveal d2">
|
||
<h3>On-device accuracy</h3>
|
||
<p>Evaluated on a held-out set of 50,000 keystrokes from 12 volunteer typists, TypeAhead matches Gboard's cloud model (95.1%) within 0.9 points — using a model 40× smaller.</p>
|
||
</div>
|
||
</div>
|
||
<div class="baseline"><span>The evidence</span><span>06 / 10</span></div>
|
||
</div>
|
||
</div>
|
||
<div class="tabs" aria-hidden="true">
|
||
<span class="tab tab-1">Intro</span><span class="tab tab-2">Problem</span><span class="tab tab-3 on">Evidence</span><span class="tab tab-4">Findings</span><span class="tab tab-5">Defense</span>
|
||
</div>
|
||
</section>
|
||
|
||
<!-- === SLIDE 7: BAR CHART (Numbers) === -->
|
||
<section class="slide">
|
||
<div class="paper">
|
||
<div class="holes"><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span></div>
|
||
<div class="margin-rule"></div>
|
||
<div class="page">
|
||
<div class="runhead"><span class="no">No. 07</span><span>Section ii · The evidence</span></div>
|
||
<p class="kicker reveal d1"><span class="chip" style="background: var(--tab-3);"></span>Latency benchmark — Pixel 7, cold cache</p>
|
||
<h2 class="h2 reveal d2" style="font-size: 58px;">18ms beats the 340ms cloud round trip</h2>
|
||
<div class="chart">
|
||
<div class="bar-group"><div class="bar b1" style="height: 8%; background: var(--tab-1);"><span class="bar-val">18ms</span></div><div class="bar-label">TypeAhead (ours)</div></div>
|
||
<div class="bar-group"><div class="bar b2" style="height: 16%; background: var(--tab-2);"><span class="bar-val">34ms</span></div><div class="bar-label">SwiftKey (on-device)</div></div>
|
||
<div class="bar-group"><div class="bar b3" style="height: 45%; background: var(--tab-3);"><span class="bar-val">120ms</span></div><div class="bar-label">Gboard (cloud)</div></div>
|
||
<div class="bar-group"><div class="bar b4" style="height: 90%; background: var(--tab-4);"><span class="bar-val">340ms</span></div><div class="bar-label">GPT-4o API (cloud)</div></div>
|
||
</div>
|
||
<div class="baseline"><span>The evidence</span><span>07 / 10</span></div>
|
||
</div>
|
||
</div>
|
||
<div class="tabs" aria-hidden="true">
|
||
<span class="tab tab-1">Intro</span><span class="tab tab-2">Problem</span><span class="tab tab-3 on">Evidence</span><span class="tab tab-4">Findings</span><span class="tab tab-5">Defense</span>
|
||
</div>
|
||
</section>
|
||
|
||
<!-- === SLIDE 8: COMPARISON (Plan) === -->
|
||
<section class="slide">
|
||
<div class="paper">
|
||
<div class="holes"><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span></div>
|
||
<div class="margin-rule"></div>
|
||
<div class="page">
|
||
<div class="runhead"><span class="no">No. 08</span><span>Section iii · The contribution</span></div>
|
||
<h2 class="h2 reveal d1" style="font-size: 58px; margin-bottom: 26px;">Existing keyboards — versus TypeAhead</h2>
|
||
<div class="compare">
|
||
<div class="col reveal d2">
|
||
<h3>Cloud keyboards (Gboard, SwiftKey)</h3>
|
||
<ul>
|
||
<li>Keystrokes leave the device</li>
|
||
<li>No offline personalization</li>
|
||
<li>120–340ms median latency over the network</li>
|
||
<li>Opaque to independent audit</li>
|
||
</ul>
|
||
</div>
|
||
<div class="col hot reveal d3">
|
||
<h3>TypeAhead (this thesis)</h3>
|
||
<ul>
|
||
<li>100% on-device inference, no network calls</li>
|
||
<li>Federated personalization, encrypted deltas only</li>
|
||
<li>18ms median latency, no radio required</li>
|
||
<li>Open evaluation harness + full weights released</li>
|
||
</ul>
|
||
</div>
|
||
</div>
|
||
<div class="baseline"><span>The contribution</span><span>08 / 10</span></div>
|
||
</div>
|
||
</div>
|
||
<div class="tabs" aria-hidden="true">
|
||
<span class="tab tab-1">Intro</span><span class="tab tab-2">Problem</span><span class="tab tab-3">Evidence</span><span class="tab tab-4 on">Findings</span><span class="tab tab-5">Defense</span>
|
||
</div>
|
||
</section>
|
||
|
||
<!-- === SLIDE 9: PRINCIPLE GRID (Plan) === -->
|
||
<section class="slide">
|
||
<div class="paper">
|
||
<div class="holes"><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span></div>
|
||
<div class="margin-rule"></div>
|
||
<div class="page">
|
||
<div class="runhead"><span class="no">No. 09</span><span>Section iii · The contribution</span></div>
|
||
<h2 class="h2 reveal d1" style="font-size: 58px; margin-bottom: 26px;">Four contributions of this thesis</h2>
|
||
<div class="grid4">
|
||
<div class="gcard reveal d2" style="--edge: var(--tab-1);"><div class="gno">Contribution i</div><h3>2.1M-param transformer</h3><p>A quantized 8-bit transformer that fits in 6.8MB and runs inference in under 20ms on a mid-range 2021-era phone.</p></div>
|
||
<div class="gcard reveal d3" style="--edge: var(--tab-2);"><div class="gno">Contribution ii</div><h3>Federated update protocol</h3><p>A gradient-delta sync protocol that personalizes overnight on Wi-Fi and charge, verified to leak no raw text in transit.</p></div>
|
||
<div class="gcard reveal d4" style="--edge: var(--tab-3);"><div class="gno">Contribution iii</div><h3>Privacy-utility benchmark</h3><p>A reproducible 12-participant, 50K-keystroke harness comparing on-device and cloud keyboards on accuracy, latency, and battery.</p></div>
|
||
<div class="gcard reveal d5" style="--edge: var(--tab-4);"><div class="gno">Contribution iv</div><h3>Open weights & code</h3><p>Full model weights, training scripts, and the eval harness published for the department's future capstones to build on.</p></div>
|
||
</div>
|
||
<div class="baseline"><span>The contribution</span><span>09 / 10</span></div>
|
||
</div>
|
||
</div>
|
||
<div class="tabs" aria-hidden="true">
|
||
<span class="tab tab-1">Intro</span><span class="tab tab-2">Problem</span><span class="tab tab-3">Evidence</span><span class="tab tab-4 on">Findings</span><span class="tab tab-5">Defense</span>
|
||
</div>
|
||
</section>
|
||
|
||
<!-- === SLIDE 10: CLOSING (End) === -->
|
||
<section class="slide">
|
||
<div class="paper">
|
||
<div class="holes"><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span></div>
|
||
<div class="margin-rule"></div>
|
||
<div class="page">
|
||
<div class="runhead"><span class="no">No. 10</span><span>Limitations & defense</span></div>
|
||
<div class="close-wrap">
|
||
<h2 class="display reveal d1" style="font-size: 132px;">Yes, it scales —<br><em>within limits.</em></h2>
|
||
<div class="close-links reveal d2">
|
||
<span class="lk"><svg viewBox="0 0 24 24" fill="none" stroke="#1a1a1a" stroke-width="2"><path d="M4 4h16c1.1 0 2 .9 2 2v12c0 1.1-.9 2-2 2H4c-1.1 0-2-.9-2-2V6c0-1.1.9-2 2-2z"/><path d="M22 6l-10 7L2 6"/></svg>Limitation: 8 languages tested, not 40+</span>
|
||
<span class="lk"><svg viewBox="0 0 24 24" fill="none" stroke="#1a1a1a" stroke-width="2"><path d="M21 10c0 7-9 13-9 13s-9-6-9-13a9 9 0 0 1 18 0z"/><circle cx="12" cy="10" r="3"/></svg>Limitation: +3% battery drain from overnight sync</span>
|
||
</div>
|
||
<p class="lede reveal d3" style="margin-top: 56px; font-size: 22px;">Committee question addressed: given six more months, TypeAhead's federated protocol generalizes to any keyboard vocabulary — the constraint is per-language training data, not architecture.</p>
|
||
</div>
|
||
<div class="baseline"><span>Defense</span><span>10 / 10</span></div>
|
||
</div>
|
||
</div>
|
||
<div class="tabs" aria-hidden="true">
|
||
<span class="tab tab-1">Intro</span><span class="tab tab-2">Problem</span><span class="tab tab-3">Evidence</span><span class="tab tab-4">Findings</span><span class="tab tab-5 on">Defense</span>
|
||
</div>
|
||
</section>
|
||
|
||
</main>
|
||
</div>
|
||
|
||
<!-- Page counter lives outside the scaled stage -->
|
||
<div class="deck-controls" aria-hidden="true"><span class="cur" id="pageCur">1</span> / <span id="pageTotal">10</span></div>
|
||
|
||
<script>
|
||
/* ===========================================
|
||
SLIDE PRESENTATION CONTROLLER
|
||
Fixed 1920×1080 stage, scaled uniformly to the viewport.
|
||
Navigation: ← → Space PageUp/PageDown Home/End, wheel,
|
||
touch swipe. Current slide is mirrored into the URL hash.
|
||
=========================================== */
|
||
(function () {
|
||
'use strict';
|
||
|
||
var STAGE_W = 1920;
|
||
var STAGE_H = 1080;
|
||
|
||
var stage = document.getElementById('deckStage');
|
||
var slides = Array.prototype.slice.call(document.querySelectorAll('.slide'));
|
||
var pageCur = document.getElementById('pageCur');
|
||
var pageTotal = document.getElementById('pageTotal');
|
||
var current = 0;
|
||
var wheelLockUntil = 0;
|
||
|
||
pageTotal.textContent = String(slides.length);
|
||
|
||
/* --- Stage scaling: one transform, letterbox as needed --- */
|
||
function fitStage() {
|
||
var factor = Math.min(window.innerWidth / STAGE_W, window.innerHeight / STAGE_H);
|
||
var x = (window.innerWidth - STAGE_W * factor) / 2;
|
||
var y = (window.innerHeight - STAGE_H * factor) / 2;
|
||
stage.style.transform = 'translate(' + x + 'px, ' + y + 'px) scale(' + factor + ')';
|
||
}
|
||
|
||
/* --- Hash routing: #/3 means slide index 3 (0-based) --- */
|
||
function indexFromHash() {
|
||
var m = /^#\/(\d+)$/.exec(window.location.hash);
|
||
if (!m) return null;
|
||
var idx = parseInt(m[1], 10);
|
||
return isNaN(idx) ? null : idx;
|
||
}
|
||
|
||
function show(index, skipHash) {
|
||
current = Math.max(0, Math.min(index, slides.length - 1));
|
||
slides.forEach(function (slide, i) {
|
||
slide.classList.toggle('active', i === current);
|
||
slide.classList.toggle('visible', i === current);
|
||
});
|
||
pageCur.textContent = String(current + 1);
|
||
if (!skipHash) {
|
||
var target = '#/' + current;
|
||
if (window.location.hash !== target) {
|
||
window.history.replaceState(null, '', target);
|
||
}
|
||
}
|
||
}
|
||
|
||
function next() { show(current + 1); }
|
||
function prev() { show(current - 1); }
|
||
|
||
/* --- Keyboard --- */
|
||
document.addEventListener('keydown', function (e) {
|
||
if (e.target && e.target.getAttribute && e.target.getAttribute('contenteditable')) 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(); show(0); break;
|
||
case 'End':
|
||
e.preventDefault(); show(slides.length - 1); break;
|
||
}
|
||
});
|
||
|
||
/* --- Mouse wheel (debounced) --- */
|
||
document.addEventListener('wheel', function (e) {
|
||
var now = Date.now();
|
||
if (now < wheelLockUntil) return;
|
||
if (Math.abs(e.deltaY) < 12) return;
|
||
wheelLockUntil = now + 650;
|
||
if (e.deltaY > 0) { next(); } else { prev(); }
|
||
}, { passive: true });
|
||
|
||
/* --- Touch swipe --- */
|
||
var touchX = null;
|
||
document.addEventListener('touchstart', function (e) {
|
||
if (e.touches.length === 1) touchX = e.touches[0].clientX;
|
||
}, { passive: true });
|
||
document.addEventListener('touchend', function (e) {
|
||
if (touchX === null) return;
|
||
var dx = e.changedTouches[0].clientX - touchX;
|
||
touchX = null;
|
||
if (Math.abs(dx) < 40) return;
|
||
if (dx < 0) { next(); } else { prev(); }
|
||
}, { passive: true });
|
||
|
||
/* --- Hash navigation (back/forward, deep links) --- */
|
||
window.addEventListener('hashchange', function () {
|
||
var idx = indexFromHash();
|
||
if (idx !== null && idx !== current) show(idx, true);
|
||
});
|
||
|
||
window.addEventListener('resize', fitStage);
|
||
|
||
fitStage();
|
||
var initial = indexFromHash();
|
||
show(initial !== null ? initial : 0, initial === null);
|
||
})();
|
||
</script>
|
||
|
||
</body>
|
||
</html>
|