Home / Blog

Create a Learning Journal in Rise 360 with localStorage and Sequential Unlocking

By Nathan Stoker ยท August 2, 2026 ยท 18 min read

Create a Learning Journal in Rise 360 with localStorage and Sequential Unlocking

What if learners could write reflections throughout your Rise 360 course and see all their thoughts compiled on a final summary page? Even better, what if the course remembered their progress and journal entries when they returned days later?

This tutorial builds on localStorage fundamentals to create something more sophisticated: a learning journal with sequential scenario unlocking, auto-saving reflections, and a polished summary dashboard with data export. The monster-themed example makes workplace scenarios memorable while demonstrating advanced localStorage patterns you can adapt to any subject matter.

How This Builds on localStorage Fundamentals

If you followed the previous tutorial on building a multi-block questionnaire, you already understand how localStorage bridges Rise 360's isolated multimedia blocks. This tutorial takes those concepts further with progressive disclosure, real-time auto-saving, and downloadable exports.

The learning journal pattern works brilliantly for scenario-based training, reflection exercises, coaching modules, and any content where learners benefit from writing down their thoughts. Unlike questionnaires that collect data once, journals encourage ongoing reflection as learners progress through material.

Sequential unlocking adds instructional value by ensuring learners complete each scenario before accessing the next. This prevents skipping ahead and reinforces the learning path you've designed.

What You'll Build

This three-block system guides learners through a monster-themed workplace scenarios course. Block 1 presents four monster characters representing different work styles, and learners select one as their "teammate." Block 2 contains five workplace scenarios that unlock progressively as learners view each one, with optional journal prompts for reflection. Block 3 displays everything on a summary dashboard including the selected monster, progress through scenarios, all journal entries, and learning statistics.

The system includes GSAP animations that respect reduced motion preferences, comprehensive keyboard navigation, screen reader announcements, and a text file export feature so learners can keep their reflections.

Block 1: Character Selection with localStorage

The first block establishes the theme by having learners choose a monster teammate. Each monster represents a workplace personality type with distinct traits. This selection persists throughout the course, appearing on the final summary page.

Rendering Dynamic Cards

Rather than hardcoding HTML for each monster, the code generates cards dynamically from a data array. This pattern makes modifications easier and keeps the markup consistent.

const monsters = [
  {
    id: 1,
    emoji: '๐Ÿ‘น',
    name: 'Grumble',
    role: 'Senior Developer',
    traits: ['Detail-oriented', 'Prefers async communication', 'Night owl'],
    description: 'Grumble works best alone with headphones on.'
  },
  {
    id: 2,
    emoji: '๐Ÿ‘ป',
    name: 'Boo',
    role: 'UX Designer',
    traits: ['Creative', 'Collaborative', 'Empathetic'],
    description: 'Boo loves brainstorming sessions and brings donuts every Friday.'
  },
  // Additional monsters...
];

function renderMonsters() {
  const grid = document.getElementById('monsterGrid');
  
  monsters.forEach(monster => {
    const col = document.createElement('div');
    col.className = 'col-md-6 col-lg-3 monster-card-col';
    
    const isSelected = monster.id === selectedMonster;
    
    col.innerHTML = `
      <div 
        class="monster-card ${isSelected ? 'selected' : ''}" 
        data-monster-id="${monster.id}"
        role="button"
        tabindex="0"
        aria-pressed="${isSelected}"
        aria-label="Select ${monster.name}, ${monster.role}">
        <span class="selection-indicator" aria-hidden="true">
          ${isSelected ? 'โœ“' : ''}
        </span>
        <span class="monster-emoji" role="img" aria-label="${monster.name} emoji">
          ${monster.emoji}
        </span>
        <div class="monster-name">${monster.name}</div>
        <div class="monster-role">${monster.role}</div>
        <div class="mb-2">
          ${monster.traits.map(trait => 
            `<span class="monster-trait">${trait}</span>`
          ).join('')}
        </div>
        <p class="text-muted small">${monster.description}</p>
      </div>
    `;
    
    const card = col.querySelector('.monster-card');
    card.addEventListener('click', () => selectMonster(monster));
    card.addEventListener('keydown', (e) => {
      if (e.key === 'Enter' || e.key === ' ') {
        e.preventDefault();
        selectMonster(monster);
      }
    });
    
    grid.appendChild(col);
  });
}

Saving the Selection

When a learner clicks a monster card, the selection saves to localStorage with all relevant data. The saved object includes the monster ID, name, emoji, role, and timestamp.

function saveMonsterSelection(monster) {
  const data = {
    monsterId: monster.id,
    monsterName: monster.name,
    monsterEmoji: monster.emoji,
    monsterRole: monster.role,
    savedAt: new Date().toISOString()
  };
  
  localStorage.setItem('monstersWork_selectedMonster', JSON.stringify(data));
  
  announceToScreenReader(
    `Selected ${monster.name}, ${monster.role}. Your choice has been saved.`
  );
}

function selectMonster(monster) {
  selectedMonster = monster.id;
  saveMonsterSelection(monster);
  updateUI();
  showCompletion();
  
  if (typeof lmsAPI !== 'undefined' && lmsAPI.SetReachedEnd) {
    lmsAPI.SetReachedEnd();
  }
}

The announceToScreenReader function provides audio feedback for screen reader users, ensuring the interaction is accessible. This pattern appears throughout all three blocks.

Block 2: Sequential Scenarios with Journal Entries

Block 2 demonstrates more complex localStorage patterns. It tracks which scenarios learners have viewed, unlocks subsequent scenarios progressively, and auto-saves journal reflections as learners type.

The Progressive Unlock Pattern

The accordion interface starts with only the first scenario enabled. As learners open each scenario, the next one unlocks. This uses Bootstrap 5's collapse events to detect when an accordion panel opens.

const viewedScenarios = new Set();
const totalScenarios = 5;
let currentUnlocked = 1;

function unlockScenario(num, announce = true) {
  const btn = document.querySelector(`[data-scenario="${num}"]`);
  
  if (btn && btn.disabled) {
    btn.disabled = false;
    btn.classList.remove('locked');
    btn.setAttribute('aria-disabled', 'false');
    btn.setAttribute('data-bs-toggle', 'collapse');
    btn.setAttribute('data-bs-target', `#scenario${num}`);
    btn.textContent = btn.textContent.replace(' ๐Ÿ”’', '');
    
    if (announce) {
      announceToScreenReader(`Scenario ${num} unlocked`);
    }
  }
}

function unlockNext() {
  if (currentUnlocked < totalScenarios) {
    currentUnlocked++;
    unlockScenario(currentUnlocked, true);
    saveProgress();
  }
}

// Listen for accordion open events
document.getElementById('scenarioAccordion').addEventListener(
  'shown.bs.collapse', 
  function(e) {
    const btn = document.querySelector(`[data-bs-target="#${e.target.id}"]`);
    const num = parseInt(btn.getAttribute('data-scenario'));
    
    if (!viewedScenarios.has(num)) {
      viewedScenarios.add(num);
      updateProgress();
      unlockNext();
      saveProgress();
    }
  }
);

The Set data structure prevents duplicate entries if a learner opens the same scenario multiple times. When saving to localStorage, the Set converts to an array since JSON doesn't support Sets directly.

Auto-Saving Journal Entries

Each scenario includes an optional reflection textarea. Rather than requiring learners to click a Save button, the journal auto-saves as they type using the input event.

const journalEntries = {};

