Changes
diff --git a/simple-slides/README.md b/simple-slides/README.md
new file mode 100644
index 0000000..c965a6d
--- /dev/null
+++ b/simple-slides/README.md
@@ -0,0 +1,55 @@
+# Random Slides
+
+A minimalist, browser-based slideshow application that allows you to create, edit, and play Markdown-based slides. It runs entirely in your browser with no backend required and persists your data locally.
+
+## Features
+
+- **Markdown Support**: Write your slides using standard Markdown syntax.
+- **No Setup**: Runs directly in the browser using modern web standards—no build steps or installation required.
+- **Local Persistence**: Your slides are automatically saved to your browser's local storage.
+- **Drag-and-Drop Reordering**: Easily rearrange your slides by dragging the slide headers.
+- **Customizable Font Size**: Set a base font size for the presentation mode (defaults to 256px for high visibility).
+- **Shuffle Mode**: Present your slides in a randomized order for study or brainstorming.
+- **Keyboard Navigation**: Simple controls for a focused presentation experience.
+
+## Quick Start
+
+### Running Locally
+
+Since the app uses ES Modules, it needs to be served via a web server (rather than opened as a local file).
+
+1. Navigate to the project directory in your terminal.
+2. Start a simple local server:
+ ```bash
+ # If you have Python installed:
+ python3 -m http.server
+
+ # Or if you have Node.js/npx:
+ npx serve .
+ ```
+3. Open your browser to `http://localhost:8000` (or the port provided by your server).
+
+## How to Use
+
+### Edit Mode
+- **Adding Slides**: Click the **"+ Add Slide"** button at the bottom.
+- **Editing Content**: Type directly into the text boxes using Markdown.
+- **Reordering**: Click and hold the header of a slide (where it says "Slide X") and drag it to a new position.
+- **Deleting**: Click the **"Delete"** button on any slide card.
+- **Settings**: Adjust the **"Font Size"** in the top header to change how large the text appears in Play Mode.
+
+### Play Mode
+- **Start Playing**: Click **"Play"** for the standard order or **"Shuffle Play"** for a randomized order.
+- **Navigation**:
+ - Click the mouse or press **any key** (except ESC) to advance to the next slide.
+ - Press **ESC** to exit Play Mode and return to the editor.
+- **Layout**: Text is automatically centered both horizontally and vertically.
+
+## Technologies Used
+- [Preact](https://preactjs.com/) (Lightweight React alternative)
+- [htm](https://github.com/developit/htm) (JSX-like syntax for standard JavaScript)
+- [Marked](https://marked.js.org/) (Markdown parsing)
+- Standard Browser APIs (LocalStorage, Drag and Drop API, ES Modules)
+
+## License
+MIT
diff --git a/simple-slides/design.md b/simple-slides/design.md
new file mode 100644
index 0000000..3f3a7fd
--- /dev/null
+++ b/simple-slides/design.md
@@ -0,0 +1,151 @@
+# Technical Design Document: Random Slides
+
+## 1. Overview
+"Random Slides" is a lightweight, browser-based slideshow application that allows users to create, edit, and play text-based slides using Markdown. The application operates entirely in the browser (Single Page Application) with no backend and requires no build steps. Data is persisted using the browser's `localStorage`.
+
+## 2. Technical Constraints & Requirements
+- **No Build Step**: Must use native ES Modules (ESM) and libraries available via CDN (UMD/ESM).
+- **Pure Frontend**: No server-side logic.
+- **Persistence**: `localStorage`.
+- **Framework**: Preact (via `htm` for JSX-like syntax without transpilation).
+- **Format**: Markdown for slide content.
+
+## 3. Architecture
+
+### 3.1 Tech Stack
+- **Core Library**: `Preact` (imported via `esm.sh` or `unpkg`).
+- **Templating**: `htm` (Hyperscript Tagged Markup) to write component layouts similar to JSX.
+- **Markdown Renderer**: `marked` (via CDN).
+- **Styling**: Standard CSS3.
+- **State Management**: Preact standard `useState`, `useEffect`.
+
+### 3.2 File Structure
+Since there is no bundler, we will use standard ES modules.
+```
+/
+├── index.html # Entry point, loads styles and scripts
+├── style.css # Global styles
+└── src/
+ ├── main.js # App entry and mount
+ ├── App.js # Main container and state manager
+ ├── components/
+ │ ├── Editor.js # Edit mode view
+ │ └── Player.js # Play mode view
+ └── utils/
+ ├── storage.js # LocalStorage wrapper
+ └── array.js # Shuffle logic
+```
+
+## 4. Data Model
+
+### 4.1 Slide Object
+```javascript
+{
+ id: string, // Unique UUID/ID for React keys and drag-drop tracking
+ content: string // Markdown text content
+}
+```
+
+### 4.2 Application State
+The `App` component will hold the central state:
+- `slides`: `Array<SlideObject>` - The source of truth for all slides.
+- `fontSize`: `number` - Base font size in pixels (default: 256).
+- `mode`: `'edit' | 'play'` - Current application mode.
+- `playDeck`: `Array<SlideObject>` - The subset/ordered list of slides currently being played (handling normal vs. shuffled).
+- `currentIndex`: `number` - Current slide index in the `playDeck`.
+
+## 5. Component Design
+
+### 5.1 `App.js` (Container)
+- **Responsibilities**:
+ - Initialize state from `localStorage` (slides and settings).
+ - Persist changes to `slides` and `settings` to `localStorage`.
+ - Handle transitions between 'edit' and 'play' modes.
+ - Handle "Play" vs "Shuffle Play" logic (generating the `playDeck`).
+ - Manage `fontSize` state.
+
+### 5.2 `Editor.js`
+- **Props**:
+ - `slides`: Array of slides.
+ - `fontSize`: Current font size setting.
+ - `onUpdate`: Function to update the slides array.
+ - `onUpdateFontSize`: Function to update the font size.
+ - `onPlay`: Function to start normal playback.
+ - `onShufflePlay`: Function to start shuffled playback.
+- **Features**:
+ - Renders a list of textareas.
+ - **Settings Control**: Input for Base Font Size (px).
+ - **Drag and Drop**: Implemented using HTML5 Drag and Drop API.
+ - **Actions**: Add Slide, Delete Slide.
+
+### 5.3 `Player.js`
+- **Props**:
+ - `deck`: Array of slides to play.
+ - `fontSize`: Base font size to apply.
+ - `onExit`: Function to return to edit mode.
+- **State**:
+ - `index`: Internal tracking of current slide (initialized to 0).
+- **Features**:
+ - Renders current slide using `marked`.
+ - **Style**:
+ - Apply `fontSize` (px).
+ - Content centered horizontally (text-align) and vertically (flexbox).
+ - **Event Listeners**:
+ - `click` / `keydown` (any key except ESC): Advance slide.
+ - `keydown` (ESC): Trigger `onExit`.
+ - **Rendering**:
+ - Centered, large text.
+ - HTML rendered from Markdown (unsanitized as per requirements).
+
+## 6. Key Workflows
+
+### 6.1 Initialization
+1. App loads.
+2. `storage.js` reads key `random_slides_data`.
+3. If null, return default `[{ id: uuid(), content: '' }]`.
+4. App mounts in `mode: 'edit'`.
+
+### 6.2 Editing
+1. User types in textarea.
+2. `onChange` triggers update to specific slide in `slides` array.
+3. `useEffect` in `App` observes `slides` and saves to `localStorage`.
+
+### 6.3 Drag and Drop Reordering
+1. User drags a slide handle.
+2. `dragstart`: Capture index.
+3. `drop`: Calculate new index, splice array, update state.
+
+### 6.4 Play Mode
+1. User clicks "Play".
+ - `playDeck` = `slides`.
+ - `mode` = `'play'`.
+2. User clicks "Shuffle".
+ - `playDeck` = `shuffle(slides)`.
+ - `mode` = `'play'`.
+
+### 6.5 Navigation (Play Mode)
+1. Render `playDeck[currentIndex]`.
+2. Input event (Click/Key):
+ - `currentIndex++`.
+ - If `currentIndex >= playDeck.length`, show "End of Slideshow" or loop? (Default: Loop or Stop. Prd implies "advance", will implement loop or stop at last slide. Design decision: **Loop** for continuous flow, or **Stop** at end. Let's **Stop** at the end to avoid confusion).
+3. ESC Key:
+ - Call `onExit()`.
+ - Reset `mode` to `'edit'`.
+
+## 7. UI/UX Plan
+- **Edit Mode**:
+ - Clean vertical list of cards.
+ - Each card has a handle (for dragging), textarea, and delete button.
+ - Floating or sticky header/footer for "Add Slide", "Play", "Shuffle".
+- **Play Mode**:
+ - Full screen overlay or full viewport.
+ - Minimalist.
+ - Content vertically and horizontally centered.
+ - High contrast text.
+
+## 8. Third-Party Libraries (CDN)
+- **Preact**: `https://esm.sh/preact`
+- **Preact Hooks**: `https://esm.sh/preact/hooks`
+- **htm**: `https://esm.sh/htm`
+- **marked**: `https://esm.sh/marked`
+- **uuid**: `https://esm.sh/uuid` (for reliable ID generation).
diff --git a/simple-slides/index.html b/simple-slides/index.html
new file mode 100644
index 0000000..677113a
--- /dev/null
+++ b/simple-slides/index.html
@@ -0,0 +1,24 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+ <meta charset="UTF-8">
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
+ <title>Random Slides</title>
+ <link rel="stylesheet" href="style.css">
+ <script type="importmap">
+ {
+ "imports": {
+ "preact": "https://esm.sh/preact@10.19.3",
+ "preact/hooks": "https://esm.sh/preact@10.19.3/hooks",
+ "htm": "https://esm.sh/htm@3.1.1",
+ "marked": "https://esm.sh/marked@11.1.1",
+ "uuid": "https://esm.sh/uuid@9.0.1"
+ }
+ }
+ </script>
+</head>
+<body>
+ <div id="app"></div>
+ <script type="module" src="./src/main.js"></script>
+</body>
+</html>
diff --git a/simple-slides/prd.md b/simple-slides/prd.md
new file mode 100644
index 0000000..af98523
--- /dev/null
+++ b/simple-slides/prd.md
@@ -0,0 +1,30 @@
+# A Simple Slideshow Web App
+
+* A pure frontend app runs in the browser that display a slideshow.
+ There’s not backend. Data are stored in browser storage.
+* The app should be able to run directly in browser, without any build
+ steps.
+* The slideshow is just a bunch of slides
+* The data for each slide is just a markdown string.
+* The app will have two modes: edit and play
+* In edit mode, the app shows a list of text boxes, where the user can
+ input the markdown string in each. Each box represents a slide.
+* In edit mode the user can add new slides and delete slides.
+* In edit mode, the user can adjust the order of the slide by draggin
+ the text boxes.
+* Edit mode has a button to play the slideshow, and the app switches
+ to play mode.
+* The user will also have the choice to play the slideshow shuffled.
+* In play mode, the markdown strings are rendered to big texts and
+ displayed. Don’t worry about sanitizing the markdown.
+* In play mode, the user can advance to the next slide by clicking the
+ mouse or press any key, except the ESC key. Pressing the ESC key
+ will switch the app to edit mode. There’s no way to go to the
+ previous slide. Don’t worry about mobile use cases.
+* When the user opens the app for the first time, they are in edit
+ mode with one empty slide.
+* Preact can be used if needed.
+* In the edit mode, there should be a control to set the base font
+ size. It should default to 256 (px).
+* In play mode, the text should be center both horizontally and
+ vertically.
diff --git a/simple-slides/src/App.js b/simple-slides/src/App.js
new file mode 100644
index 0000000..b039fd6
--- /dev/null
+++ b/simple-slides/src/App.js
@@ -0,0 +1,75 @@
+import { h } from 'preact';
+import { useState, useEffect } from 'preact/hooks';
+import { v4 as uuid } from 'uuid';
+import htm from 'htm';
+import { loadSlides, saveSlides, loadSettings, saveSettings } from './utils/storage.js';
+import { shuffle } from './utils/array.js';
+import { Editor } from './components/Editor.js';
+import { Player } from './components/Player.js';
+
+const html = htm.bind(h);
+
+export function App() {
+ // Initialize slides from storage or default
+ const [slides, setSlides] = useState(() => {
+ const saved = loadSlides();
+ if (saved && saved.length > 0) return saved;
+ return [{ id: uuid(), content: '' }];
+ });
+
+ // Initialize settings
+ const [fontSize, setFontSize] = useState(() => {
+ const settings = loadSettings();
+ return settings.fontSize || 256;
+ });
+
+ const [mode, setMode] = useState('edit'); // 'edit' | 'play'
+ const [playDeck, setPlayDeck] = useState([]);
+
+ // Auto-save whenever slides change
+ useEffect(() => {
+ saveSlides(slides);
+ }, [slides]);
+
+ // Auto-save settings
+ useEffect(() => {
+ saveSettings({ fontSize });
+ }, [fontSize]);
+
+ const handleUpdateSlides = (newSlides) => {
+ setSlides(newSlides);
+ };
+
+ const handlePlay = () => {
+ setPlayDeck(slides);
+ setMode('play');
+ };
+
+ const handlePlayShuffled = () => {
+ setPlayDeck(shuffle(slides));
+ setMode('play');
+ };
+
+ const handleExitPlay = () => {
+ setMode('edit');
+ setPlayDeck([]);
+ };
+
+ return html`
+ ${mode === 'edit'
+ ? html`<${Editor}
+ slides=${slides}
+ fontSize=${fontSize}
+ onUpdate=${handleUpdateSlides}
+ onUpdateFontSize=${setFontSize}
+ onPlay=${handlePlay}
+ onPlayShuffled=${handlePlayShuffled}
+ />`
+ : html`<${Player}
+ deck=${playDeck}
+ fontSize=${fontSize}
+ onExit=${handleExitPlay}
+ />`
+ }
+ `;
+}
\ No newline at end of file
diff --git a/simple-slides/src/components/Editor.js b/simple-slides/src/components/Editor.js
new file mode 100644
index 0000000..32c1d68
--- /dev/null
+++ b/simple-slides/src/components/Editor.js
@@ -0,0 +1,115 @@
+import { h } from 'preact';
+import { useState } from 'preact/hooks';
+import { v4 as uuid } from 'uuid';
+import htm from 'htm';
+
+const html = htm.bind(h);
+
+export function Editor({ slides, fontSize, onUpdate, onUpdateFontSize, onPlay, onPlayShuffled }) {
+ const [draggedIndex, setDraggedIndex] = useState(null);
+ const [dropTargetIndex, setDropTargetIndex] = useState(null);
+
+ const updateSlide = (index, newContent) => {
+ const newSlides = [...slides];
+ newSlides[index] = { ...newSlides[index], content: newContent };
+ onUpdate(newSlides);
+ };
+
+ const addSlide = () => {
+ onUpdate([...slides, { id: uuid(), content: '' }]);
+ };
+
+ const deleteSlide = (index) => {
+ const newSlides = slides.filter((_, i) => i !== index);
+ onUpdate(newSlides);
+ };
+
+ // Drag and Drop handlers
+ const handleDragStart = (e, index) => {
+ if (e.target.tagName === 'TEXTAREA' || e.target.tagName === 'BUTTON') {
+ e.preventDefault(); // Prevent dragging the card when interacting with inputs
+ return;
+ }
+
+ setDraggedIndex(index);
+ e.dataTransfer.effectAllowed = "move";
+ e.dataTransfer.setData("text/plain", String(index));
+ };
+
+ const handleDragOver = (e, index) => {
+ e.preventDefault();
+ e.dataTransfer.dropEffect = "move";
+ if (draggedIndex !== index) {
+ setDropTargetIndex(index);
+ }
+ };
+
+ const handleDragLeave = (e) => {
+ // Optional: clear if leaving the list entirely, but often tricky with child elements
+ };
+
+ const handleDrop = (e, targetIndex) => {
+ e.preventDefault();
+
+ const sourceIndexStr = e.dataTransfer.getData("text/plain");
+ if (!sourceIndexStr && sourceIndexStr !== '0') return;
+
+ const sourceIndex = parseInt(sourceIndexStr, 10);
+ if (isNaN(sourceIndex) || sourceIndex === targetIndex) {
+ setDropTargetIndex(null);
+ return;
+ }
+
+ const newSlides = [...slides];
+ const [movedItem] = newSlides.splice(sourceIndex, 1);
+ newSlides.splice(targetIndex, 0, movedItem);
+
+ onUpdate(newSlides);
+ setDraggedIndex(null);
+ setDropTargetIndex(null);
+ };
+
+ const handleDragEnd = () => {
+ setDraggedIndex(null);
+ setDropTargetIndex(null);
+ };
+
+ return html`
+ <div class="editor-container">
+ <div class="editor-header">
+ <h1>Random Slides</h1>
+ <div class="controls">
+ <div style="display: flex; align-items: center; margin-right: 15px;">
+ <label for="font-size" style="margin-right: 5px; font-size: 0.9rem;">Font Size:</label>
+ <input id="font-size" type="number" value=${fontSize} onInput=${(e) => onUpdateFontSize(Number(e.target.value))} style="width: 60px; padding: 5px;" />
+ </div>
+ <button class="btn btn-primary" onClick=${onPlay} disabled=${slides.length === 0}>Play</button>
+ <button class="btn btn-secondary" onClick=${onPlayShuffled} disabled=${slides.length === 0}>Shuffle Play</button>
+ </div>
+ </div>
+
+ <div class="slide-list">
+ ${slides.map((slide, index) => html`
+ <div class="slide-card ${draggedIndex === index ? 'dragging' : ''} ${dropTargetIndex === index ? 'drop-target' : ''}"
+ draggable="true"
+ onDragStart=${(e) => handleDragStart(e, index)}
+ onDragOver=${(e) => handleDragOver(e, index)}
+ onDrop=${(e) => handleDrop(e, index)}
+ onDragEnd=${handleDragEnd}
+ key=${slide.id}>
+ <div class="card-header">
+ <span>Slide ${index + 1}</span>
+ <button class="btn btn-danger" onClick=${() => deleteSlide(index)}>Delete</button>
+ </div>
+ <textarea class="slide-input"
+ value=${slide.content}
+ onInput=${(e) => updateSlide(index, e.target.value)}
+ placeholder="# Markdown content here..."></textarea>
+ </div>
+ `)}
+ </div>
+
+ <button class="add-slide-btn" onClick=${addSlide}>+ Add Slide</button>
+ </div>
+ `;
+}
\ No newline at end of file
diff --git a/simple-slides/src/components/Player.js b/simple-slides/src/components/Player.js
new file mode 100644
index 0000000..79a287d
--- /dev/null
+++ b/simple-slides/src/components/Player.js
@@ -0,0 +1,61 @@
+import { h } from 'preact';
+import { useState, useEffect, useRef } from 'preact/hooks';
+import { marked } from 'marked';
+import htm from 'htm';
+
+const html = htm.bind(h);
+
+export function Player({ deck, fontSize, onExit }) {
+ const [index, setIndex] = useState(0);
+ const containerRef = useRef(null);
+
+ useEffect(() => {
+ const handleKeyDown = (e) => {
+ if (e.key === 'Escape') {
+ e.preventDefault();
+ onExit();
+ } else {
+ setIndex(prev => {
+ if (prev < deck.length - 1) {
+ return prev + 1;
+ }
+ return prev;
+ });
+ }
+ };
+
+ window.addEventListener('keydown', handleKeyDown);
+
+ if (containerRef.current) {
+ containerRef.current.focus();
+ }
+
+ return () => window.removeEventListener('keydown', handleKeyDown);
+ }, [deck.length, onExit]);
+
+ const handleClick = (e) => {
+ setIndex(prev => {
+ if (prev < deck.length - 1) {
+ return prev + 1;
+ }
+ return prev;
+ });
+ };
+
+ const currentSlide = deck[index];
+ const rawHtml = marked.parse(currentSlide.content || '# (Empty Slide)');
+
+ return html`
+ <div class="player-container"
+ ref=${containerRef}
+ onClick=${handleClick}
+ title="Click to advance">
+ <div class="slide-content"
+ style="font-size: ${fontSize}px;"
+ dangerouslySetInnerHTML=${{ __html: rawHtml }}></div>
+ <div class="hint-overlay">
+ ${index + 1} / ${deck.length} • Press ESC to exit
+ </div>
+ </div>
+ `;
+}
\ No newline at end of file
diff --git a/simple-slides/src/main.js b/simple-slides/src/main.js
new file mode 100644
index 0000000..e521f23
--- /dev/null
+++ b/simple-slides/src/main.js
@@ -0,0 +1,7 @@
+import { h, render } from 'preact';
+import htm from 'htm';
+import { App } from './App.js';
+
+const html = htm.bind(h);
+
+render(html`<${App} />`, document.getElementById('app'));
diff --git a/simple-slides/src/utils/array.js b/simple-slides/src/utils/array.js
new file mode 100644
index 0000000..08cf6ca
--- /dev/null
+++ b/simple-slides/src/utils/array.js
@@ -0,0 +1,8 @@
+export const shuffle = (array) => {
+ const newArray = [...array];
+ for (let i = newArray.length - 1; i > 0; i--) {
+ const j = Math.floor(Math.random() * (i + 1));
+ [newArray[i], newArray[j]] = [newArray[j], newArray[i]];
+ }
+ return newArray;
+};
diff --git a/simple-slides/src/utils/storage.js b/simple-slides/src/utils/storage.js
new file mode 100644
index 0000000..58f4624
--- /dev/null
+++ b/simple-slides/src/utils/storage.js
@@ -0,0 +1,38 @@
+const KEY = 'random_slides_data';
+const SETTINGS_KEY = 'random_slides_settings';
+
+export const loadSlides = () => {
+ try {
+ const data = localStorage.getItem(KEY);
+ return data ? JSON.parse(data) : null;
+ } catch (e) {
+ console.error("Failed to load slides", e);
+ return null;
+ }
+};
+
+export const saveSlides = (slides) => {
+ try {
+ localStorage.setItem(KEY, JSON.stringify(slides));
+ } catch (e) {
+ console.error("Failed to save slides", e);
+ }
+};
+
+export const loadSettings = () => {
+ try {
+ const data = localStorage.getItem(SETTINGS_KEY);
+ return data ? JSON.parse(data) : { fontSize: 256 };
+ } catch (e) {
+ console.error("Failed to load settings", e);
+ return { fontSize: 256 };
+ }
+};
+
+export const saveSettings = (settings) => {
+ try {
+ localStorage.setItem(SETTINGS_KEY, JSON.stringify(settings));
+ } catch (e) {
+ console.error("Failed to save settings", e);
+ }
+};
\ No newline at end of file
diff --git a/simple-slides/style.css b/simple-slides/style.css
new file mode 100644
index 0000000..492cd1a
--- /dev/null
+++ b/simple-slides/style.css
@@ -0,0 +1,238 @@
+:root {
+ --bg-color: #f4f4f9;
+ --card-bg: #ffffff;
+ --text-color: #333;
+ --primary-color: #007bff;
+ --danger-color: #dc3545;
+ --border-color: #ddd;
+ --font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
+}
+
+* {
+ box-sizing: border-box;
+}
+
+body {
+ margin: 0;
+ padding: 0;
+ font-family: var(--font-family);
+ background-color: var(--bg-color);
+ color: var(--text-color);
+}
+
+#app {
+ display: flex;
+ flex-direction: column;
+ height: 100vh;
+}
+
+/* Editor Styles */
+.editor-container {
+ max-width: 800px;
+ margin: 0 auto;
+ padding: 20px;
+ width: 100%;
+ flex: 1;
+ display: flex;
+ flex-direction: column;
+}
+
+.editor-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ margin-bottom: 20px;
+ position: sticky;
+ top: 0;
+ background-color: var(--bg-color);
+ z-index: 10;
+ padding: 10px 0;
+ border-bottom: 1px solid transparent;
+}
+
+.editor-header h1 {
+ margin: 0;
+ font-size: 1.5rem;
+}
+
+.controls {
+ display: flex;
+ gap: 10px;
+}
+
+.slide-list {
+ display: flex;
+ flex-direction: column;
+ gap: 15px;
+ padding-bottom: 80px; /* Space for FAB or just scrolling */
+}
+
+.slide-card {
+ background: var(--card-bg);
+ border: 1px solid var(--border-color);
+ border-radius: 8px;
+ padding: 15px;
+ display: flex;
+ flex-direction: column;
+ gap: 10px;
+ box-shadow: 0 2px 4px rgba(0,0,0,0.05);
+ transition: transform 0.2s, box-shadow 0.2s;
+ position: relative;
+}
+
+.slide-card.dragging {
+ opacity: 0.5;
+ border: 2px dashed var(--primary-color);
+}
+
+.slide-card.drop-target {
+ border: 2px solid var(--primary-color);
+ box-shadow: 0 0 10px rgba(0,123,255,0.3);
+ transform: scale(1.02);
+}
+
+.card-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ color: #888;
+ font-size: 0.9rem;
+ cursor: grab;
+ user-select: none;
+ padding-bottom: 5px;
+ border-bottom: 1px solid #eee;
+ margin-bottom: 5px;
+}
+
+.card-header:active {
+ cursor: grabbing;
+}
+
+.slide-input {
+ width: 100%;
+ min-height: 100px;
+ padding: 10px;
+ border: 1px solid var(--border-color);
+ border-radius: 4px;
+ font-family: monospace;
+ font-size: 1rem;
+ resize: vertical;
+}
+
+.btn {
+ padding: 8px 16px;
+ border: none;
+ border-radius: 4px;
+ cursor: pointer;
+ font-weight: 600;
+ font-size: 0.9rem;
+ transition: background-color 0.2s;
+}
+
+.btn-primary {
+ background-color: var(--primary-color);
+ color: white;
+}
+
+.btn-primary:hover {
+ background-color: #0056b3;
+}
+
+.btn-danger {
+ background-color: transparent;
+ color: var(--danger-color);
+ border: 1px solid var(--danger-color);
+ padding: 4px 8px;
+ font-size: 0.8rem;
+}
+
+.btn-danger:hover {
+ background-color: var(--danger-color);
+ color: white;
+}
+
+.btn-secondary {
+ background-color: #6c757d;
+ color: white;
+}
+
+.btn-secondary:hover {
+ background-color: #545b62;
+}
+
+.add-slide-btn {
+ margin-top: 20px;
+ width: 100%;
+ padding: 15px;
+ background-color: white;
+ border: 2px dashed var(--border-color);
+ color: #888;
+ font-size: 1rem;
+ cursor: pointer;
+}
+
+.add-slide-btn:hover {
+ border-color: var(--primary-color);
+ color: var(--primary-color);
+}
+
+/* Player Styles */
+.player-container {
+ position: fixed;
+ top: 0;
+ left: 0;
+ width: 100vw;
+ height: 100vh;
+ background-color: white;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ z-index: 1000;
+ padding: 40px;
+ overflow: hidden;
+}
+
+.slide-content {
+ max-width: 90vw;
+ max-height: 90vh;
+ font-size: 2rem; /* Fallback base size */
+ line-height: 1.5;
+ overflow-y: auto;
+ width: 100%;
+ text-align: center;
+}
+
+/* Markdown Styles in Player */
+.slide-content h1 { font-size: 2.5em; margin-bottom: 0.5em; }
+.slide-content h2 { font-size: 2em; margin-bottom: 0.5em; }
+.slide-content h3 { font-size: 1.75em; margin-bottom: 0.5em; }
+.slide-content p { margin-bottom: 1em; }
+.slide-content ul, .slide-content ol { margin-bottom: 1em; padding-left: 1.5em; }
+.slide-content pre {
+ background: #f4f4f4;
+ padding: 1em;
+ border-radius: 5px;
+ overflow-x: auto;
+ font-size: 0.6em; /* Code blocks smaller */
+}
+.slide-content img {
+ max-width: 100%;
+ height: auto;
+ display: block;
+ margin: 0 auto;
+}
+.slide-content blockquote {
+ border-left: 5px solid #ccc;
+ padding-left: 1em;
+ color: #666;
+ font-style: italic;
+}
+
+.hint-overlay {
+ position: absolute;
+ bottom: 20px;
+ right: 20px;
+ color: #ccc;
+ font-size: 0.8rem;
+ pointer-events: none;
+}
\ No newline at end of file