mirror of
https://github.com/nexu-io/open-design.git
synced 2026-08-03 06:05:05 +08:00
* feat(deck): enhance slide deck functionality with speaker notes and navigation improvements - Added a switch for including speaker notes in slide decks, defaulting to true. - Updated keyboard navigation to include 'R' for resetting to the first slide. - Implemented click navigation for half-slide transitions. - Enhanced the deck framework to support hidden programmatic prev/next controls. - Improved handling of file inputs in question forms to support file submissions. This update aims to improve user experience in creating and presenting slide decks, ensuring better accessibility and functionality. * feat(artifacts): enforce semantic naming for user-facing deliverables - Updated guidelines to require semantic filenames derived from user briefs for new artifacts, moving away from defaulting to `index.html`. - Enhanced documentation to clarify naming conventions for HTML files, emphasizing the use of descriptive names like `investor-pitch-deck.html`. - Adjusted prompts and hints across the system to reflect these changes, ensuring consistency in user-facing outputs. - Improved test coverage to validate the new naming conventions and ensure proper implementation in various contexts. This update aims to enhance clarity and usability in project file management, promoting better organization and user experience. * feat(file-viewer): enhance speaker notes display and improve file workspace functionality - Updated the speaker notes section to include a styled placeholder for empty notes, improving user experience. - Changed the icon in the file workspace from "slides" to "panel-left" for better clarity. - Introduced new sub-category filtering options in the file workspace, enhancing organization and navigation. - Improved styling for the page creator search input and added visual feedback for loading states. These changes aim to enhance usability and visual consistency across the file viewer and workspace components. * test(web): align workspace tests with pages switcher * test(e2e): open design files through pages menu * fix(deck): keep source mode reachable * fix(workspace): restore uploaded file tabs * feat(deck-thumbnail): implement virtualized thumbnail rail for improved performance - Introduced a new `DeckThumbnailRail` component to optimize the rendering of deck thumbnails, reducing memory usage and improving load times. - Enhanced the `FileViewer` to integrate the new thumbnail rail, allowing for lazy loading of thumbnails based on viewport visibility. - Added tests for the `DeckThumbnailRail` to ensure correct functionality and caching behavior. - Updated styles for better visual consistency and performance during thumbnail rendering. These changes aim to enhance user experience by providing a smoother and more efficient way to navigate through deck slides. * feat(deck): enhance deck rendering with thumbnail support and keyboard shortcuts - Integrated `DeckSlideThumbnail` for rendering individual slides in a shadow root, improving performance and reducing main-thread load. - Updated `DeckThumbnailRail` to utilize the new thumbnail component, allowing for efficient slide navigation. - Implemented keyboard shortcuts for deck navigation, enhancing user experience during presentations. - Added tests for `buildDeckRenderInput` to ensure proper functionality of the deck stage fallback. - Improved styling for loading states in the viewer, providing a smoother visual experience. These changes aim to optimize the deck viewing experience by reducing resource consumption and enhancing interactivity. * feat(deck): improve thumbnail loading and speaker notes editing experience - Refactored `DeckSlideThumbnail` to enhance loading states with a new shimmer effect, improving visual feedback during thumbnail rendering. - Updated `DeckThumbnailRail` to manage loading states for thumbnails, ensuring a smoother user experience. - Simplified the speaker notes editing interface by removing the toggle switch, allowing direct editing from the notes body. - Enhanced accessibility by adding roles and focus states to speaker notes elements. - Added tests to verify the new loading states and speaker notes functionality. These changes aim to optimize the user experience in deck navigation and speaker notes management, providing a more intuitive and responsive interface. * feat(file-viewer): enhance version export functionality and speaker notes interaction - Introduced new export options for file versions, including PDF and image formats, improving the ability to download and share content. - Implemented a streamlined process for exporting version content, with progress indicators and error handling for a better user experience. - Enhanced speaker notes editing by refining event handling to prevent unintended focus shifts, ensuring a smoother editing experience. - Updated styles for improved visual consistency in the file version download interface. - Added tests to verify the functionality of version exports and speaker notes interactions. These changes aim to optimize the file viewer's usability and enhance the overall experience when managing and exporting project files. * fix(web): polish export menus and creator filters * fix(file-viewer): fall back to preview exports * test(file-viewer): cover version export fallbacks * test(file-viewer): assert loading skeleton by role * fix(file-viewer): restore html source mode affordance * fix(file-viewer): capture full version fallback snapshots * docs: trim slides handoff eof * test(exports): cover full preview snapshot requests * test(file-viewer): account for snapshot options arg * refactor(file-viewer, file-workspace): streamline export functions and enhance page creator dialog visibility * test(file-workspace): enhance slide template creation and mock plugin responses * feat(export): add support for versioned exports and enhance HTML export functionality * feat(export): implement versioned export support across multiple formats and enhance related tests * feat(file-viewer): enhance manual edit functionality with commit sequence tracking and add test for inline text session saving * style(speaker-notes): update filmstrip and thumb-frame styles for improved responsiveness and add corresponding tests * feat(file-viewer): enhance comment composer functionality with portal metrics and improve draw toolbar layout for mobile views * feat(analytics): track slide deck viewing experience Add analytics for the new deck/slides experience introduced in this PR: the deck viewer surface, in-deck slide navigation, thumbnail rail, and speaker-notes authoring. Presentation entry stays on the existing present_popover click to avoid double-counting. - contracts: DeckViewerSurfaceViewProps (surface_view), DeckViewerClickProps (ui_click), SpeakerNotesSaveResultProps (new speaker_notes_save_result event), wired into the event unions. - web: track* wrappers + fire helpers in FileViewer — deck_viewer surface_view on deck mount, slide_prev/next/reset, thumbnail_select, thumbnail_rail_toggle (expand/collapse), speaker_notes_edit, and speaker_notes_save_result (preview + presenter surfaces). - tests: deck analytics suite asserting the emitted (event, props) pairs. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * feat(file-viewer, file-workspace): enhance export toast functionality and improve localization for page creation * feat(home-view, plugins-home-section, agent-skill): update default facet preference, enhance category selection handling, and improve localization support - Set `preferDefaultFacet` to true in HomeView component for better user experience. - Introduced `handlePickCategory` function in PluginsHomeSection to manage category selection based on card layout visibility. - Updated PRIMARY_CATEGORIES to reflect the new order and added localization for titles and descriptions in adaptAgentSkill. - Enhanced tests to verify the correct handling of display copy and tags from SKILL frontmatter. * feat(plugins-home, file-workspace): introduce commercial category labels and enhance deck handling - Added a new module for resolving and localizing commercial category labels for plugins. - Updated the FileWorkspace and PluginCard components to display the commercial category label on deck cards. - Enhanced the PageCreatorPresetFrame to adjust frame classes based on deck presets. - Refactored subcategory handling in facets to align with the new commercial category taxonomy. - Improved localization support for new category labels across multiple languages. * feat(deck-preview): implement deck preview scaling and enhance home hero layout - Introduced `useDeckPreviewScale` hook to manage scaling of deck previews within a fixed 1280×720 logical viewport, ensuring proportional rendering across various templates. - Updated `FileWorkspace`, `HomeHero`, and `PluginCard` components to utilize the new scaling functionality for improved visual consistency. - Enhanced the layout of the home hero section to accommodate new subtype chips and descriptions, including a "More" dropdown for overflow handling. - Improved localization support for new UI elements across multiple languages. * fix(PreviewModal): clarify sidebar prop documentation and update test description - Updated the documentation for the `PreviewSidebar` interface to specify that the sidebar's stage-edge handles are not shown when the sidebar prop is absent. - Modified the test suite description for `PreviewModal` to be more concise and relevant. * test(e2e): select prototype visual card --------- Co-authored-by: free666799 <293857035+free666799@users.noreply.github.com> Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
1142 lines
36 KiB
HTML
1142 lines
36 KiB
HTML
<!DOCTYPE html>
|
||
<html lang='en'>
|
||
<head>
|
||
<meta charset='utf-8' />
|
||
<meta name='viewport' content='width=device-width, initial-scale=1' />
|
||
<title>Turn a Study into a Lab Meeting Deck like a Senior Postdoc - Academic research</title>
|
||
<meta name='description' content='A gut-microbiome lab-meeting research deck on kami parchment. Warm parchment with ink-blue cover and chapter slides, serif at one weight, no italic.' />
|
||
<link rel='preconnect' href='https://fonts.googleapis.com' />
|
||
<link rel='preconnect' href='https://fonts.gstatic.com' crossorigin />
|
||
<link href='https://fonts.googleapis.com/css2?family=Source+Serif+4:wght@400;500&family=Source+Sans+3:wght@400;500&family=JetBrains+Mono:wght@400;500&display=swap' rel='stylesheet' />
|
||
<style>
|
||
/*
|
||
* kami-deck — single-file slide deck stylesheet.
|
||
*
|
||
* Kami token language (warm parchment, ink-blue accent, serif at one
|
||
* weight, no italic) — but laid out as a horizontal swipe deck. Macro
|
||
* tokens are scaled × ~1.6 vs. print baseline; letter-spacing scaled
|
||
* × ~0.6 (per design-systems/kami/DESIGN.md §3).
|
||
*
|
||
* Navigation model is borrowed from skills/guizang-ppt — same flex
|
||
* track + transform translateX, same key/wheel/touch handlers.
|
||
*/
|
||
|
||
:root {
|
||
/* surface */
|
||
--parchment: #f5f4ed;
|
||
--ivory: #faf9f5;
|
||
|
||
/* brand (single chromatic accent) */
|
||
--brand: #1B365D;
|
||
--brand-light: #2D5A8A;
|
||
--brand-steel: #B5C8DC; /* highlight ink on navy: cover/end .hl */
|
||
|
||
/* text */
|
||
--near-black: #141413;
|
||
--dark-warm: #3d3d3a;
|
||
--olive: #504e49;
|
||
--stone: #6b6a64;
|
||
|
||
/* border */
|
||
--border: #e8e6dc;
|
||
--border-soft: #e5e3d8;
|
||
|
||
/* tag tints (solid hex, NEVER rgba) */
|
||
--tag-08: #EEF2F7;
|
||
--tag-14: #E4ECF5;
|
||
|
||
/* type */
|
||
--serif: 'Source Serif 4', Charter, Georgia, Palatino, 'Times New Roman', serif;
|
||
--sans: 'Source Sans 3', -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif;
|
||
--mono: 'JetBrains Mono', 'SF Mono', 'Fira Code', Consolas, Monaco, monospace;
|
||
}
|
||
|
||
* { box-sizing: border-box; margin: 0; padding: 0; }
|
||
html, body {
|
||
width: 100%; height: 100%;
|
||
overflow: hidden;
|
||
background: var(--parchment);
|
||
color: var(--near-black);
|
||
font-family: var(--serif);
|
||
font-weight: 400;
|
||
-webkit-font-smoothing: antialiased;
|
||
}
|
||
strong { font-weight: 500; }
|
||
|
||
/* ---------- deck flex track ---------- */
|
||
#deck {
|
||
position: fixed; inset: 0;
|
||
height: 100vh;
|
||
display: flex;
|
||
flex-wrap: nowrap;
|
||
transition: transform 0.9s cubic-bezier(0.77, 0, 0.175, 1);
|
||
z-index: 5;
|
||
will-change: transform;
|
||
}
|
||
.slide {
|
||
width: 100vw; height: 100vh;
|
||
flex: 0 0 100vw;
|
||
position: relative;
|
||
padding: 80px 96px;
|
||
display: flex;
|
||
flex-direction: column;
|
||
overflow: hidden;
|
||
background: var(--parchment);
|
||
color: var(--near-black);
|
||
}
|
||
.slide.dark {
|
||
background: var(--brand);
|
||
color: var(--ivory);
|
||
}
|
||
.slide-inner {
|
||
max-width: 1280px;
|
||
margin: 0 auto;
|
||
width: 100%;
|
||
height: 100%;
|
||
display: grid;
|
||
align-content: center;
|
||
gap: 24px;
|
||
position: relative;
|
||
min-height: 0;
|
||
}
|
||
|
||
/* ---------- per-slide chrome strip ---------- */
|
||
.slide-chrome {
|
||
position: absolute;
|
||
top: 28px; left: 0; right: 0;
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
padding: 0 96px;
|
||
font-family: var(--sans);
|
||
font-size: 11px;
|
||
font-weight: 500;
|
||
letter-spacing: 0.6px;
|
||
text-transform: uppercase;
|
||
color: var(--stone);
|
||
z-index: 4;
|
||
pointer-events: none;
|
||
}
|
||
.slide.dark .slide-chrome { color: rgba(250, 249, 245, 0.74); } /* 11px caps on navy: keep >= AA */
|
||
.slide-chrome b { color: var(--near-black); font-weight: 500; }
|
||
.slide.dark .slide-chrome b { color: var(--ivory); }
|
||
.slide-chrome .left { display: inline-flex; align-items: center; gap: 12px; }
|
||
.slide-chrome .right {
|
||
display: inline-flex; align-items: center; gap: 12px;
|
||
font-variant-numeric: tabular-nums;
|
||
}
|
||
.slide-chrome .mark {
|
||
/* printer's mark: square hairline seal, not a generic pill chip */
|
||
height: 22px;
|
||
padding: 0 9px;
|
||
border-radius: 2px;
|
||
border: 1px solid currentColor;
|
||
display: inline-flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
font-family: var(--serif);
|
||
font-weight: 500;
|
||
font-size: 11px;
|
||
letter-spacing: 0.5px;
|
||
text-transform: lowercase;
|
||
opacity: 0.85;
|
||
}
|
||
.slide-foot {
|
||
position: absolute;
|
||
bottom: 28px; left: 0; right: 0;
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
padding: 0 96px;
|
||
font-family: var(--mono);
|
||
font-size: 10.5px;
|
||
letter-spacing: 0.4px;
|
||
color: var(--stone);
|
||
font-variant-numeric: tabular-nums;
|
||
z-index: 4;
|
||
pointer-events: none;
|
||
}
|
||
.slide.dark .slide-foot { color: rgba(250, 249, 245, 0.74); } /* 10.5px mono on navy: keep >= AA */
|
||
.slide-foot .counter {
|
||
font-family: var(--mono);
|
||
letter-spacing: 0.04em;
|
||
color: var(--near-black);
|
||
background: var(--ivory);
|
||
padding: 4px 8px;
|
||
border: 1px solid var(--border);
|
||
border-radius: 2px; /* match the square hairline language of the mark */
|
||
}
|
||
.slide.dark .slide-foot .counter {
|
||
color: var(--ivory);
|
||
background: rgba(250, 249, 245, 0.08);
|
||
border-color: rgba(250, 249, 245, 0.22);
|
||
}
|
||
|
||
/* ---------- progress bar ---------- */
|
||
.deck-progress {
|
||
position: fixed;
|
||
left: 0; right: 0; bottom: 0;
|
||
height: 2px;
|
||
background: rgba(27, 54, 93, 0.08);
|
||
z-index: 30;
|
||
}
|
||
.deck-progress .bar {
|
||
height: 100%;
|
||
background: var(--brand);
|
||
width: 0%;
|
||
transition: width 0.6s cubic-bezier(0.77, 0, 0.175, 1);
|
||
}
|
||
|
||
/* ---------- dot nav ---------- */
|
||
#nav {
|
||
position: fixed;
|
||
left: 50%;
|
||
bottom: 40px;
|
||
transform: translateX(-50%);
|
||
z-index: 30;
|
||
display: flex;
|
||
gap: 9px;
|
||
padding: 8px 14px;
|
||
/* flat parchment strip with a hairline, square corners: the glassy
|
||
blurred pill was outside the kami print language */
|
||
border-radius: 2px;
|
||
background: rgba(245, 244, 237, 0.92);
|
||
border: 1px solid var(--border);
|
||
}
|
||
#nav .dot {
|
||
width: 7px; height: 7px;
|
||
border-radius: 50%;
|
||
background: rgba(27, 54, 93, 0.22);
|
||
cursor: pointer;
|
||
transition: background-color 0.3s cubic-bezier(0.16, 1, 0.3, 1),
|
||
width 0.3s cubic-bezier(0.16, 1, 0.3, 1),
|
||
transform 0.3s cubic-bezier(0.16, 1, 0.3, 1);
|
||
border: 0;
|
||
padding: 0;
|
||
}
|
||
#nav .dot:hover {
|
||
background: rgba(27, 54, 93, 0.5);
|
||
transform: scale(1.15);
|
||
}
|
||
#nav .dot.active {
|
||
background: var(--brand);
|
||
width: 22px;
|
||
border-radius: 1px; /* active marker reads as an ink dash, not a capsule */
|
||
}
|
||
|
||
#hint {
|
||
position: fixed;
|
||
bottom: 64px; right: 28px;
|
||
z-index: 30;
|
||
font-family: var(--mono);
|
||
font-size: 10px;
|
||
letter-spacing: 0.4px;
|
||
text-transform: uppercase;
|
||
color: var(--stone);
|
||
/* hint floats over both parchment and navy slides; bare stone text was
|
||
sub-AA on the dark cover/end, so it gets the same parchment hairline
|
||
chip language as the nav strip and counter */
|
||
background: rgba(245, 244, 237, 0.92);
|
||
border: 1px solid var(--border);
|
||
border-radius: 2px;
|
||
padding: 5px 9px;
|
||
}
|
||
|
||
/* ---------- COVER slide (dark) ---------- */
|
||
/* opening beat: transform+opacity only, ease-out, staggered 0.08s.
|
||
Scoped to the cover; the swipe track paints every slide at load, so
|
||
animating all of them would burn the effect off-screen. */
|
||
@keyframes kami-rise {
|
||
from { opacity: 0; transform: translateY(18px); }
|
||
to { opacity: 1; transform: none; }
|
||
}
|
||
.s-cover .slide-inner > * {
|
||
animation: kami-rise 0.7s cubic-bezier(0.16, 1, 0.3, 1) both;
|
||
}
|
||
.s-cover .slide-inner > *:nth-child(2) { animation-delay: 0.08s; }
|
||
.s-cover .slide-inner > *:nth-child(3) { animation-delay: 0.16s; }
|
||
.s-cover .slide-inner > *:nth-child(4) { animation-delay: 0.24s; }
|
||
.s-cover .slide-inner {
|
||
grid-template-columns: 1fr;
|
||
text-align: left;
|
||
align-content: center;
|
||
gap: 28px;
|
||
max-width: 980px;
|
||
}
|
||
.s-cover .eyebrow {
|
||
font-family: var(--sans);
|
||
font-size: 12px;
|
||
font-weight: 500;
|
||
letter-spacing: 1.2px;
|
||
text-transform: uppercase;
|
||
color: rgba(250, 249, 245, 0.74); /* small caps on navy: keep >= AA */
|
||
}
|
||
.s-cover h1 {
|
||
font-family: var(--serif);
|
||
font-weight: 500;
|
||
font-size: clamp(60px, 7vw, 110px);
|
||
line-height: 1.05;
|
||
letter-spacing: -1.2px;
|
||
color: var(--ivory);
|
||
margin: 0;
|
||
}
|
||
.s-cover h1 .hl { color: var(--brand-steel); }
|
||
.s-cover .tagline {
|
||
font-family: var(--serif);
|
||
font-weight: 500;
|
||
font-size: 21px;
|
||
color: rgba(250, 249, 245, 0.75);
|
||
max-width: 50ch;
|
||
line-height: 1.45;
|
||
}
|
||
.s-cover .meta {
|
||
margin-top: 20px;
|
||
display: flex;
|
||
align-items: center;
|
||
flex-wrap: wrap;
|
||
gap: 14px 20px;
|
||
font-family: var(--mono);
|
||
font-size: 11px;
|
||
letter-spacing: 0.4px;
|
||
text-transform: uppercase;
|
||
color: rgba(250, 249, 245, 0.72); /* 11px mono on navy: keep >= AA */
|
||
font-variant-numeric: tabular-nums;
|
||
}
|
||
.s-cover .meta .rule {
|
||
width: 56px; height: 1px;
|
||
background: rgba(250, 249, 245, 0.4);
|
||
display: inline-block;
|
||
}
|
||
|
||
/* ---------- CHAPTER divider (dark) ---------- */
|
||
.s-chapter .slide-inner {
|
||
grid-template-columns: 1fr;
|
||
text-align: center;
|
||
align-content: center;
|
||
gap: 36px;
|
||
}
|
||
.s-chapter .num {
|
||
font-family: var(--serif);
|
||
font-weight: 500;
|
||
font-size: clamp(64px, 8vw, 130px);
|
||
color: rgba(250, 249, 245, 0.62); /* ghosted chapter numeral, kept >= 3:1 for large text */
|
||
letter-spacing: -0.5px;
|
||
line-height: 1;
|
||
font-variant-numeric: tabular-nums;
|
||
}
|
||
.s-chapter h2 {
|
||
font-family: var(--serif);
|
||
font-weight: 500;
|
||
font-size: clamp(48px, 5.4vw, 84px);
|
||
line-height: 1.1;
|
||
letter-spacing: -0.6px;
|
||
color: var(--ivory);
|
||
margin: 0 auto;
|
||
max-width: 22ch;
|
||
}
|
||
.s-chapter .lede {
|
||
font-family: var(--serif);
|
||
font-weight: 500;
|
||
font-size: 19px;
|
||
color: rgba(250, 249, 245, 0.78);
|
||
max-width: 42ch;
|
||
margin: 0 auto;
|
||
line-height: 1.5;
|
||
}
|
||
|
||
/* ---------- CONTENT slide ---------- */
|
||
.s-content .slide-inner {
|
||
grid-template-columns: 1fr 2.4fr;
|
||
gap: 56px;
|
||
align-content: center;
|
||
}
|
||
.s-content.layout-full .slide-inner { grid-template-columns: 1fr; max-width: 980px; }
|
||
.s-content .head { display: flex; flex-direction: column; gap: 16px; }
|
||
.s-content .num {
|
||
font-family: var(--serif);
|
||
font-weight: 500;
|
||
font-size: 14px;
|
||
letter-spacing: 0.4px;
|
||
color: var(--brand);
|
||
font-variant-numeric: tabular-nums;
|
||
}
|
||
.s-content h2 {
|
||
font-family: var(--serif);
|
||
font-weight: 500;
|
||
font-size: clamp(36px, 4vw, 56px);
|
||
line-height: 1.1;
|
||
letter-spacing: -0.6px;
|
||
color: var(--near-black);
|
||
margin: 0;
|
||
max-width: 14ch;
|
||
}
|
||
.s-content .lede {
|
||
font-family: var(--serif);
|
||
font-weight: 500;
|
||
font-size: 15px;
|
||
color: var(--olive);
|
||
max-width: 28ch;
|
||
line-height: 1.5;
|
||
}
|
||
.s-content .body { display: flex; flex-direction: column; gap: 18px; }
|
||
.s-content .body p {
|
||
font-family: var(--serif);
|
||
font-weight: 400;
|
||
font-size: 17px;
|
||
line-height: 1.55;
|
||
color: var(--dark-warm);
|
||
max-width: 64ch;
|
||
}
|
||
.s-content .body p strong { color: var(--near-black); font-weight: 500; }
|
||
.s-content .body code {
|
||
font-family: var(--mono);
|
||
font-size: 14px;
|
||
color: var(--brand);
|
||
background: var(--tag-08);
|
||
padding: 1px 6px;
|
||
border-radius: 3px;
|
||
}
|
||
.s-content ul.dash {
|
||
list-style: none; padding: 0; margin: 4px 0 0;
|
||
display: flex; flex-direction: column; gap: 10px;
|
||
}
|
||
.s-content ul.dash li {
|
||
position: relative; padding-left: 18px;
|
||
font-family: var(--serif);
|
||
font-weight: 400;
|
||
font-size: 16px;
|
||
line-height: 1.55;
|
||
color: var(--dark-warm);
|
||
}
|
||
.s-content ul.dash li::before {
|
||
/* hairline tick instead of a literal dash glyph (anti-slop: no -/-- in visible text) */
|
||
content: '';
|
||
position: absolute; left: 0; top: 0.72em;
|
||
width: 10px; height: 1px;
|
||
background: var(--brand);
|
||
}
|
||
.s-content .body .tag-row { margin-top: 4px; }
|
||
.s-content .tag {
|
||
display: inline-block;
|
||
font-family: var(--sans);
|
||
font-size: 12px;
|
||
font-weight: 500;
|
||
padding: 2px 8px;
|
||
border-radius: 2px;
|
||
color: var(--brand);
|
||
background: var(--tag-14);
|
||
letter-spacing: 0.4px;
|
||
}
|
||
|
||
/* ---------- STATS slide ---------- */
|
||
.s-stats .slide-inner { grid-template-columns: 1fr; gap: 48px; }
|
||
.s-stats .head { display: flex; flex-direction: column; gap: 18px; }
|
||
.s-stats .num {
|
||
font-family: var(--serif);
|
||
font-weight: 500;
|
||
font-size: 14px;
|
||
letter-spacing: 0.4px;
|
||
color: var(--brand);
|
||
font-variant-numeric: tabular-nums;
|
||
}
|
||
.s-stats h2 {
|
||
font-family: var(--serif);
|
||
font-weight: 500;
|
||
font-size: clamp(36px, 4.2vw, 60px);
|
||
line-height: 1.1;
|
||
letter-spacing: -0.6px;
|
||
color: var(--near-black);
|
||
max-width: 22ch;
|
||
margin: 0;
|
||
}
|
||
.s-stats .grid {
|
||
display: grid;
|
||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||
gap: 0;
|
||
border-top: 1px solid var(--border);
|
||
border-bottom: 1px solid var(--border);
|
||
}
|
||
.s-stats .stat {
|
||
padding: 32px 24px 28px;
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 8px;
|
||
border-right: 1px solid var(--border-soft);
|
||
}
|
||
.s-stats .stat:last-child { border-right: 0; }
|
||
.s-stats .stat .v {
|
||
font-family: var(--serif);
|
||
font-weight: 500;
|
||
font-size: clamp(48px, 6vw, 88px);
|
||
line-height: 1;
|
||
letter-spacing: -0.8px;
|
||
color: var(--brand);
|
||
font-variant-numeric: tabular-nums;
|
||
}
|
||
.s-stats .stat .l {
|
||
font-family: var(--serif);
|
||
font-weight: 500;
|
||
font-size: 15px;
|
||
color: var(--near-black);
|
||
margin-top: 8px;
|
||
}
|
||
.s-stats .stat .s {
|
||
font-family: var(--serif);
|
||
font-weight: 400;
|
||
font-size: 13px;
|
||
color: var(--olive);
|
||
line-height: 1.5;
|
||
max-width: 28ch;
|
||
}
|
||
.s-stats .caption {
|
||
font-family: var(--mono);
|
||
font-size: 11px;
|
||
color: var(--stone);
|
||
letter-spacing: 0.4px;
|
||
font-variant-numeric: tabular-nums;
|
||
}
|
||
|
||
/* ---------- QUOTE slide ---------- */
|
||
.s-quote .slide-inner {
|
||
grid-template-columns: 1fr;
|
||
max-width: 1080px;
|
||
gap: 36px;
|
||
align-content: center;
|
||
}
|
||
.s-quote blockquote {
|
||
border-left: 2px solid var(--brand);
|
||
padding: 6px 0 6px 28px;
|
||
font-family: var(--serif);
|
||
font-weight: 500;
|
||
font-size: clamp(28px, 3.4vw, 42px);
|
||
line-height: 1.32;
|
||
color: var(--near-black);
|
||
margin: 0;
|
||
letter-spacing: -0.4px; /* display serif tracks tight, matching h1/h2; loose +tracking was residue */
|
||
max-width: 36ch;
|
||
}
|
||
.s-quote .author {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 16px;
|
||
margin-top: 12px;
|
||
}
|
||
.s-quote .author .glyph {
|
||
/* hanko-style seal: hairline square in brand ink, not a solid avatar disc */
|
||
width: 40px; height: 40px;
|
||
border-radius: 3px;
|
||
border: 1px solid var(--brand);
|
||
background: transparent; color: var(--brand);
|
||
font-family: var(--serif); font-weight: 500;
|
||
font-size: 17px;
|
||
display: inline-flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
text-transform: uppercase;
|
||
letter-spacing: 0;
|
||
}
|
||
.s-quote .author p {
|
||
font-family: var(--serif);
|
||
font-weight: 500;
|
||
font-size: 14px;
|
||
color: var(--near-black);
|
||
}
|
||
.s-quote .author p span {
|
||
display: block;
|
||
color: var(--olive);
|
||
font-weight: 400;
|
||
margin-top: 2px;
|
||
}
|
||
|
||
/* ---------- CTA slide ---------- */
|
||
.s-cta .slide-inner {
|
||
grid-template-columns: 1fr;
|
||
max-width: 980px;
|
||
gap: 28px;
|
||
align-content: center;
|
||
text-align: left;
|
||
}
|
||
.s-cta .eyebrow {
|
||
font-family: var(--sans);
|
||
font-size: 12px; font-weight: 500;
|
||
letter-spacing: 1.2px;
|
||
text-transform: uppercase;
|
||
color: var(--brand);
|
||
}
|
||
.s-cta h2 {
|
||
font-family: var(--serif);
|
||
font-weight: 500;
|
||
font-size: clamp(48px, 5.4vw, 88px);
|
||
line-height: 1.05;
|
||
letter-spacing: -0.6px;
|
||
color: var(--near-black);
|
||
margin: 0;
|
||
}
|
||
.s-cta .body {
|
||
font-family: var(--serif);
|
||
font-weight: 400;
|
||
font-size: 17px;
|
||
color: var(--dark-warm);
|
||
max-width: 50ch;
|
||
line-height: 1.55;
|
||
}
|
||
.s-cta .actions {
|
||
display: inline-flex;
|
||
gap: 12px;
|
||
margin-top: 12px;
|
||
align-items: center;
|
||
flex-wrap: wrap;
|
||
}
|
||
.s-cta .btn {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
font-family: var(--sans);
|
||
font-size: 13px;
|
||
font-weight: 500;
|
||
letter-spacing: 0.4px;
|
||
padding: 11px 18px;
|
||
border-radius: 2px;
|
||
border: 0;
|
||
cursor: pointer;
|
||
text-decoration: none;
|
||
}
|
||
.s-cta .btn-primary {
|
||
background: var(--brand);
|
||
color: var(--ivory);
|
||
box-shadow: 0 0 0 1px var(--brand);
|
||
}
|
||
.s-cta .btn-primary:hover { background: var(--brand-light); box-shadow: 0 0 0 1px var(--brand-light); }
|
||
.s-cta .btn-ghost {
|
||
background: transparent;
|
||
color: var(--brand);
|
||
box-shadow: 0 0 0 1px var(--brand);
|
||
}
|
||
.s-cta .btn-ghost:hover { background: var(--tag-08); }
|
||
|
||
/* ---------- END slide (dark) ---------- */
|
||
.s-end .slide-inner {
|
||
grid-template-columns: 1fr;
|
||
align-content: end;
|
||
padding-bottom: 24px;
|
||
text-align: left;
|
||
gap: 18px;
|
||
max-width: none;
|
||
}
|
||
.s-end .word {
|
||
font-family: var(--serif);
|
||
font-weight: 500;
|
||
/* 16vw clipped "Open Design." mid-glyph behind overflow-x: hidden at every
|
||
common width (needs ~0.93vw of run length vs ~0.85vw available after
|
||
padding); 13vw keeps the full word on screen. */
|
||
font-size: clamp(88px, 13vw, 210px);
|
||
line-height: 1;
|
||
letter-spacing: -1.2px;
|
||
color: var(--ivory);
|
||
white-space: nowrap;
|
||
overflow-x: hidden;
|
||
}
|
||
.s-end .word .hl { color: var(--brand-steel); }
|
||
.s-end .colophon {
|
||
border-top: 1px solid rgba(250, 249, 245, 0.22);
|
||
padding-top: 22px;
|
||
font-family: var(--sans);
|
||
font-size: 11px;
|
||
font-weight: 500;
|
||
letter-spacing: 1.2px;
|
||
text-transform: uppercase;
|
||
color: rgba(250, 249, 245, 0.74); /* 11px caps colophon on navy: keep >= AA */
|
||
font-variant-numeric: tabular-nums;
|
||
}
|
||
|
||
/* ---------- ESC overview ---------- */
|
||
#overview {
|
||
position: fixed; inset: 0;
|
||
z-index: 100;
|
||
background: rgba(245, 244, 237, 0.98); /* near-solid parchment sheet, no glass blur */
|
||
display: none;
|
||
overflow-y: auto;
|
||
padding: 60px 56px;
|
||
}
|
||
#overview .ov-head {
|
||
display: flex; justify-content: space-between; align-items: baseline;
|
||
margin-bottom: 32px;
|
||
font-family: var(--sans); font-size: 11px;
|
||
letter-spacing: 1.2px; text-transform: uppercase;
|
||
color: var(--stone);
|
||
font-variant-numeric: tabular-nums;
|
||
}
|
||
#overview .ov-head b { color: var(--near-black); font-weight: 500; }
|
||
#overview .ov-grid {
|
||
display: grid;
|
||
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
|
||
gap: 22px;
|
||
max-width: 1280px;
|
||
margin: 0 auto;
|
||
}
|
||
#overview .ov-card {
|
||
cursor: pointer;
|
||
border-radius: 3px;
|
||
overflow: hidden;
|
||
border: 1px solid var(--border);
|
||
transition: border-color 0.2s, transform 0.2s;
|
||
background: var(--ivory);
|
||
}
|
||
#overview .ov-card:hover { border-color: var(--brand); transform: translateY(-2px); }
|
||
#overview .ov-card.active { border-color: var(--brand); border-width: 2px; }
|
||
#overview .ov-thumb {
|
||
width: 100%;
|
||
aspect-ratio: 16 / 10;
|
||
overflow: hidden;
|
||
position: relative;
|
||
pointer-events: none;
|
||
background: var(--parchment);
|
||
}
|
||
#overview .ov-thumb .clone {
|
||
width: 100vw; height: 100vh;
|
||
transform: scale(0.18);
|
||
transform-origin: top left;
|
||
position: absolute;
|
||
top: 0; left: 0;
|
||
pointer-events: none;
|
||
}
|
||
#overview .ov-label {
|
||
padding: 8px 12px;
|
||
font-family: var(--mono); font-size: 10px;
|
||
letter-spacing: 0.4px; text-transform: uppercase;
|
||
color: var(--stone);
|
||
display: flex; justify-content: space-between; align-items: center;
|
||
font-variant-numeric: tabular-nums;
|
||
}
|
||
#overview .ov-label b { color: var(--near-black); font-weight: 500; }
|
||
|
||
/* ---------- reduced motion ---------- */
|
||
@media (prefers-reduced-motion: reduce) {
|
||
#deck { transition: none; }
|
||
.s-cover .slide-inner > * { animation: none; }
|
||
.deck-progress .bar { transition: none; }
|
||
#nav .dot { transition: none; }
|
||
#overview .ov-card { transition: none; }
|
||
#overview .ov-card:hover { transform: none; }
|
||
}
|
||
|
||
/* ---------- responsive ---------- */
|
||
@media (max-width: 1080px) {
|
||
.slide { padding: 64px 56px; }
|
||
.slide-chrome, .slide-foot { padding: 0 56px; }
|
||
.s-content .slide-inner { grid-template-columns: 1fr; gap: 28px; }
|
||
}
|
||
@media (max-width: 640px) {
|
||
.slide { padding: 44px 28px; }
|
||
.slide-chrome, .slide-foot { padding: 0 28px; font-size: 9.5px; letter-spacing: 0.6px; }
|
||
#hint { display: none; }
|
||
}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
|
||
<div id='deck'>
|
||
|
||
<!-- ===== 01 · COVER (dark) ===== -->
|
||
<section class='slide s-cover dark' data-slide-kind='cover'>
|
||
<div class='slide-chrome'>
|
||
<span class='left'><span class='mark'>kami</span> <b>Raman Lab</b> · Lab Meeting · Week 14</span>
|
||
<span class='right'>Gut-Sleep Cohort · Academic research</span>
|
||
</div>
|
||
<div class='slide-inner'>
|
||
<span class='eyebrow'>Lab meeting · Raman Lab · Week 14</span>
|
||
<h1>Gut microbes track <span class='hl'>deep sleep</span>, not total sleep.</h1>
|
||
<p class='tagline'>A 6-week shotgun-metagenomics cohort linking the Bacteroides:Firmicutes ratio to slow-wave sleep duration — the design, the results, and what we still can't claim.</p>
|
||
<div class='meta'>
|
||
<span>n = 64 adults · PSG subsample n = 22</span>
|
||
<span class='rule'></span>
|
||
<span>Dept. of Neuroscience · Sleep & Microbiome Lab</span>
|
||
</div>
|
||
</div>
|
||
<div class='slide-foot'>
|
||
<span>Raman Lab · MMXXVI</span>
|
||
<span class='counter'>01 / 09</span>
|
||
</div>
|
||
</section>
|
||
|
||
<!-- ===== 02 · CHAPTER (dark) ===== -->
|
||
<section class='slide s-chapter dark' data-slide-kind='chapter'>
|
||
<div class='slide-chrome'>
|
||
<span class='left'><span class='mark'>kami</span> <b>Raman Lab</b> · Lab Meeting · Week 14</span>
|
||
<span class='right'>Gut-Sleep Cohort · Academic research</span>
|
||
</div>
|
||
<div class='slide-inner'>
|
||
<p class='num'>01</p>
|
||
<h2>The field: fast-moving, still noisy.</h2>
|
||
<p class='lede'>Rodent work shows the gut-brain axis can shape sleep causally, through short-chain fatty acids and vagal afferents. Human evidence stays correlational, small-n, and inconsistent across cohorts.</p>
|
||
</div>
|
||
<div class='slide-foot'>
|
||
<span>Raman Lab · MMXXVI</span>
|
||
<span class='counter'>02 / 09</span>
|
||
</div>
|
||
</section>
|
||
|
||
<!-- ===== 03 · CONTENT ===== -->
|
||
<section class='slide s-content' data-slide-kind='content'>
|
||
<div class='slide-chrome'>
|
||
<span class='left'><span class='mark'>kami</span> <b>Raman Lab</b> · Lab Meeting · Week 14</span>
|
||
<span class='right'>Gut-Sleep Cohort · Academic research</span>
|
||
</div>
|
||
<div class='slide-inner'>
|
||
<div class='head'>
|
||
<p class='num'>01.1</p>
|
||
<h2>The gap no cohort has closed.</h2>
|
||
<p class='lede'>Most human studies measure sleep by wristband, not EEG.</p>
|
||
</div>
|
||
<div class='body'>
|
||
<p>
|
||
Prior cohorts (typically <strong>n < 30</strong>) rely on consumer-grade actigraphy, which is blind to sleep stage — it cannot tell slow-wave sleep from light sleep. None have paired <code>16S</code>-or-deeper microbiome profiling with polysomnography-grade staging in the same participants, and few control for the confounders that move both gut and sleep at once.
|
||
</p>
|
||
<ul class='dash'>
|
||
<li>Actigraphy-only designs can report "sleep quality" but not sleep architecture.</li>
|
||
<li>Diet, BMI, and antibiotic history are rarely logged as covariates.</li>
|
||
<li>No prior cohort repeated sampling across a full six-week window.</li>
|
||
</ul>
|
||
<div class='tag-row'>
|
||
<span class='tag'>Metagenomics</span>
|
||
<span class='tag'>Polysomnography</span>
|
||
<span class='tag'>Gut-brain axis</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div class='slide-foot'>
|
||
<span>Raman Lab · MMXXVI</span>
|
||
<span class='counter'>03 / 09</span>
|
||
</div>
|
||
</section>
|
||
|
||
<!-- ===== 04 · CONTENT ===== -->
|
||
<section class='slide s-content' data-slide-kind='content'>
|
||
<div class='slide-chrome'>
|
||
<span class='left'><span class='mark'>kami</span> <b>Raman Lab</b> · Lab Meeting · Week 14</span>
|
||
<span class='right'>Gut-Sleep Cohort · Academic research</span>
|
||
</div>
|
||
<div class='slide-inner'>
|
||
<div class='head'>
|
||
<p class='num'>01.2</p>
|
||
<h2>Design: what we actually ran.</h2>
|
||
<p class='lede'>Six weeks, 64 adults, a polysomnography subsample.</p>
|
||
</div>
|
||
<div class='body'>
|
||
<p>
|
||
Stool samples were collected at week 0 and week 6 and run through shotgun metagenomic sequencing. All 64 participants wore actigraphy nightly; a <strong>22-person subsample also underwent overnight polysomnography</strong> at both timepoints, giving us gold-standard sleep staging alongside microbial composition.
|
||
</p>
|
||
<p>
|
||
Diet was logged by 3-day food record each week. Linear mixed-effects models carry participant as a random intercept and adjust for BMI, dietary fiber intake, and antibiotic use in the prior 90 days.
|
||
</p>
|
||
<ul class='dash'>
|
||
<li>Week 0 & week 6 stool sampling, shotgun metagenomics (Illumina NovaSeq).</li>
|
||
<li>Nightly actigraphy, all 64; overnight PSG on a 22-person subsample.</li>
|
||
<li>LME models adjust for BMI, fiber intake, and 90-day antibiotic history.</li>
|
||
</ul>
|
||
</div>
|
||
</div>
|
||
<div class='slide-foot'>
|
||
<span>Raman Lab · MMXXVI</span>
|
||
<span class='counter'>04 / 09</span>
|
||
</div>
|
||
</section>
|
||
|
||
<!-- ===== 05 · STATS ===== -->
|
||
<section class='slide s-stats' data-slide-kind='stats'>
|
||
<div class='slide-chrome'>
|
||
<span class='left'><span class='mark'>kami</span> <b>Raman Lab</b> · Lab Meeting · Week 14</span>
|
||
<span class='right'>Gut-Sleep Cohort · Academic research</span>
|
||
</div>
|
||
<div class='slide-inner'>
|
||
<div class='head'>
|
||
<p class='num'>01.3</p>
|
||
<h2>What moved, and what didn't.</h2>
|
||
</div>
|
||
<div class='grid'>
|
||
<div class='stat'>
|
||
<div class='v'>+18min</div>
|
||
<div class='l'>Slow-wave sleep</div>
|
||
<div class='s'>per SD increase in Bacteroides:Firmicutes ratio, PSG subsample.</div>
|
||
</div>
|
||
<div class='stat'>
|
||
<div class='v'>r=0.41</div>
|
||
<div class='l'>SCFA – SWS correlation</div>
|
||
<div class='s'>fecal butyrate vs. slow-wave minutes, FDR q < 0.05.</div>
|
||
</div>
|
||
<div class='stat'>
|
||
<div class='v'>n.s.</div>
|
||
<div class='l'>Total sleep time</div>
|
||
<div class='s'>no association with any measured microbial ratio, p = 0.61.</div>
|
||
</div>
|
||
<div class='stat'>
|
||
<div class='v'>22/64</div>
|
||
<div class='l'>PSG subsample</div>
|
||
<div class='s'>gold-standard staging; remainder actigraphy-only.</div>
|
||
</div>
|
||
</div>
|
||
<p class='caption'>Raman Lab · shotgun metagenomics + PSG cohort · preprint in preparation · MMXXVI</p>
|
||
</div>
|
||
<div class='slide-foot'>
|
||
<span>Raman Lab · MMXXVI</span>
|
||
<span class='counter'>05 / 09</span>
|
||
</div>
|
||
</section>
|
||
|
||
<!-- ===== 06 · CHAPTER (dark) ===== -->
|
||
<section class='slide s-chapter dark' data-slide-kind='chapter'>
|
||
<div class='slide-chrome'>
|
||
<span class='left'><span class='mark'>kami</span> <b>Raman Lab</b> · Lab Meeting · Week 14</span>
|
||
<span class='right'>Gut-Sleep Cohort · Academic research</span>
|
||
</div>
|
||
<div class='slide-inner'>
|
||
<p class='num'>02</p>
|
||
<h2>The claim we can actually make.</h2>
|
||
<p class='lede'>Gut composition tracks sleep architecture — slow-wave depth — not sleep quantity. A distinction prior actigraphy-only studies could never see.</p>
|
||
</div>
|
||
<div class='slide-foot'>
|
||
<span>Raman Lab · MMXXVI</span>
|
||
<span class='counter'>06 / 09</span>
|
||
</div>
|
||
</section>
|
||
|
||
<!-- ===== 07 · QUOTE ===== -->
|
||
<section class='slide s-quote' data-slide-kind='quote'>
|
||
<div class='slide-chrome'>
|
||
<span class='left'><span class='mark'>kami</span> <b>Raman Lab</b> · Lab Meeting · Week 14</span>
|
||
<span class='right'>Gut-Sleep Cohort · Academic research</span>
|
||
</div>
|
||
<div class='slide-inner'>
|
||
<blockquote>
|
||
A correlation this size, in a subsample this small, does not tell us the microbiome causes deeper sleep. It tells us exactly where to point a causal experiment next.
|
||
</blockquote>
|
||
<div class='author'>
|
||
<span class='glyph'>e</span>
|
||
<p>
|
||
Dr. Elena Cho
|
||
<span>Principal Investigator · Sleep & Microbiome Lab</span>
|
||
</p>
|
||
</div>
|
||
</div>
|
||
<div class='slide-foot'>
|
||
<span>Raman Lab · MMXXVI</span>
|
||
<span class='counter'>07 / 09</span>
|
||
</div>
|
||
</section>
|
||
|
||
<!-- ===== 08 · CTA ===== -->
|
||
<section class='slide s-cta' data-slide-kind='cta'>
|
||
<div class='slide-chrome'>
|
||
<span class='left'><span class='mark'>kami</span> <b>Raman Lab</b> · Lab Meeting · Week 14</span>
|
||
<span class='right'>Gut-Sleep Cohort · Academic research</span>
|
||
</div>
|
||
<div class='slide-inner'>
|
||
<span class='eyebrow'>Next in the pipeline</span>
|
||
<h2>Can we move the ratio — and does sleep follow?</h2>
|
||
<p class='body'>
|
||
Proposed follow-up: a 4-week fiber-supplementation crossover trial (n = 40, PSG on every participant) testing whether raising the Bacteroides:Firmicutes ratio causally increases slow-wave sleep. IRB submission targeted for Q3.
|
||
</p>
|
||
<div class='actions'>
|
||
<a class='btn btn-primary' href='#'>Review the IRB draft</a>
|
||
<a class='btn btn-ghost' href='#'>See the full dataset</a>
|
||
</div>
|
||
</div>
|
||
<div class='slide-foot'>
|
||
<span>Raman Lab · MMXXVI</span>
|
||
<span class='counter'>08 / 09</span>
|
||
</div>
|
||
</section>
|
||
|
||
<!-- ===== 09 · END (dark) ===== -->
|
||
<section class='slide s-end dark' data-slide-kind='end'>
|
||
<div class='slide-chrome'>
|
||
<span class='left'><span class='mark'>kami</span> <b>Raman Lab</b> · Lab Meeting · Week 14</span>
|
||
<span class='right'>Gut-Sleep Cohort · Academic research</span>
|
||
</div>
|
||
<div class='slide-inner'>
|
||
<div class='word'>Sleep, <span class='hl'>deeper.</span></div>
|
||
<p class='colophon'>Raman Lab · Dept. of Neuroscience · MMXXVI · Preprint in preparation · Lab meeting Nº 14</p>
|
||
</div>
|
||
<div class='slide-foot'>
|
||
<span>Raman Lab · MMXXVI</span>
|
||
<span class='counter'>09 / 09</span>
|
||
</div>
|
||
</section>
|
||
|
||
</div>
|
||
|
||
<div id='nav'></div>
|
||
<div id='hint'>← / → · esc · swipe</div>
|
||
<div class='deck-progress'><div class='bar'></div></div>
|
||
|
||
<script>
|
||
(function () {
|
||
var deck = document.getElementById('deck');
|
||
if (!deck) return;
|
||
var slides = Array.prototype.slice.call(deck.querySelectorAll('.slide'));
|
||
var nav = document.getElementById('nav');
|
||
var bar = document.querySelector('.deck-progress .bar');
|
||
var total = slides.length;
|
||
var idx = 0, lock = false;
|
||
|
||
deck.style.width = (total * 100) + 'vw';
|
||
|
||
slides.forEach(function (s, i) {
|
||
var b = document.createElement('button');
|
||
b.className = 'dot';
|
||
b.dataset.i = i;
|
||
b.setAttribute('aria-label', 'Slide ' + (i + 1));
|
||
b.onclick = function () { go(i); };
|
||
nav.appendChild(b);
|
||
});
|
||
|
||
/* Unthrottled state update. The interaction throttle (`lock`) only
|
||
guards wheel/key/touch so a fast input burst doesn't overshoot the
|
||
transition; host- and observer-driven sync must bypass it, otherwise
|
||
a host message or restoreInitialSlide that lands inside the 700ms
|
||
window after go(0) silently no-ops and the deck stays on slide 1
|
||
while the host counter advances. */
|
||
function applySlide(n) {
|
||
idx = Math.max(0, Math.min(total - 1, n));
|
||
deck.style.transform = 'translateX(' + (-idx * 100) + 'vw)';
|
||
/* load-bearing: .slide.active is read by Open Design's host bridge
|
||
(src/runtime/srcdoc.ts findActiveByClass) to drive the slide
|
||
counter. No CSS targets it — do not remove. */
|
||
slides.forEach(function (s, i) { s.classList.toggle('active', i === idx); });
|
||
nav.querySelectorAll('.dot').forEach(function (d, i) {
|
||
d.classList.toggle('active', i === idx);
|
||
});
|
||
if (bar) bar.style.width = (((idx + 1) / total) * 100) + '%';
|
||
}
|
||
|
||
function go(n) {
|
||
if (lock) return;
|
||
applySlide(n);
|
||
lock = true;
|
||
setTimeout(function () { lock = false; }, 700);
|
||
}
|
||
|
||
/* ESC overview */
|
||
var overviewOn = false;
|
||
var ov = document.createElement('div');
|
||
ov.id = 'overview';
|
||
document.body.appendChild(ov);
|
||
|
||
function buildOverview() {
|
||
ov.innerHTML = '';
|
||
var head = document.createElement('div');
|
||
head.className = 'ov-head';
|
||
head.innerHTML = '<span><b>Slide overview</b> · esc to close</span><span>' +
|
||
String(idx + 1).padStart(2, '0') + ' / ' + String(total).padStart(2, '0') + '</span>';
|
||
ov.appendChild(head);
|
||
var grid = document.createElement('div');
|
||
grid.className = 'ov-grid';
|
||
slides.forEach(function (s, i) {
|
||
var card = document.createElement('div');
|
||
card.className = 'ov-card' + (i === idx ? ' active' : '');
|
||
var thumb = document.createElement('div');
|
||
thumb.className = 'ov-thumb';
|
||
var clone = s.cloneNode(true);
|
||
clone.className = clone.className + ' clone';
|
||
clone.style.transform = 'scale(0.18)';
|
||
thumb.appendChild(clone);
|
||
var label = document.createElement('div');
|
||
label.className = 'ov-label';
|
||
label.innerHTML = '<b>' + String(i + 1).padStart(2, '0') + '</b><span>' +
|
||
(s.dataset.slideKind || '') + '</span>';
|
||
card.appendChild(thumb);
|
||
card.appendChild(label);
|
||
card.onclick = function () { toggleOverview(); go(i); };
|
||
grid.appendChild(card);
|
||
});
|
||
ov.appendChild(grid);
|
||
}
|
||
|
||
function toggleOverview() {
|
||
overviewOn = !overviewOn;
|
||
if (overviewOn) { buildOverview(); ov.style.display = 'block'; }
|
||
else { ov.style.display = 'none'; }
|
||
}
|
||
|
||
addEventListener('keydown', function (e) {
|
||
if (e.key === 'Escape') { e.preventDefault(); toggleOverview(); return; }
|
||
if (overviewOn) return;
|
||
if (e.key === 'ArrowRight' || e.key === 'PageDown' || e.key === ' ' || e.key === 'ArrowDown') {
|
||
e.preventDefault(); go(idx + 1);
|
||
} else if (e.key === 'ArrowLeft' || e.key === 'PageUp' || e.key === 'ArrowUp') {
|
||
e.preventDefault(); go(idx - 1);
|
||
} else if (e.key === 'Home') {
|
||
e.preventDefault(); go(0);
|
||
} else if (e.key === 'End') {
|
||
e.preventDefault(); go(total - 1);
|
||
}
|
||
});
|
||
|
||
var wheelTO = null, wheelAcc = 0;
|
||
addEventListener('wheel', function (e) {
|
||
if (overviewOn) return;
|
||
wheelAcc += e.deltaY + e.deltaX;
|
||
if (Math.abs(wheelAcc) > 60) {
|
||
go(idx + (wheelAcc > 0 ? 1 : -1));
|
||
wheelAcc = 0;
|
||
}
|
||
clearTimeout(wheelTO);
|
||
wheelTO = setTimeout(function () { wheelAcc = 0; }, 150);
|
||
}, { passive: true });
|
||
|
||
var tx = 0, ty = 0;
|
||
addEventListener('touchstart', function (e) {
|
||
tx = e.touches[0].clientX; ty = e.touches[0].clientY;
|
||
}, { passive: true });
|
||
addEventListener('touchend', function (e) {
|
||
if (overviewOn) return;
|
||
var dx = e.changedTouches[0].clientX - tx;
|
||
var dy = e.changedTouches[0].clientY - ty;
|
||
if (Math.abs(dx) > 50 && Math.abs(dx) > Math.abs(dy)) {
|
||
go(idx + (dx < 0 ? 1 : -1));
|
||
}
|
||
}, { passive: true });
|
||
|
||
/* Host-driven navigation: Open Design's host bridge classifies this deck
|
||
as class-driven (because go() toggles .slide.active) but the visible
|
||
slide is moved by deck.style.transform, which the bridge can't drive.
|
||
Two cooperating handlers keep the deck in sync with the host:
|
||
1. An od:slide message listener routes host nav through go() and
|
||
calls stopImmediatePropagation() so the bridge's own listener
|
||
(registered after this one) doesn't run a second time and
|
||
overshoot by re-reading the freshly-toggled .active class.
|
||
2. A MutationObserver on each slide watches .active and pulls the
|
||
deck transform onto the active index for class changes that
|
||
don't come through a message — chiefly the bridge's
|
||
restoreInitialSlide path, which calls setActive() directly. */
|
||
addEventListener('message', function (e) {
|
||
var data = e && e.data;
|
||
if (!data || data.type !== 'od:slide') return;
|
||
if (typeof e.stopImmediatePropagation === 'function') e.stopImmediatePropagation();
|
||
if (data.action === 'go' && typeof data.index === 'number') applySlide(data.index);
|
||
else if (data.action === 'next') applySlide(idx + 1);
|
||
else if (data.action === 'prev') applySlide(idx - 1);
|
||
else if (data.action === 'first') applySlide(0);
|
||
else if (data.action === 'last') applySlide(total - 1);
|
||
});
|
||
|
||
if (typeof MutationObserver !== 'undefined') {
|
||
var syncFromActiveClass = function () {
|
||
for (var i = 0; i < slides.length; i++) {
|
||
if (slides[i].classList && slides[i].classList.contains('active') && i !== idx) {
|
||
applySlide(i);
|
||
return;
|
||
}
|
||
}
|
||
};
|
||
var mo = new MutationObserver(syncFromActiveClass);
|
||
slides.forEach(function (s) { mo.observe(s, { attributes: true, attributeFilter: ['class'] }); });
|
||
}
|
||
|
||
applySlide(0);
|
||
})();
|
||
</script>
|
||
|
||
</body>
|
||
</html>
|