function autoSaveJournal(num, value) {
  const trimmed = value.trim();
  
  if (trimmed) {
    journalEntries[num] = trimmed;
  } else {
    delete journalEntries[num];
  }
  
  localStorage.setItem(
    'monstersWork_journalEntries', 
    JSON.stringify(journalEntries)
  );
  
  announceToScreenReader(
    `Reflection for scenario ${num} ${trimmed ? 'saved' : 'cleared'}`
  );
}

// Attach listeners on page load
window.addEventListener('DOMContentLoaded', function() {
  for (let i = 1; i <= totalScenarios; i++) {
    const textarea = document.getElementById(`journal${i}`);
    if (textarea) {
      textarea.addEventListener('input', function() {
        autoSaveJournal(i, this.value);
      });
    }
  }
});

Deleting empty entries rather than storing empty strings keeps the data clean and makes the summary page logic simpler.

Saving and Restoring Progress

Two separate localStorage keys track different data types. The progress key stores viewed scenarios and unlock state. The journal key stores reflection text.

function saveProgress() {
  const data = {
    viewedScenarios: Array.from(viewedScenarios),
    currentUnlocked: currentUnlocked,
    savedAt: new Date().toISOString()
  };
  
  localStorage.setItem('monstersWork_scenarioProgress', JSON.stringify(data));
}

function loadProgress() {
  const saved = localStorage.getItem('monstersWork_scenarioProgress');
  
  if (saved) {
    const data = JSON.parse(saved);
    
    // Restore viewed scenarios
    (data.viewedScenarios || []).forEach(s => {
      viewedScenarios.add(parseInt(s));
    });
    
    currentUnlocked = data.currentUnlocked || 1;
    
    // Unlock all previously unlocked scenarios
    for (let i = 1; i <= currentUnlocked; i++) {
      unlockScenario(i, false);
    }
    
    updateProgress();
  }
  
  // Load journal entries separately
  const savedJournal = localStorage.getItem('monstersWork_journalEntries');
  
  if (savedJournal) {
    const entries = JSON.parse(savedJournal);
    Object.assign(journalEntries, entries);
    
    // Populate textareas with saved content
    Object.keys(entries).forEach(key => {
      const textarea = document.getElementById(`journal${key}`);
      if (textarea) {
        textarea.value = entries[key];
      }
    });
  }
}

Separating concerns into multiple localStorage keys provides flexibility. You could clear journal entries without resetting progress, or vice versa.

Block 3: The Summary Dashboard

The summary dashboard pulls data from all three localStorage keys and presents a comprehensive view of the learner's journey. It includes the selected monster, scenario progress, all journal reflections, statistics, and export functionality.

Loading and Displaying Data

Each section loads independently, allowing partial data display if a learner hasn't completed all blocks.

function loadMonsterSummary() {
  const saved = localStorage.getItem('monstersWork_selectedMonster');
  const section = document.getElementById('monsterSection');
  
  if (saved) {
    const data = JSON.parse(saved);
    section.innerHTML = `
      <div class="monster-summary" role="region" aria-label="Selected monster">
        <span class="monster-emoji-large" role="img" 
              aria-label="${data.monsterName} emoji">
          ${data.monsterEmoji}
        </span>
        <h2 style="color:#7c3aed;">${data.monsterName}</h2>
        <p class="lead">${data.monsterRole}</p>
        <p class="text-muted">
          Selected on ${new Date(data.savedAt).toLocaleDateString()}
        </p>
      </div>
    `;
  } else {
    section.innerHTML = `
      <div class="no-data-message" role="status">
        <p><strong>โš ๏ธ No Monster Selected</strong></p>
        <p>Go back to Block 1 to select your monster teammate!</p>
      </div>
    `;
  }
}

function loadJournalSummary() {
  const saved = localStorage.getItem('monstersWork_journalEntries');
  const section = document.getElementById('journalSection');
  
  if (saved) {
    const entries = JSON.parse(saved);
    const entryCount = Object.keys(entries).length;
    
    if (entryCount > 0) {
      let entriesHTML = '';
      
      Object.keys(entries)
        .sort((a, b) => parseInt(a) - parseInt(b))
        .forEach(key => {
          entriesHTML += `
            <div class="journal-entry-display">
              <h4>๐Ÿ“ Scenario ${key} Reflection</h4>
              <p>${entries[key]}</p>
            </div>
          `;
        });
      
      section.innerHTML = `
        <div class="journal-summary" role="region" 
             aria-label="Journal reflections">
          <h2 class="mb-3">๐Ÿ“” Your Reflections</h2>
          <p class="text-muted mb-3">
            You've written ${entryCount} reflection${entryCount !== 1 ? 's' : ''}
          </p>
          ${entriesHTML}
        </div>
      `;
    }
  }
}

Calculating Learning Statistics

The statistics section aggregates data across all localStorage keys to show completion percentage and counts.

function updateStats() {
  const monster = localStorage.getItem('monstersWork_selectedMonster');
  const progress = localStorage.getItem('monstersWork_scenarioProgress');
  const journal = localStorage.getItem('monstersWork_journalEntries');
  
  const hasMonster = monster ? 1 : 0;
  const scenariosViewed = progress 
    ? JSON.parse(progress).viewedScenarios.length 
    : 0;
  const reflectionsWritten = journal 
    ? Object.keys(JSON.parse(journal)).length 
    : 0;
  
  const totalRequired = 6; // 1 monster + 5 scenarios
  const totalCompleted = hasMonster + scenariosViewed;
  const completionPercent = Math.round((totalCompleted / totalRequired) * 100);
  
  const statsSection = document.getElementById('statsSection');
  statsSection.innerHTML = `
    <div class="col-md-3">
      <div class="stat-box">
        <div class="stat-number">${hasMonster}</div>
        <div class="stat-label">Monster Selected</div>
      </div>
    </div>
    <div class="col-md-3">
      <div class="stat-box">
        <div class="stat-number">${scenariosViewed}</div>
        <div class="stat-label">Scenarios Viewed</div>
      </div>
    </div>
    <div class="col-md-3">
      <div class="stat-box">
        <div class="stat-number">${reflectionsWritten}</div>
        <div class="stat-label">Reflections Written<br><small>(Optional)</small></div>
      </div>
    </div>
    <div class="col-md-3">
      <div class="stat-box">
        <div class="stat-number">${completionPercent}%</div>
        <div class="stat-label">Journey Complete</div>
      </div>
    </div>
  `;
}

Exporting Journal Data

The export feature creates a downloadable text file containing all the learner's data. This gives learners a tangible takeaway from the course.

function exportJourney() {
  const monster = JSON.parse(
    localStorage.getItem('monstersWork_selectedMonster') || 'null'
  );
  const progress = JSON.parse(
    localStorage.getItem('monstersWork_scenarioProgress') || 'null'
  );
  const journal = JSON.parse(
    localStorage.getItem('monstersWork_journalEntries') || 'null'
  );
  
  let textContent = '๐ŸŽญ MY MONSTER JOURNEY\n' + '='.repeat(50) + '\n\n';
  
  if (monster) {
    textContent += `SELECTED MONSTER: ${monster.monsterEmoji} ${monster.monsterName}\n`;
    textContent += `Role: ${monster.monsterRole}\n`;
    textContent += `Selected on: ${new Date(monster.savedAt).toLocaleDateString()}\n\n`;
  }
  
  if (progress) {
    textContent += `SCENARIOS COMPLETED: ${progress.viewedScenarios.length} out of 5\n`;
    textContent += `Scenarios viewed: ${progress.viewedScenarios.join(', ')}\n\n`;
  }
  
  if (journal && Object.keys(journal).length > 0) {
    textContent += 'MY REFLECTIONS\n' + '-'.repeat(50) + '\n\n';
    
    Object.keys(journal)
      .sort((a, b) => a - b)
      .forEach(key => {
        textContent += `๐Ÿ“ Scenario ${key} Reflection:\n`;
        textContent += `${journal[key]}\n\n`;
      });
  }
  
  textContent += '='.repeat(50) + '\n';
  textContent += `Exported on: ${new Date().toLocaleString()}\n`;
  
  // Create downloadable file
  const blob = new Blob([textContent], { type: 'text/plain' });
  const url = URL.createObjectURL(blob);
  const a = document.createElement('a');
  a.href = url;
  a.download = `My-Monster-Journey-${Date.now()}.txt`;
  a.click();
  URL.revokeObjectURL(url);
}

The Blob API creates a file in memory, and the temporary anchor element triggers the download. The URL.revokeObjectURL call cleans up memory after the download starts.

Accessibility Features Throughout

All three blocks include accessibility features that make the experience usable for everyone.

Screen Reader Announcements

A live region announces state changes without requiring focus shifts.

function announceToScreenReader(message) {
  const announcer = document.getElementById('sr-announcements');
  announcer.textContent = message;
}

The announcer element uses aria-live="polite" so messages queue without interrupting current speech.

Keyboard Navigation

Interactive elements support both click and keyboard activation.

card.addEventListener('keydown', (e) => {
  if (e.key === 'Enter' || e.key === ' ') {
    e.preventDefault();
    selectMonster(monster);
  }
});

Reduced Motion Support

GSAP animations respect user preferences for reduced motion.

const prefersReducedMotion = window.matchMedia(
  '(prefers-reduced-motion: reduce)'
).matches;

if (!prefersReducedMotion) {
  gsap.from('.monster-card', {
    opacity: 0,
    y: 30,
    duration: 0.5,
    stagger: 0.1,
    ease: 'power2.out'
  });
}

The CSS also includes a fallback that eliminates animations when reduced motion is preferred.

Complete Code: All Three Blocks

Below you'll find the complete HTML files for the learning journal system. Each file is self-contained with all HTML, CSS, and JavaScript needed to function in Rise 360.

Block 1: Monster Selection (Complete Code)

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Meet Your Monster Team</title>
  
  <!-- Bootstrap 5 CSS -->
  <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
  
  <!-- GSAP Animation -->
  <script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/3.12.5/gsap.min.js"></script>
  
  <style>
    .sr-only {
      position: absolute;
      width: 1px;
      height: 1px;
      padding: 0;
      margin: -1px;
      overflow: hidden;
      clip: rect(0, 0, 0, 0);
      white-space: nowrap;
      border-width: 0;
    }
    
    *:focus-visible {
      outline: 3px solid #0066cc;
      outline-offset: 2px;
    }
    
    .monster-card {
      border: 3px solid #ddd;
      border-radius: 15px;
      padding: 20px;
      margin: 15px 0;
      cursor: pointer;
      transition: all 0.3s ease;
      background: white;
      height: 100%;
      display: flex;
      flex-direction: column;
    }
    
    .monster-card:hover {
      transform: translateY(-5px);
      box-shadow: 0 10px 25px rgba(0,0,0,0.15);
      border-color: #7c3aed;
    }
    
    .monster-card.selected {
      border-color: #7c3aed;
      background: linear-gradient(135deg, #f3e7ff 0%, #ffffff 100%);
    }
    
    .monster-card-col {
      display: flex;
    }
    
    .monster-emoji {
      font-size: 4rem;
      display: block;
      margin-bottom: 10px;
    }
    
    .monster-name {
      font-size: 1.5rem;
      font-weight: bold;
      color: #7c3aed;
      margin-bottom: 10px;
    }
    
    .monster-role {
      color: #6b7280;
      font-size: 0.9rem;
      margin-bottom: 10px;
    }
    
    .monster-trait {
      background: #f3f4f6;
      padding: 8px 12px;
      border-radius: 20px;
      display: inline-block;
      font-size: 0.85rem;
      margin: 3px;
    }
    
    .selection-indicator {
      float: right;
      font-size: 1.5rem;
    }
    
    .completion-badge {
      background: #10b981;
      color: white;
      padding: 10px 20px;
      border-radius: 25px;
      display: inline-block;
      margin: 20px 0;
      font-weight: bold;
    }
    
    .progress-container {
      background: #f3f4f6;
      border-radius: 10px;
      padding: 20px;
      margin: 20px 0;
    }
    
    @media (prefers-reduced-motion: reduce) {
      * {
        animation-duration: 0.01ms !important;
        transition-duration: 0.01ms !important;
      }
    }
  </style>
</head>
<body>
  <!-- Skip to main content -->
  <a href="#main-content" class="sr-only">Skip to main content</a>
  
  <!-- Screen reader announcements -->
  <div id="sr-announcements" role="status" aria-live="polite" aria-atomic="true" class="sr-only"></div>
  
  <main id="main-content" tabindex="-1">
    <div class="container py-4">
      <h1 class="text-center mb-4" style="color: #7c3aed;">๐ŸŽญ Meet Your Monster Team</h1>
      
      <div class="progress-container" role="status" aria-live="polite">
        <div id="progress-text">
          Select your favorite monster to continue! 
          <span class="sr-only">0 monsters selected</span>
        </div>
      </div>
      
      <p class="text-center text-muted mb-4">
        Click on a monster to learn about their workplace personality. Your selection will be saved!
      </p>
      
      <div class="row justify-content-center" id="monsterGrid">
        <!-- Monster cards will be generated here -->
      </div>
      
      <div class="text-center mt-4" id="completionArea"></div>
    </div>
  </main>
  
  <!-- Bootstrap 5 JS -->
  <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
  
  <script>
    // Monster data
    const monsters = [
      {
        id: 1,
        emoji: '๐Ÿ‘น',
        name: 'Grumble',
        role: 'Senior Developer',
        traits: ['Detail-oriented', 'Prefers async communication', 'Night owl'],
        description: 'Grumble works best alone with headphones on. Don\'t disturb before coffee!'
      },
      {
        id: 2,
        emoji: '๐Ÿ‘ป',
        name: 'Boo',
        role: 'UX Designer',
        traits: ['Creative', 'Collaborative', 'Empathetic'],
        description: 'Boo loves brainstorming sessions and brings donuts every Friday.'
      },
      {
        id: 3,
        emoji: '๐Ÿง›',
        name: 'Count',
        role: 'Project Manager',
        traits: ['Organized', 'Data-driven', 'Deadline-focused'],
        description: 'Count tracks everything in spreadsheets and loves a good Gantt chart.'
      },
      {
        id: 4,
        emoji: '๐ŸงŸ',
        name: 'Shuffle',
        role: 'QA Tester',
        traits: ['Thorough', 'Patient', 'Bug hunter'],
        description: 'Shuffle finds bugs others miss. Moves slowly but never misses a detail.'
      }
    ];
    
    let selectedMonster = null;
    
    // Screen reader announcement helper
    function announceToScreenReader(message) {
      const announcer = document.getElementById('sr-announcements');
      announcer.textContent = message;
    }
    
    // Load saved selection
    function loadSavedData() {
      const saved = localStorage.getItem('monstersWork_selectedMonster');
      if (saved) {
        const data = JSON.parse(saved);
        selectedMonster = data.monsterId;
        console.log('Loaded saved monster selection:', data);
        announceToScreenReader(`Previously selected monster: ${data.monsterName}, loaded`);
      }
    }
    
    // Save selection
    function saveMonsterSelection(monster) {
      const data = {
        monsterId: monster.id,
        monsterName: monster.name,
        monsterEmoji: monster.emoji,
        monsterRole: monster.role,
        savedAt: new Date().toISOString()
      };
      
      localStorage.setItem('monstersWork_selectedMonster', JSON.stringify(data));
      console.log('Saved monster selection:', data);
      
      announceToScreenReader(`Selected ${monster.name}, ${monster.role}. Your choice has been saved.`);
    }
    
    // Handle monster selection
    function selectMonster(monster) {
      selectedMonster = monster.id;
      saveMonsterSelection(monster);
      updateUI();
      showCompletion();
      
      // Call Rise API to unlock next block
      if (typeof lmsAPI !== 'undefined' && lmsAPI.SetReachedEnd) {
        lmsAPI.SetReachedEnd();
        console.log('lmsAPI.SetReachedEnd() called - Rise block unlocked!');
      }
    }
    
    // Update UI based on selection
    function updateUI() {
      const cards = document.querySelectorAll('.monster-card');
      cards.forEach(card => {
        const cardId = parseInt(card.getAttribute('data-monster-id'));
        if (cardId === selectedMonster) {
          card.classList.add('selected');
          card.setAttribute('aria-pressed', 'true');
          const indicator = card.querySelector('.selection-indicator');
          if (indicator) indicator.textContent = 'โœ“';
        } else {
          card.classList.remove('selected');
          card.setAttribute('aria-pressed', 'false');
          const indicator = card.querySelector('.selection-indicator');
          if (indicator) indicator.textContent = '';
        }
      });
      
      const progressText = document.getElementById('progress-text');
      if (selectedMonster) {
        const monster = monsters.find(m => m.id === selectedMonster);
        progressText.innerHTML = `
          <span class="sr-only">1 monster selected. </span>
          โœ“ You selected <strong>${monster.name}</strong> - ${monster.role}
        `;
      }
    }
    
    // Show completion
    function showCompletion() {
      const completionArea = document.getElementById('completionArea');
      const monster = monsters.find(m => m.id === selectedMonster);
      
      completionArea.innerHTML = `
        <div class="completion-badge" role="status">
          <span class="sr-only">Complete: </span>
          ๐ŸŽ‰ Great choice! ${monster.name} is ready to work with you!
        </div>
        <p class="text-muted">Continue to the next section to learn how to work with your monster team.</p>
      `;
    }
    
    // Render monster cards
    function renderMonsters() {
      const grid = document.getElementById('monsterGrid');
      
      monsters.forEach(monster => {
        const col = document.createElement('div');
        col.className = 'col-md-6 col-lg-3 monster-card-col';
        
        const isSelected = monster.id === selectedMonster;
        
        col.innerHTML = `
          <div 
            class="monster-card ${isSelected ? 'selected' : ''}" 
            data-monster-id="${monster.id}"
            role="button"
            tabindex="0"
            aria-pressed="${isSelected}"
            aria-label="Select ${monster.name}, ${monster.role}">
            <span class="selection-indicator" aria-hidden="true">${isSelected ? 'โœ“' : ''}</span>
            <span class="monster-emoji" role="img" aria-label="${monster.name} emoji">${monster.emoji}</span>
            <div class="monster-name">${monster.name}</div>
            <div class="monster-role">${monster.role}</div>
            <div class="mb-2">
              ${monster.traits.map(trait => `<span class="monster-trait">${trait}</span>`).join('')}
            </div>
            <p class="text-muted small">${monster.description}</p>
          </div>
        `;
        
        const card = col.querySelector('.monster-card');
        
        // Click handler
        card.addEventListener('click', () => selectMonster(monster));
        
        // Keyboard handler
        card.addEventListener('keydown', (e) => {
          if (e.key === 'Enter' || e.key === ' ') {
            e.preventDefault();
            selectMonster(monster);
          }
        });
        
        grid.appendChild(col);
      });
    }
    
    // Initialize
    window.addEventListener('DOMContentLoaded', function() {
      loadSavedData();
      renderMonsters();
      updateUI();
      
      if (selectedMonster) {
        showCompletion();
      }
      
      // Respect prefers-reduced-motion
      const prefersReducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
      
      if (!prefersReducedMotion) {
        gsap.from('.monster-card', {
          opacity: 0,
          y: 30,
          duration: 0.5,
          stagger: 0.1,
          ease: 'power2.out',
          clearProps: 'all'
        });
      }
    });
  </script>
</body>
</html>

Block 2: Scenario Journal (Complete Code)

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Monster Workplace Scenarios</title>
  <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
  <script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/3.12.5/gsap.min.js"></script>

  <style>
    .sr-only {
      position: absolute;
      width: 1px;
      height: 1px;
      padding: 0;
      margin: -1px;
      overflow: hidden;
      clip: rect(0, 0, 0, 0);
      white-space: nowrap;
      border-width: 0;
    }
    
    *:focus-visible {
      outline: 3px solid #0066cc;
      outline-offset: 2px;
    }
    
    .accordion-button {
      font-size: 1.1rem;
      font-weight: 600;
      padding: 1.25rem;
      background: linear-gradient(135deg, #f3e7ff 0%, #ffffff 100%);
    }
    
    .accordion-button:not(.collapsed) {
      background: linear-gradient(135deg, #7c3aed 0%, #a855f7 100%);
      color: white;
    }
    
    .accordion-button.locked {
      background-color: #e5e7eb;
      color: #9ca3af;
      cursor: not-allowed;
      opacity: 0.6;
    }
    
    .accordion-body {
      background: #fafafa;
      padding: 1.5rem;
    }
    
    .scenario-emoji {
      font-size: 2.5rem;
      display: block;
      text-align: center;
      margin-bottom: 1rem;
    }
    
    .tip-box {
      background: #dbeafe;
      border-left: 4px solid #3b82f6;
      padding: 1rem;
      margin: 1rem 0;
      border-radius: 5px;
    }
    
    .progress-bar-container {
      background: #e5e7eb;
      border-radius: 10px;
      height: 30px;
      margin: 20px 0;
      overflow: hidden;
      position: relative;
    }
    
    .progress-bar-fill {
      background: linear-gradient(90deg, #7c3aed 0%, #a855f7 100%);
      height: 100%;
      transition: width 0.5s ease;
      display: flex;
      align-items: center;
      justify-content: center;
      color: white;
      font-weight: bold;
    }
    
    .completion-badge {
      background: #10b981;
      color: white;
      padding: 15px 30px;
      border-radius: 30px;
      display: inline-block;
      margin: 20px 0;
      font-weight: bold;
      font-size: 1.1rem;
    }
    
    .journal-section {
      background: #fff7ed;
      border: 2px solid #fb923c;
      border-radius: 10px;
      padding: 20px;
      margin: 20px 0;
    }
    
    .journal-entry {
      background: white;
      border: 1px solid #fed7aa;
      border-radius: 5px;
      padding: 15px;
      margin: 10px 0;
    }
    
    .accordion-item {
      margin-bottom: 10px;
    }
    
    @media (prefers-reduced-motion: reduce) {
      * {
        animation-duration: 0.01ms !important;
        transition-duration: 0.01ms !important;
      }
    }
  </style>
</head>
<body>
  <a href="#main-content" class="sr-only">Skip to main content</a>
  <div id="sr-announcements" role="status" aria-live="polite" aria-atomic="true" class="sr-only"></div>

  <main id="main-content" tabindex="-1">
    <div class="container py-4">
      <h1 class="text-center mb-4" style="color:#7c3aed;">๐Ÿข Monster Workplace Scenarios</h1>

      <div class="alert alert-info" role="status">
        <strong>Instructions:</strong> View each scenario in order to unlock the next one. 
        <span class="sr-only">You must complete each scenario before moving to the next.</span>
      </div>

      <div role="status" aria-live="polite">
        <div class="progress-bar-container" role="progressbar" aria-valuenow="0" aria-valuemin="0" aria-valuemax="5" aria-label="Scenario completion progress">
          <div class="progress-bar-fill" id="progressBar" style="width:0%">
            <span id="progressText">0 / 5</span>
            <span class="sr-only" id="progressSR">0 of 5 scenarios completed</span>
          </div>
        </div>
      </div>

      <div class="accordion" id="scenarioAccordion">
        <!-- Scenario 1 -->
        <div class="accordion-item">
          <h2 class="accordion-header">
            <button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#scenario1" data-scenario="1" aria-expanded="false" aria-controls="scenario1">๐Ÿ“… Scenario 1: The Monday Morning Meeting</button>
          </h2>
          <div id="scenario1" class="accordion-collapse collapse" data-bs-parent="#scenarioAccordion">
            <div class="accordion-body">
              <span class="scenario-emoji">๐Ÿ“…</span>
              <h3>The Monday Morning Meeting</h3>
              <p>It's 9 AM Monday. Grumble hasn't had coffee yet, Boo is bubbling with weekend stories, and Count wants to dive straight into the sprint planning.</p>
              <div class="tip-box"><strong>๐Ÿ’ก Pro Tip:</strong> Let Grumble get coffee first, give Boo 5 minutes to share, then Count can run the structured agenda. Everyone's needs are met!</div>
              <div class="journal-section">
                <label for="journal1" class="form-label"><strong>๐Ÿ“ Your Reflection (Optional):</strong></label>
                <textarea id="journal1" class="form-control" rows="3" placeholder="How would you handle this Monday morning meeting?"></textarea>
                <small class="text-muted">Your reflection is automatically saved as you type</small>
              </div>
            </div>
          </div>
        </div>

        <!-- Scenario 2 -->
        <div class="accordion-item">
          <h2 class="accordion-header">
            <button class="accordion-button collapsed locked" type="button" disabled data-scenario="2">๐Ÿ› Scenario 2: The Urgent Bug ๐Ÿ”’</button>
          </h2>
          <div id="scenario2" class="accordion-collapse collapse" data-bs-parent="#scenarioAccordion">
            <div class="accordion-body">
              <span class="scenario-emoji">๐Ÿ›</span>
              <h3>The Urgent Bug</h3>
              <p>A critical bug was found in production! Shuffle wants to test thoroughly, Grumble wants to fix it fast, and Count is tracking the impact on the timeline.</p>
              <div class="tip-box"><strong>๐Ÿ’ก Pro Tip:</strong> Grumble fixes, Shuffle tests, Count communicates. Teamwork makes the dream work!</div>
              <div class="journal-section">
                <label for="journal2" class="form-label"><strong>๐Ÿ“ Your Reflection (Optional):</strong></label>
                <textarea id="journal2" class="form-control" rows="3" placeholder="In a critical bug situation, how would you coordinate the team?"></textarea>
                <small class="text-muted">Your reflection is automatically saved as you type</small>
              </div>
            </div>
          </div>
        </div>

        <!-- Scenario 3 -->
        <div class="accordion-item">
          <h2 class="accordion-header">
            <button class="accordion-button collapsed locked" type="button" disabled data-scenario="3">๐ŸŽจ Scenario 3: The Design Review ๐Ÿ”’</button>
          </h2>
          <div id="scenario3" class="accordion-collapse collapse" data-bs-parent="#scenarioAccordion">
            <div class="accordion-body">
              <span class="scenario-emoji">๐ŸŽจ</span>
              <h3>The Design Review</h3>
              <p>Boo presents a new design. Grumble thinks it's too complex, Shuffle worries about testing, Count wants the timeline.</p>
              <div class="tip-box"><strong>๐Ÿ’ก Pro Tip:</strong> Boo explains, Grumble simplifies, everyone finds middle ground.</div>
              <div class="journal-section">
                <label for="journal3" class="form-label"><strong>๐Ÿ“ Your Reflection (Optional):</strong></label>
                <textarea id="journal3" class="form-control" rows="3" placeholder="How do you facilitate compromise?"></textarea>
                <small class="text-muted">Your reflection is automatically saved as you type</small>
              </div>
            </div>
          </div>
        </div>

        <!-- Scenario 4 -->
        <div class="accordion-item">
          <h2 class="accordion-header">
            <button class="accordion-button collapsed locked" type="button" disabled data-scenario="4">๐Ÿ”„ Scenario 4: The Friday Retrospective ๐Ÿ”’</button>
          </h2>
          <div id="scenario4" class="accordion-collapse collapse" data-bs-parent="#scenarioAccordion">
            <div class="accordion-body">
              <span class="scenario-emoji">๐Ÿ”„</span>
              <h3>The Friday Retrospective</h3>
              <p>Time to reflect! Each monster has different views on what went well.</p>
              <div class="tip-box"><strong>๐Ÿ’ก Pro Tip:</strong> Encourage all voices equally โ€” critiques and praise.</div>
              <div class="journal-section">
                <label for="journal4" class="form-label"><strong>๐Ÿ“ Your Reflection (Optional):</strong></label>
                <textarea id="journal4" class="form-control" rows="3" placeholder="How do you create psychological safety?"></textarea>
                <small class="text-muted">Your reflection is automatically saved as you type</small>
              </div>
            </div>
          </div>
        </div>

        <!-- Scenario 5 -->
        <div class="accordion-item">
          <h2 class="accordion-header">
            <button class="accordion-button collapsed locked" type="button" disabled data-scenario="5">๐ŸŽ‰ Scenario 5: The Team Celebration ๐Ÿ”’</button>
          </h2>
          <div id="scenario5" class="accordion-collapse collapse" data-bs-parent="#scenarioAccordion">
            <div class="accordion-body">
              <span class="scenario-emoji">๐ŸŽ‰</span>
              <h3>The Team Celebration</h3>
              <p>You shipped the product! Time to celebrate. How do you make everyone feel appreciated?</p>
              <div class="tip-box"><strong>๐Ÿ’ก Pro Tip:</strong> Mix it up โ€” parties, quiet thanks, recognition in metrics.</div>
              <div class="journal-section">
                <label for="journal5" class="form-label"><strong>๐Ÿ“ Your Reflection (Optional):</strong></label>
                <textarea id="journal5" class="form-control" rows="3" placeholder="How would you ensure everyone feels valued?"></textarea>
                <small class="text-muted">Your reflection is automatically saved as you type</small>
              </div>
            </div>
          </div>
        </div>
      </div>

      <div class="text-center mt-4" id="completionArea"></div>
    </div>
  </main>

  <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>

  <script>
    const viewedScenarios = new Set();
    const totalScenarios = 5;
    let currentUnlocked = 1;
    const journalEntries = {};

    function announceToScreenReader(message) {
      document.getElementById('sr-announcements').textContent = message;
    }

    function loadProgress() {
      const saved = localStorage.getItem('monstersWork_scenarioProgress');
      if (saved) {
        const data = JSON.parse(saved);
        (data.viewedScenarios || []).forEach(s => viewedScenarios.add(parseInt(s)));
        currentUnlocked = data.currentUnlocked || 1;
        for (let i = 1; i <= currentUnlocked; i++) unlockScenario(i, false);
        updateProgress();
        announceToScreenReader(`Progress restored: ${viewedScenarios.size} of ${totalScenarios} scenarios completed`);
      }
      
      const savedJournal = localStorage.getItem('monstersWork_journalEntries');
      if (savedJournal) {
        const entries = JSON.parse(savedJournal);
        Object.assign(journalEntries, entries);
        Object.keys(entries).forEach(key => {
          const textarea = document.getElementById(`journal${key}`);
          if (textarea) textarea.value = entries[key];
        });
      }
    }

    function saveProgress() {
      const data = {
        viewedScenarios: Array.from(viewedScenarios),
        currentUnlocked: currentUnlocked,
        savedAt: new Date().toISOString()
      };
      localStorage.setItem('monstersWork_scenarioProgress', JSON.stringify(data));
    }

    function autoSaveJournal(num, value) {
      const trimmed = value.trim();
      if (trimmed) {
        journalEntries[num] = trimmed;
      } else {
        delete journalEntries[num];
      }
      localStorage.setItem('monstersWork_journalEntries', JSON.stringify(journalEntries));
      announceToScreenReader(`Reflection for scenario ${num} ${trimmed ? 'saved' : 'cleared'}`);
    }

    function updateProgress() {
      const count = viewedScenarios.size;
      const percent = (count / totalScenarios) * 100;
      const pb = document.getElementById('progressBar');
      const pt = document.getElementById('progressText');
      const psr = document.getElementById('progressSR');
      const cont = pb.closest('.progress-bar-container');
      pb.style.width = `${percent}%`;
      pt.textContent = `${count} / ${totalScenarios}`;
      psr.textContent = `${count} of ${totalScenarios} scenarios completed`;
      cont.setAttribute('aria-valuenow', count);
      if (count === totalScenarios) showCompletion();
    }

    function unlockScenario(num, announce = true) {
      const btn = document.querySelector(`[data-scenario="${num}"]`);
      if (btn && btn.disabled) {
        btn.disabled = false;
        btn.classList.remove('locked');
        btn.setAttribute('aria-disabled', 'false');
        btn.setAttribute('data-bs-toggle', 'collapse');
        btn.setAttribute('data-bs-target', `#scenario${num}`);
        btn.textContent = btn.textContent.replace(' ๐Ÿ”’', '');
        if (announce) announceToScreenReader(`Scenario ${num} unlocked`);
      }
    }

    function unlockNext() {
      if (currentUnlocked < totalScenarios) {
        currentUnlocked++;
        unlockScenario(currentUnlocked, true);
        saveProgress();
      }
    }

    function showCompletion() {
      const area = document.getElementById('completionArea');
      area.innerHTML = `
        <div class="completion-badge" role="status">
          ๐ŸŽŠ Congratulations! You've completed all scenarios!
        </div>
        <p class="text-muted">Continue to the next section to see your Monster Journey Summary.</p>
      `;
      announceToScreenReader('All scenarios completed! You may now continue to the summary.');
      
      if (typeof lmsAPI !== 'undefined' && lmsAPI.SetReachedEnd) {
        lmsAPI.SetReachedEnd();
        console.log('lmsAPI.SetReachedEnd() called - Rise block unlocked!');
      }
    }

    document.getElementById('scenarioAccordion').addEventListener('shown.bs.collapse', function(e) {
      const btn = document.querySelector(`[data-bs-target="#${e.target.id}"]`);
      const num = parseInt(btn.getAttribute('data-scenario'));
      if (!viewedScenarios.has(num)) {
        viewedScenarios.add(num);
        updateProgress();
        unlockNext();
        announceToScreenReader(`Scenario ${num} of ${totalScenarios} viewed`);
        saveProgress();
      }
    });

    window.addEventListener('DOMContentLoaded', function() {
      loadProgress();
      
      for (let i = 1; i <= totalScenarios; i++) {
        const textarea = document.getElementById(`journal${i}`);
        if (textarea) {
          textarea.addEventListener('input', function() {
            autoSaveJournal(i, this.value);
          });
        }
      }
      
      const prefersReducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
      if (!prefersReducedMotion) {
        gsap.from('.accordion-item', {
          opacity: 0,
          x: -30,
          duration: 0.4,
          stagger: 0.1,
          ease: 'power2.out'
        });
      }
    });
  </script>
</body>
</html>

Block 3: Journey Summary Dashboard (Complete Code)

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Your Monster Journey Summary</title>
  
  <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
  <script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/3.12.5/gsap.min.js"></script>
  
  <style>
    .sr-only {
      position: absolute;
      width: 1px;
      height: 1px;
      padding: 0;
      margin: -1px;
      overflow: hidden;
      clip: rect(0, 0, 0, 0);
      white-space: nowrap;
      border-width: 0;
    }
    
    *:focus-visible {
      outline: 3px solid #0066cc;
      outline-offset: 2px;
    }
    
    .summary-card {
      background: white;
      border-radius: 15px;
      padding: 30px;
      margin: 20px 0;
      box-shadow: 0 4px 6px rgba(0,0,0,0.1);
    }
    
    .monster-summary {
      background: linear-gradient(135deg, #f3e7ff 0%, #ffffff 100%);
      border: 3px solid #7c3aed;
      border-radius: 15px;
      padding: 25px;
      text-align: center;
      margin: 20px 0;
    }
    
    .monster-emoji-large {
      font-size: 5rem;
      display: block;
      margin-bottom: 15px;
    }
    
    .progress-summary {
      background: #f0fdf4;
      border: 2px solid #10b981;
      border-radius: 10px;
      padding: 20px;
      margin: 20px 0;
    }
    
    .journal-summary {
      background: #fff7ed;
      border: 2px solid #fb923c;
      border-radius: 10px;
      padding: 20px;
      margin: 20px 0;
    }
    
    .journal-entry-display {
      background: white;
      border: 1px solid #fed7aa;
      border-radius: 8px;
      padding: 15px;
      margin: 10px 0;
    }
    
    .completion-badge-large {
      background: linear-gradient(135deg, #10b981 0%, #059669 100%);
      color: white;
      padding: 20px 40px;
      border-radius: 50px;
      display: inline-block;
      margin: 30px 0;
      font-weight: bold;
      font-size: 1.3rem;
      box-shadow: 0 4px 12px rgba(16,185,129,0.3);
    }
    
    .stat-box {
      background: #f3f4f6;
      border-radius: 10px;
      padding: 20px;
      text-align: center;
      margin: 10px 0;
    }
    
    .stat-number {
      font-size: 2.5rem;
      font-weight: bold;
      color: #7c3aed;
    }
    
    .stat-label {
      color: #6b7280;
      font-size: 0.9rem;
      margin-top: 5px;
    }
    
    .export-btn {
      background: #3b82f6;
      color: white;
      border: none;
      padding: 12px 30px;
      border-radius: 25px;
      font-weight: 600;
      cursor: pointer;
      transition: all 0.3s;
    }
    
    .export-btn:hover {
      background: #2563eb;
      transform: translateY(-2px);
      box-shadow: 0 4px 12px rgba(59,130,246,0.3);
    }
    
    .no-data-message {
      background: #fef3c7;
      border: 2px solid #f59e0b;
      border-radius: 10px;
      padding: 20px;
      margin: 20px 0;
      text-align: center;
    }
    
    .auto-update-indicator {
      background: #dbeafe;
      padding: 10px 20px;
      border-radius: 20px;
      display: inline-block;
      font-size: 0.9rem;
      margin: 10px 0;
    }
    
    @media (prefers-reduced-motion: reduce) {
      * {
        animation-duration: 0.01ms !important;
        transition-duration: 0.01ms !important;
      }
    }
  </style>
</head>
<body>
  <a href="#main-content" class="sr-only">Skip to main content</a>
  <div id="sr-announcements" role="status" aria-live="polite" aria-atomic="true" class="sr-only"></div>

  <main id="main-content" tabindex="-1">
    <div class="container py-4">
      <h1 class="text-center mb-4" style="color:#7c3aed;">๐Ÿ“Š Your Monster Journey Summary</h1>
      <div class="text-center">
        <span class="auto-update-indicator" role="status" aria-live="polite">
          ๐Ÿ”„ Auto-updating every 2 seconds
        </span>
      </div>

      <div id="monsterSection"></div>
      <div id="progressSection"></div>
      <div id="journalSection"></div>

      <div class="summary-card">
        <h2 class="mb-4">๐Ÿ“ˆ Your Learning Stats</h2>
        <div class="row" id="statsSection"></div>
      </div>

      <div class="summary-card text-center">
        <h2 class="mb-4">๐Ÿ’พ Export Your Journey</h2>
        <p class="text-muted mb-3">Download your monster journey data and reflections as a text file</p>
        <button onclick="exportJourney()" class="export-btn" aria-label="Export your journey data as text file">๐Ÿ“ฅ Download My Journey (.txt)</button>
        <button onclick="clearAllData()" class="btn btn-outline-danger ms-2" aria-label="Clear all saved data">๐Ÿ—‘๏ธ Clear All Data</button>
      </div>

      <div class="text-center" id="completionBadge"></div>
    </div>
  </main>

  <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>

  <script>
    let lastCheck = { monster: null, progress: null, journal: null };

    function announceToScreenReader(message) {
      document.getElementById('sr-announcements').textContent = message;
    }

    function loadAllData() {
      loadMonsterSummary();
      loadProgressSummary();
      loadJournalSummary();
      updateStats();
    }

    function loadMonsterSummary() {
      const saved = localStorage.getItem('monstersWork_selectedMonster');
      const section = document.getElementById('monsterSection');
      
      if (saved) {
        const data = JSON.parse(saved);
        section.innerHTML = `
          <div class="monster-summary" role="region" aria-label="Selected monster summary">
            <span class="monster-emoji-large" role="img" aria-label="${data.monsterName} emoji">${data.monsterEmoji}</span>
            <h2 style="color:#7c3aed;">${data.monsterName}</h2>
            <p class="lead">${data.monsterRole}</p>
            <p class="text-muted">Selected on ${new Date(data.savedAt).toLocaleDateString()}</p>
          </div>
        `;
      } else {
        section.innerHTML = `
          <div class="no-data-message" role="status">
            <p><strong>โš ๏ธ No Monster Selected</strong></p>
            <p>Go back to Block 1 to select your monster teammate!</p>
          </div>
        `;
      }
    }

    function loadProgressSummary() {
      const saved = localStorage.getItem('monstersWork_scenarioProgress');
      const section = document.getElementById('progressSection');

      if (saved) {
        const data = JSON.parse(saved);
        data.viewedScenarios = (data.viewedScenarios || [])
          .map(s => parseInt(s))
          .filter((v, i, a) => a.indexOf(v) === i && !isNaN(v));

        const completed = data.viewedScenarios.length;
        const percent = (completed / 5) * 100;
        const isComplete = completed === 5;

        const scenarioList = data.viewedScenarios
          .sort((a, b) => a - b)
          .join(', ');

        section.innerHTML = `
          <div class="progress-summary" role="region" aria-label="Progress summary">
            <h2 class="mb-3">โœ… Scenario Progress</h2>
            <div role="progressbar" aria-valuenow="${completed}" aria-valuemin="0" aria-valuemax="5" 
                 aria-label="Scenarios completed"
                 style="background:#e5e7eb;border-radius:10px;height:40px;overflow:hidden;">
              <div style="background:linear-gradient(90deg,#10b981 0%,#059669 100%);height:100%;width:${percent}%;
                          display:flex;align-items:center;justify-content:center;color:white;font-weight:bold;
                          transition:width .5s;">
                ${completed} / 5 Scenarios
              </div>
            </div>
            <p class="mt-3 text-muted">${isComplete ? '๐ŸŽ‰ All scenarios complete!' : `${5 - completed} scenarios remaining`}</p>
            <p class="small text-muted">Viewed scenarios: ${scenarioList || 'None yet'}</p>
            <p class="small text-muted">Last updated: ${new Date(data.savedAt).toLocaleString()}</p>
          </div>
        `;
      } else {
        section.innerHTML = `
          <div class="no-data-message" role="status">
            <p><strong>โš ๏ธ No Progress Yet</strong></p>
            <p>Go to Block 2 to start viewing scenarios!</p>
          </div>
        `;
      }
    }

    function loadJournalSummary() {
      const saved = localStorage.getItem('monstersWork_journalEntries');
      const section = document.getElementById('journalSection');
      
      if (saved) {
        const entries = JSON.parse(saved);
        const entryCount = Object.keys(entries).length;
        
        if (entryCount > 0) {
          let entriesHTML = '';
          
          Object.keys(entries)
            .sort((a, b) => parseInt(a) - parseInt(b))
            .forEach(key => {
              entriesHTML += `
                <div class="journal-entry-display">
                  <h4>๐Ÿ“ Scenario ${key} Reflection</h4>
                  <p>${entries[key]}</p>
                </div>
              `;
            });
          
          section.innerHTML = `
            <div class="journal-summary" role="region" aria-label="Journal reflections summary">
              <h2 class="mb-3">๐Ÿ“” Your Reflections</h2>
              <p class="text-muted mb-3">You've written ${entryCount} reflection${entryCount !== 1 ? 's' : ''}</p>
              ${entriesHTML}
            </div>
          `;
        } else {
          section.innerHTML = `
            <div class="no-data-message" role="status">
              <p><strong>โš ๏ธ No Reflections Yet</strong></p>
              <p>Write reflections in Block 2 to see them here!</p>
            </div>
          `;
        }
      } else {
        section.innerHTML = `
          <div class="no-data-message" role="status">
            <p><strong>โš ๏ธ No Reflections Yet</strong></p>
            <p>Write reflections in Block 2 to see them here!</p>
          </div>
        `;
      }
    }

    function updateStats() {
      const monster = localStorage.getItem('monstersWork_selectedMonster');
      const progress = localStorage.getItem('monstersWork_scenarioProgress');
      const journal = localStorage.getItem('monstersWork_journalEntries');
      
      const hasMonster = monster ? 1 : 0;
      const scenariosViewed = progress ? JSON.parse(progress).viewedScenarios.length : 0;
      const reflectionsWritten = journal ? Object.keys(JSON.parse(journal)).length : 0;
      
      const totalRequired = 6;
      const totalCompleted = hasMonster + scenariosViewed;
      const completionPercent = Math.round((totalCompleted / totalRequired) * 100);
      
      const statsSection = document.getElementById('statsSection');
      statsSection.innerHTML = `
        <div class="col-md-3">
          <div class="stat-box">
            <div class="stat-number">${hasMonster}</div>
            <div class="stat-label">Monster Selected</div>
          </div>
        </div>
        <div class="col-md-3">
          <div class="stat-box">
            <div class="stat-number">${scenariosViewed}</div>
            <div class="stat-label">Scenarios Viewed</div>
          </div>
        </div>
        <div class="col-md-3">
          <div class="stat-box">
            <div class="stat-number">${reflectionsWritten}</div>
            <div class="stat-label">Reflections Written<br><small>(Optional)</small></div>
          </div>
        </div>
        <div class="col-md-3">
          <div class="stat-box">
            <div class="stat-number">${completionPercent}%</div>
            <div class="stat-label">Journey Complete<br><small>(1 Monster + 5 Scenarios)</small></div>
          </div>
        </div>
      `;
      
      if (hasMonster && scenariosViewed === 5) {
        const badge = document.getElementById('completionBadge');
        badge.innerHTML = `
          <div class="completion-badge-large" role="status">
            ๐Ÿ† Congratulations! Monster Journey Complete!
          </div>
          <p class="lead">You've mastered working with your monster team!</p>
        `;
        
        if (typeof lmsAPI !== 'undefined' && lmsAPI.SetReachedEnd) {
          lmsAPI.SetReachedEnd();
          console.log('lmsAPI.SetReachedEnd() called - Rise block unlocked!');
        }
      }
    }

    function exportJourney() {
      const monster = JSON.parse(localStorage.getItem('monstersWork_selectedMonster') || 'null');
      const progress = JSON.parse(localStorage.getItem('monstersWork_scenarioProgress') || 'null');
      const journal = JSON.parse(localStorage.getItem('monstersWork_journalEntries') || 'null');
      
      let textContent = '๐ŸŽญ MY MONSTER JOURNEY\n' + '='.repeat(50) + '\n\n';
      
      if (monster) {
        textContent += `SELECTED MONSTER: ${monster.monsterEmoji} ${monster.monsterName}\n`;
        textContent += `Role: ${monster.monsterRole}\n`;
        textContent += `Selected on: ${new Date(monster.savedAt).toLocaleDateString()}\n\n`;
      }
      
      if (progress) {
        textContent += `SCENARIOS COMPLETED: ${progress.viewedScenarios.length} out of 5\n`;
        textContent += `Scenarios viewed: ${progress.viewedScenarios.join(', ')}\n`;
        textContent += `Last updated: ${new Date(progress.savedAt).toLocaleString()}\n\n`;
      }
      
      if (journal && Object.keys(journal).length > 0) {
        textContent += 'MY REFLECTIONS\n' + '-'.repeat(50) + '\n\n';
        
        Object.keys(journal)
          .sort((a, b) => a - b)
          .forEach(key => {
            textContent += `๐Ÿ“ Scenario ${key} Reflection:\n`;
            textContent += `${journal[key]}\n\n`;
          });
      }
      
      textContent += '='.repeat(50) + '\n';
      textContent += `Exported on: ${new Date().toLocaleString()}\n`;
      
      try {
        const blob = new Blob([textContent], { type: 'text/plain' });
        const url = URL.createObjectURL(blob);
        const a = document.createElement('a');
        a.href = url;
        a.download = `My-Monster-Journey-${Date.now()}.txt`;
        a.style.display = 'none';
        document.body.appendChild(a);
        a.click();
        document.body.removeChild(a);
        URL.revokeObjectURL(url);
        
        announceToScreenReader('Journey data exported successfully as text file');
        
        const btn = window.event.target;
        const originalText = btn.innerHTML;
        btn.innerHTML = 'โœ“ Downloaded!';
        btn.style.background = '#10b981';
        setTimeout(() => {
          btn.innerHTML = originalText;
          btn.style.background = '#3b82f6';
        }, 2000);
      } catch (e) {
        alert('Download not available in preview mode. Here is your journey data:\n\n' + textContent);
        announceToScreenReader('Journey data displayed in alert');
      }
    }

    function clearAllData() {
      if (confirm('โš ๏ธ Clear all saved data? This cannot be undone.')) {
        try {
          localStorage.removeItem('monstersWork_selectedMonster');
          localStorage.removeItem('monstersWork_scenarioProgress');
          localStorage.removeItem('monstersWork_journalEntries');
          announceToScreenReader('All data cleared');
          loadAllData();
          alert('โœ“ All data has been cleared!');
        } catch (e) {
          alert('Unable to clear data in preview mode.');
        }
      }
    }

    function checkForUpdates() {
      const current = {
        monster: localStorage.getItem('monstersWork_selectedMonster'),
        progress: localStorage.getItem('monstersWork_scenarioProgress'),
        journal: localStorage.getItem('monstersWork_journalEntries')
      };
      
      if (current.monster !== lastCheck.monster || 
          current.progress !== lastCheck.progress || 
          current.journal !== lastCheck.journal) {
        loadAllData();
        announceToScreenReader('Summary updated with latest data');
        lastCheck = current;
      }
    }

    window.addEventListener('DOMContentLoaded', function() {
      loadAllData();
      
      lastCheck = {
        monster: localStorage.getItem('monstersWork_selectedMonster'),
        progress: localStorage.getItem('monstersWork_scenarioProgress'),
        journal: localStorage.getItem('monstersWork_journalEntries')
      };
      
      setInterval(checkForUpdates, 2000);
      
      window.addEventListener('storage', function(e) {
        if (e.key && e.key.startsWith('monstersWork_')) {
          loadAllData();
          announceToScreenReader('Summary updated from another tab');
        }
      });
      
      const prefersReducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
      if (!prefersReducedMotion) {
        gsap.from('.summary-card, .monster-summary, .progress-summary, .journal-summary', {
          opacity: 0,
          y: 30,
          duration: 0.6,
          stagger: 0.15,
          ease: 'power2.out'
        });
      }
    });
  </script>
</body>
</html>

Adding the Journal to Rise 360

Follow these steps to implement the learning journal in your Rise 360 course.

Create three separate lessons in your Rise course for the three blocks. In each lesson, add a Multimedia block and select the code embed option. Paste the complete code for each block into its respective embed area.

Set the multimedia block height to at least 600 pixels for Block 1, 800 pixels for Block 2 due to the accordion content, and 1000 pixels for Block 3 to accommodate the full dashboard. Adjust these values based on how your content renders.

For the sequential unlock to work properly within Block 2, learners must actually open each accordion panel. The Rise lesson navigation remains separate from the in-block progression, so consider using Rise's lesson locking features in combination with the in-block unlocking for complete content control.

Extending the Learning Journal Pattern

This foundation supports many variations beyond workplace scenarios.

For coaching modules, replace the workplace scenarios with leadership challenges and expand the journal prompts to include action commitments. For compliance training, use the sequential unlock to ensure learners read all policy sections before certifying completion. For onboarding, customize the character selection to represent different departments or mentors.

The export feature could generate PDF reports instead of text files using libraries like jsPDF. The statistics dashboard could track time spent on each scenario using additional localStorage timestamps. The journal entries could include image uploads by converting images to base64 strings.

Wrapping Up

Building a Rise 360 learning journal with localStorage creates persistent, personalized experiences that standard Rise blocks can't achieve. Sequential unlocking ensures learners engage with content in order, auto-saving journals eliminate friction, and export features give learners tangible takeaways.

The key patterns to remember include using Sets for tracking unique completions, separating different data types into their own localStorage keys, and providing accessible feedback through ARIA live regions.

Download the complete code files and adapt the monster theme to your content domain. The underlying localStorage patterns work for any subject matter. If you have questions about implementing these patterns, drop a comment below.