Merge pull request 'style-fixes' (#26) from mstoeck3/forensic-pathways:style-fixes into main
Reviewed-on: mstoeck3/cc24-hub#26
This commit is contained in:
		
						commit
						e94c3a0f47
					
				@ -88,6 +88,8 @@ npm install
 | 
			
		||||
cp .env.example .env
 | 
			
		||||
# .env bearbeiten (siehe Konfiguration unten)
 | 
			
		||||
 | 
			
		||||
npm run astro build
 | 
			
		||||
 | 
			
		||||
# Development Server starten
 | 
			
		||||
npm run dev
 | 
			
		||||
```
 | 
			
		||||
@ -125,6 +127,7 @@ sudo npm install
 | 
			
		||||
 | 
			
		||||
# Production-Build erstellen
 | 
			
		||||
sudo npm run build
 | 
			
		||||
npm run astro build
 | 
			
		||||
 | 
			
		||||
# Berechtigungen setzen
 | 
			
		||||
sudo chown -R www-data:www-data /opt/forensic-pathways
 | 
			
		||||
 | 
			
		||||
@ -8,9 +8,9 @@ const phases = data.phases;
 | 
			
		||||
const domainAgnosticSoftware = data['domain-agnostic-software'] || [];
 | 
			
		||||
---
 | 
			
		||||
 | 
			
		||||
<section id="ai-interface" class="ai-interface" style="display: none;">
 | 
			
		||||
<section id="ai-interface" class="ai-interface hidden">
 | 
			
		||||
  <div class="ai-query-section">
 | 
			
		||||
    <div style="text-align: center; margin-bottom: 2rem;">
 | 
			
		||||
    <div class="content-center-lg">
 | 
			
		||||
      <h2 style="margin-bottom: 1rem; color: var(--color-primary);">
 | 
			
		||||
        <svg width="28" height="28" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="margin-right: 0.75rem; vertical-align: middle;">
 | 
			
		||||
          <path d="M9 11H5a2 2 0 0 0-2 2v7a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7a2 2 0 0 0-2-2h-4"/>
 | 
			
		||||
@ -27,7 +27,7 @@ const domainAgnosticSoftware = data['domain-agnostic-software'] || [];
 | 
			
		||||
  <div class="ai-input-container" style="max-width: 1000px; margin: 0 auto;">
 | 
			
		||||
    <div class="ai-mode-toggle" style="display: flex; align-items: center; justify-content: center; gap: 1rem; margin-bottom: 1.5rem; padding: 1rem; background-color: var(--color-bg-secondary); border-radius: 0.75rem; border: 1px solid var(--color-border);">
 | 
			
		||||
      <span id="workflow-label" class="toggle-label active" style="font-weight: 500; color: var(--color-primary); cursor: pointer; transition: var(--transition-fast);">
 | 
			
		||||
        <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="margin-right: 0.5rem; vertical-align: middle;">
 | 
			
		||||
        <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" class="mr-2 align-middle">
 | 
			
		||||
          <polyline points="9,11 12,14 22,4"/>
 | 
			
		||||
          <path d="M21 12v7a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11"/>
 | 
			
		||||
        </svg>
 | 
			
		||||
@ -39,7 +39,7 @@ const domainAgnosticSoftware = data['domain-agnostic-software'] || [];
 | 
			
		||||
      </div>
 | 
			
		||||
      
 | 
			
		||||
      <span id="tool-label" class="toggle-label" style="font-weight: 500; color: var(--color-text-secondary); cursor: pointer; transition: var(--transition-fast);">
 | 
			
		||||
        <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="margin-right: 0.5rem; vertical-align: middle;">
 | 
			
		||||
        <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" class="mr-2 align-middle">
 | 
			
		||||
          <path d="M14.7 6.3a1 1 0 0 0 0 1.4l1.6 1.6a1 1 0 0 0 1.4 0l3.77-3.77a6 6 0 0 1-7.94 7.94l-6.91 6.91a2.12 2.12 0 0 1-3-3l6.91-6.91a6 6 0 0 1 7.94-7.94l-3.76 3.76z"/>
 | 
			
		||||
        </svg>
 | 
			
		||||
        Spezifische Software oder Methode
 | 
			
		||||
@ -78,19 +78,19 @@ const domainAgnosticSoftware = data['domain-agnostic-software'] || [];
 | 
			
		||||
            </div>
 | 
			
		||||
          </div>
 | 
			
		||||
        </div>
 | 
			
		||||
        <div id="smart-prompting-container" class="smart-prompting-container" style="display: none;">
 | 
			
		||||
        <div id="smart-prompting-container" class="smart-prompting-container hidden">
 | 
			
		||||
          <div class="prompting-card">
 | 
			
		||||
            <div id="prompting-status" class="prompting-status">
 | 
			
		||||
              <div class="status-icon">💡</div>
 | 
			
		||||
              <span class="status-text">Analysiere Eingabe...</span>
 | 
			
		||||
              <div id="prompting-spinner" class="prompting-spinner" style="display: none;">
 | 
			
		||||
              <div id="prompting-spinner" class="prompting-spinner hidden">
 | 
			
		||||
                <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="var(--color-accent)" stroke-width="2">
 | 
			
		||||
                  <path d="M21 12a9 9 0 11-6.219-8.56"/>
 | 
			
		||||
                </svg>
 | 
			
		||||
              </div>
 | 
			
		||||
            </div>
 | 
			
		||||
            
 | 
			
		||||
            <div id="suggested-questions" class="suggested-questions" style="display: none;">
 | 
			
		||||
            <div id="suggested-questions" class="suggested-questions hidden">
 | 
			
		||||
              <div class="suggestions-header">
 | 
			
		||||
                <span class="suggestions-label">Zur besseren Analyse:</span>
 | 
			
		||||
              </div>
 | 
			
		||||
@ -139,9 +139,9 @@ const domainAgnosticSoftware = data['domain-agnostic-software'] || [];
 | 
			
		||||
        <path d="M12 17h.01"/>
 | 
			
		||||
      </svg>
 | 
			
		||||
    </div>
 | 
			
		||||
    <p id="loading-text" style="color: var(--color-text-secondary);">Analysiere Szenario und generiere Empfehlungen...</p>
 | 
			
		||||
    <p id="loading-text" class="text-secondary">Analysiere Szenario und generiere Empfehlungen...</p>
 | 
			
		||||
    
 | 
			
		||||
    <div id="queue-status" class="queue-status-card" style="display: none;">
 | 
			
		||||
    <div id="queue-status" class="queue-status-card hidden">
 | 
			
		||||
      <div class="queue-header">
 | 
			
		||||
        <div class="queue-position-display">
 | 
			
		||||
          <div id="queue-position-badge" class="position-badge">1</div>
 | 
			
		||||
@ -180,7 +180,7 @@ const domainAgnosticSoftware = data['domain-agnostic-software'] || [];
 | 
			
		||||
    </div>
 | 
			
		||||
  </div>
 | 
			
		||||
<br>
 | 
			
		||||
  <div id="ai-results" class="ai-results" style="display: none;">
 | 
			
		||||
  <div id="ai-results" class="ai-results hidden">
 | 
			
		||||
  </div>
 | 
			
		||||
</section>
 | 
			
		||||
 | 
			
		||||
@ -812,7 +812,7 @@ document.addEventListener('DOMContentLoaded', () => {
 | 
			
		||||
      <div class="tool-results-container">
 | 
			
		||||
        <div style="text-align: center; margin-bottom: 2rem; padding: 1.5rem; background: linear-gradient(135deg, var(--color-primary) 0%, #525252 100%); color: white; border-radius: 0.75rem;">
 | 
			
		||||
          <h3 style="margin: 0 0 0.75rem 0; font-size: 1.5rem;">
 | 
			
		||||
            <svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="margin-right: 0.5rem; vertical-align: middle;">
 | 
			
		||||
            <svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" class="mr-2 align-middle">
 | 
			
		||||
              <path d="M14.7 6.3a1 1 0 0 0 0 1.4l1.6 1.6a1 1 0 0 0 1.4 0l3.77-3.77a6 6 0 0 1-7.94 7.94l-6.91 6.91a2.12 2.12 0 0 1-3-3l6.91-6.91a6 6 0 0 1 7.94-7.94l-3.76 3.76z"/>
 | 
			
		||||
            </svg>
 | 
			
		||||
            Passende Empfehlungen
 | 
			
		||||
@ -887,7 +887,7 @@ document.addEventListener('DOMContentLoaded', () => {
 | 
			
		||||
                </div>
 | 
			
		||||
                <div class="tool-detailed-explanation" style="margin-bottom: 1.5rem;">
 | 
			
		||||
                  <h4 style="margin: 0.8rem 0 0.75rem 0; color: var(--color-accent); font-size: 1rem;">
 | 
			
		||||
                    <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="margin-right: 0.5rem; vertical-align: middle;">
 | 
			
		||||
                    <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" class="mr-2 align-middle">
 | 
			
		||||
                      <path d="M9 11H5a2 2 0 0 0-2 2v7a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7a2 2 0 0 0-2-2h-4"/>
 | 
			
		||||
                      <path d="M9 11V7a3 3 0 0 1 6 0v4"/>
 | 
			
		||||
                    </svg>
 | 
			
		||||
@ -896,7 +896,7 @@ document.addEventListener('DOMContentLoaded', () => {
 | 
			
		||||
                  ${formatWorkflowSuggestion(toolRec.detailed_explanation)}                  
 | 
			
		||||
                  ${toolRec.implementation_approach ? `
 | 
			
		||||
                    <h4 style="margin: 0.8rem 0 0.75rem 0; color: var(--color-primary); font-size: 1rem;">
 | 
			
		||||
                      <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="margin-right: 0.5rem; vertical-align: middle;">
 | 
			
		||||
                      <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" class="mr-2 align-middle">
 | 
			
		||||
                        <polyline points="9,11 12,14 22,4"/>
 | 
			
		||||
                        <path d="M21 12v7a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11"/>
 | 
			
		||||
                      </svg>
 | 
			
		||||
 | 
			
		||||
@ -65,7 +65,7 @@ const sortedTags = Object.entries(tagFrequency)
 | 
			
		||||
      <label for="include-proprietary">Proprietäre Software mit einschließen</label>
 | 
			
		||||
    </div>
 | 
			
		||||
    
 | 
			
		||||
    <div style="margin-bottom: 1rem;">
 | 
			
		||||
    <div class="mb-4">
 | 
			
		||||
      <div class="tag-header">
 | 
			
		||||
        <label style="font-weight: 500;">
 | 
			
		||||
          Nach Tags filtern
 | 
			
		||||
@ -95,8 +95,8 @@ const sortedTags = Object.entries(tagFrequency)
 | 
			
		||||
  </div>
 | 
			
		||||
  
 | 
			
		||||
  <div style="display: flex; gap: 1rem; margin-bottom: 1.5rem; align-items: center;">
 | 
			
		||||
    <button class="btn btn-secondary view-toggle active" style="height:50px" data-view="grid">Kachelansicht</button>
 | 
			
		||||
    <button class="btn btn-secondary view-toggle" style="height:50px" data-view="matrix">Matrix-Ansicht</button>
 | 
			
		||||
    <button class="btn btn-secondary view-toggle active h-12" data-view="grid">Kachelansicht</button>
 | 
			
		||||
    <button class="btn btn-secondary view-toggle h-12" data-view="matrix">Matrix-Ansicht</button>
 | 
			
		||||
    
 | 
			
		||||
    <button 
 | 
			
		||||
      id="ai-view-toggle" 
 | 
			
		||||
 | 
			
		||||
@ -29,7 +29,7 @@ domains.forEach((domain: any) => {
 | 
			
		||||
});
 | 
			
		||||
---
 | 
			
		||||
 | 
			
		||||
<div id="matrix-container" class="matrix-wrapper" style="display: none;">
 | 
			
		||||
<div id="matrix-container" class="matrix-wrapper hidden">
 | 
			
		||||
  {domainAgnosticTools.map((sectionData: any, index: number) => (
 | 
			
		||||
    <div id={`domain-agnostic-section-${sectionData.section.id}`} class="card collaboration-section-collapsed mb-6 border-l-4" style="border-left-color: var(--color-accent);">
 | 
			
		||||
      <div class="collaboration-header cursor-pointer flex items-center gap-3" onclick={`toggleDomainAgnosticSection('${sectionData.section.id}')`} style="margin-bottom: 0.1rem;">
 | 
			
		||||
@ -39,7 +39,7 @@ domains.forEach((domain: any) => {
 | 
			
		||||
          <line x1="20" y1="8" x2="20" y2="14"/>
 | 
			
		||||
          <line x1="23" y1="11" x2="17" y2="11"/>
 | 
			
		||||
        </svg>
 | 
			
		||||
        <h3 class="m-0 text-lg" style="color: var(--color-accent);">
 | 
			
		||||
        <h3 class="m-0 text-lg text-accent">
 | 
			
		||||
          {sectionData.section.name}
 | 
			
		||||
          <span id={`count-${sectionData.section.id}`} class="badge text-xs" style="background-color: var(--color-text-secondary); color: var(--color-bg); margin-left: 0.5rem;">
 | 
			
		||||
            {sectionData.tools.length}
 | 
			
		||||
@ -51,7 +51,7 @@ domains.forEach((domain: any) => {
 | 
			
		||||
          </svg>
 | 
			
		||||
        </div>
 | 
			
		||||
      </div>
 | 
			
		||||
      <div class="collaboration-content" style="display: none;">
 | 
			
		||||
      <div class="collaboration-content hidden">
 | 
			
		||||
        <div class="collaboration-tools-compact" id={`domain-agnostic-tools-${sectionData.section.id}`}>
 | 
			
		||||
          {sectionData.tools.map((tool: any) => {
 | 
			
		||||
            const hasValidProjectUrl = tool.projectUrl !== undefined &&
 | 
			
		||||
@ -74,7 +74,7 @@ domains.forEach((domain: any) => {
 | 
			
		||||
                <p class="text-muted">              
 | 
			
		||||
                  {tool.description}
 | 
			
		||||
                </p>
 | 
			
		||||
                <div class="flex gap-3 text-xs" style="color: var(--color-text-secondary);">
 | 
			
		||||
                <div class="flex gap-3 text-xs text-secondary">
 | 
			
		||||
                  <span>{tool.platforms.join(', ')}</span>
 | 
			
		||||
                  <span>•</span>
 | 
			
		||||
                  <span>{tool.skillLevel}</span>
 | 
			
		||||
@ -140,9 +140,9 @@ domains.forEach((domain: any) => {
 | 
			
		||||
  <div class="flex justify-between items-start mb-4">
 | 
			
		||||
    <h2 id="tool-name-primary" class="m-0">Tool Name</h2>
 | 
			
		||||
    <div class="flex items-center gap-2">
 | 
			
		||||
      <div id="share-button-primary" style="display: none;">
 | 
			
		||||
      <div id="share-button-primary" class="hidden">
 | 
			
		||||
      </div>
 | 
			
		||||
      <div id="contribute-button-primary" style="display: none;">
 | 
			
		||||
      <div id="contribute-button-primary" class="hidden">
 | 
			
		||||
      </div>
 | 
			
		||||
      <button class="btn-icon" onclick="window.hideToolDetails('primary')">
 | 
			
		||||
        <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
 | 
			
		||||
@ -168,9 +168,9 @@ domains.forEach((domain: any) => {
 | 
			
		||||
  <div class="flex justify-between items-start mb-4">
 | 
			
		||||
    <h2 id="tool-name-secondary" class="m-0">Tool Name</h2>
 | 
			
		||||
    <div class="flex items-center gap-2">
 | 
			
		||||
      <div id="share-button-secondary" style="display: none;">
 | 
			
		||||
      <div id="share-button-secondary" class="hidden">
 | 
			
		||||
      </div>
 | 
			
		||||
      <div id="contribute-button-secondary" style="display: none;">
 | 
			
		||||
      <div id="contribute-button-secondary" class="hidden">
 | 
			
		||||
      </div>
 | 
			
		||||
      <button class="btn-icon" onclick="window.hideToolDetails('secondary')">
 | 
			
		||||
        <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
 | 
			
		||||
@ -549,7 +549,7 @@ domains.forEach((domain: any) => {
 | 
			
		||||
              </button>
 | 
			
		||||
            ` : ''}
 | 
			
		||||
          </div>
 | 
			
		||||
          <div ${collapseOnMobile ? 'style="display: none;"' : ''} class="flex flex-wrap gap-1">
 | 
			
		||||
          <div ${collapseOnMobile ? 'class="hidden"' : ''} class="flex flex-wrap gap-1">
 | 
			
		||||
            ${conceptLinks}
 | 
			
		||||
          </div>
 | 
			
		||||
        </div>
 | 
			
		||||
 | 
			
		||||
@ -20,219 +20,224 @@ const { title, description = 'ForensicPathways - A comprehensive directory of di
 | 
			
		||||
  <title>{title} - ForensicPathways</title>
 | 
			
		||||
  <link rel="icon" type="image/x-icon" href="/favicon.ico">
 | 
			
		||||
  
 | 
			
		||||
  <script>
 | 
			
		||||
    document.addEventListener('DOMContentLoaded', () => {
 | 
			
		||||
      const THEME_KEY = 'dfir-theme';
 | 
			
		||||
<script>
 | 
			
		||||
  // Move utility functions OUTSIDE DOMContentLoaded to avoid race conditions
 | 
			
		||||
  function createToolSlug(toolName) {
 | 
			
		||||
    if (!toolName || typeof toolName !== 'string') {
 | 
			
		||||
      console.warn('[toolHelpers] Invalid toolName provided to createToolSlug:', toolName);
 | 
			
		||||
      return '';
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    return toolName.toLowerCase()
 | 
			
		||||
      .replace(/[^a-z0-9\s-]/g, '')     // Remove special characters
 | 
			
		||||
      .replace(/\s+/g, '-')             // Replace spaces with hyphens
 | 
			
		||||
      .replace(/-+/g, '-')              // Remove duplicate hyphens
 | 
			
		||||
      .replace(/^-|-$/g, '');           // Remove leading/trailing hyphens
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
      function getSystemTheme() {
 | 
			
		||||
        return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
 | 
			
		||||
      }
 | 
			
		||||
  function findToolByIdentifier(tools, identifier) {
 | 
			
		||||
    if (!identifier || !Array.isArray(tools)) return undefined;
 | 
			
		||||
    
 | 
			
		||||
    return tools.find(tool => 
 | 
			
		||||
      tool.name === identifier || 
 | 
			
		||||
      createToolSlug(tool.name) === identifier.toLowerCase()
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
      function getStoredTheme() {
 | 
			
		||||
        return localStorage.getItem(THEME_KEY) || 'auto';
 | 
			
		||||
      }
 | 
			
		||||
  function isToolHosted(tool) {
 | 
			
		||||
    return tool.projectUrl !== undefined && 
 | 
			
		||||
           tool.projectUrl !== null && 
 | 
			
		||||
           tool.projectUrl !== "" && 
 | 
			
		||||
           tool.projectUrl.trim() !== "";
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
      function applyTheme(theme) {
 | 
			
		||||
        const effectiveTheme = theme === 'auto' ? getSystemTheme() : theme;
 | 
			
		||||
        document.documentElement.setAttribute('data-theme', effectiveTheme);
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      function updateThemeToggle(theme) {
 | 
			
		||||
        document.querySelectorAll('[data-theme-toggle]').forEach(button => {
 | 
			
		||||
          button.setAttribute('data-current-theme', theme);
 | 
			
		||||
        });
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      function initTheme() {
 | 
			
		||||
        const storedTheme = getStoredTheme();
 | 
			
		||||
        applyTheme(storedTheme);
 | 
			
		||||
        updateThemeToggle(storedTheme);
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      function toggleTheme() {
 | 
			
		||||
        const current = getStoredTheme();
 | 
			
		||||
        const themes = ['light', 'dark', 'auto'];
 | 
			
		||||
        const currentIndex = themes.indexOf(current);
 | 
			
		||||
        const nextIndex = (currentIndex + 1) % themes.length;
 | 
			
		||||
        const nextTheme = themes[nextIndex];
 | 
			
		||||
        
 | 
			
		||||
        localStorage.setItem(THEME_KEY, nextTheme);
 | 
			
		||||
        applyTheme(nextTheme);
 | 
			
		||||
        updateThemeToggle(nextTheme);
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', () => {
 | 
			
		||||
        if (getStoredTheme() === 'auto') {
 | 
			
		||||
          applyTheme('auto');
 | 
			
		||||
        }
 | 
			
		||||
  // Consolidated scrolling utility - also moved outside DOMContentLoaded
 | 
			
		||||
  function scrollToElement(element, options = {}) {
 | 
			
		||||
    if (!element) return;
 | 
			
		||||
    
 | 
			
		||||
    // Calculate target position manually to avoid double-scroll
 | 
			
		||||
    setTimeout(() => {
 | 
			
		||||
      const headerHeight = document.querySelector('nav')?.offsetHeight || 80;
 | 
			
		||||
      const elementRect = element.getBoundingClientRect();
 | 
			
		||||
      const absoluteElementTop = elementRect.top + window.pageYOffset;
 | 
			
		||||
      const targetPosition = absoluteElementTop - headerHeight - 20; // Adjust this 20 as needed
 | 
			
		||||
      
 | 
			
		||||
      window.scrollTo({
 | 
			
		||||
        top: targetPosition,
 | 
			
		||||
        behavior: 'smooth'
 | 
			
		||||
      });
 | 
			
		||||
    }, 100);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
      (window as any).themeUtils = {
 | 
			
		||||
        initTheme,
 | 
			
		||||
        toggleTheme,
 | 
			
		||||
        getStoredTheme
 | 
			
		||||
      };
 | 
			
		||||
  // Convenience functions for common scroll targets
 | 
			
		||||
  function scrollToElementById(elementId, options = {}) {
 | 
			
		||||
    const element = document.getElementById(elementId);
 | 
			
		||||
    scrollToElement(element, options);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
      // Consolidated scrolling utility
 | 
			
		||||
      (window as any).scrollToElement = function(element, options = {}) {
 | 
			
		||||
        if (!element) return;
 | 
			
		||||
        
 | 
			
		||||
        // Calculate target position manually to avoid double-scroll
 | 
			
		||||
        setTimeout(() => {
 | 
			
		||||
          const headerHeight = document.querySelector('nav')?.offsetHeight || 80;
 | 
			
		||||
          const elementRect = element.getBoundingClientRect();
 | 
			
		||||
          const absoluteElementTop = elementRect.top + window.pageYOffset;
 | 
			
		||||
          const targetPosition = absoluteElementTop - headerHeight - 20; // Adjust this 20 as needed
 | 
			
		||||
          
 | 
			
		||||
          window.scrollTo({
 | 
			
		||||
            top: targetPosition,
 | 
			
		||||
            behavior: 'smooth'
 | 
			
		||||
          });
 | 
			
		||||
        }, 100);
 | 
			
		||||
      };
 | 
			
		||||
  function scrollToElementBySelector(selector, options = {}) {
 | 
			
		||||
    const element = document.querySelector(selector);
 | 
			
		||||
    scrollToElement(element, options);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
      // Convenience functions for common scroll targets
 | 
			
		||||
      (window as any).scrollToElementById = function(elementId, options = {}) {
 | 
			
		||||
        const element = document.getElementById(elementId);
 | 
			
		||||
        (window as any).scrollToElement(element, options);
 | 
			
		||||
      };
 | 
			
		||||
  // Attach to window immediately - BEFORE DOMContentLoaded
 | 
			
		||||
  (window as any).createToolSlug = createToolSlug;
 | 
			
		||||
  (window as any).findToolByIdentifier = findToolByIdentifier;
 | 
			
		||||
  (window as any).isToolHosted = isToolHosted;
 | 
			
		||||
  (window as any).scrollToElement = scrollToElement;
 | 
			
		||||
  (window as any).scrollToElementById = scrollToElementById;
 | 
			
		||||
  (window as any).scrollToElementBySelector = scrollToElementBySelector;
 | 
			
		||||
 | 
			
		||||
      (window as any).scrollToElementBySelector = function(selector, options = {}) {
 | 
			
		||||
        const element = document.querySelector(selector);
 | 
			
		||||
        (window as any).scrollToElement(element, options);
 | 
			
		||||
      };
 | 
			
		||||
  document.addEventListener('DOMContentLoaded', () => {
 | 
			
		||||
    const THEME_KEY = 'dfir-theme';
 | 
			
		||||
 | 
			
		||||
      function createToolSlug(toolName) {
 | 
			
		||||
        if (!toolName || typeof toolName !== 'string') {
 | 
			
		||||
          console.warn('[toolHelpers] Invalid toolName provided to createToolSlug:', toolName);
 | 
			
		||||
          return '';
 | 
			
		||||
        }
 | 
			
		||||
        
 | 
			
		||||
        return toolName.toLowerCase()
 | 
			
		||||
          .replace(/[^a-z0-9\s-]/g, '')     // Remove special characters
 | 
			
		||||
          .replace(/\s+/g, '-')             // Replace spaces with hyphens
 | 
			
		||||
          .replace(/-+/g, '-')              // Remove duplicate hyphens
 | 
			
		||||
          .replace(/^-|-$/g, '');           // Remove leading/trailing hyphens
 | 
			
		||||
      }
 | 
			
		||||
    function getSystemTheme() {
 | 
			
		||||
      return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
      function findToolByIdentifier(tools, identifier) {
 | 
			
		||||
        if (!identifier || !Array.isArray(tools)) return undefined;
 | 
			
		||||
        
 | 
			
		||||
        return tools.find(tool => 
 | 
			
		||||
          tool.name === identifier || 
 | 
			
		||||
          createToolSlug(tool.name) === identifier.toLowerCase()
 | 
			
		||||
        );
 | 
			
		||||
      }
 | 
			
		||||
    function getStoredTheme() {
 | 
			
		||||
      return localStorage.getItem(THEME_KEY) || 'auto';
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
      function isToolHosted(tool) {
 | 
			
		||||
        return tool.projectUrl !== undefined && 
 | 
			
		||||
               tool.projectUrl !== null && 
 | 
			
		||||
               tool.projectUrl !== "" && 
 | 
			
		||||
               tool.projectUrl.trim() !== "";
 | 
			
		||||
      }
 | 
			
		||||
    function applyTheme(theme) {
 | 
			
		||||
      const effectiveTheme = theme === 'auto' ? getSystemTheme() : theme;
 | 
			
		||||
      document.documentElement.setAttribute('data-theme', effectiveTheme);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
      (window as any).createToolSlug = createToolSlug;
 | 
			
		||||
      (window as any).findToolByIdentifier = findToolByIdentifier;
 | 
			
		||||
      (window as any).isToolHosted = isToolHosted;
 | 
			
		||||
    function updateThemeToggle(theme) {
 | 
			
		||||
      document.querySelectorAll('[data-theme-toggle]').forEach(button => {
 | 
			
		||||
        button.setAttribute('data-current-theme', theme);
 | 
			
		||||
      });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
      async function checkClientAuth(context = 'general') {
 | 
			
		||||
        try {
 | 
			
		||||
          const response = await fetch('/api/auth/status');
 | 
			
		||||
          const data = await response.json();
 | 
			
		||||
          
 | 
			
		||||
          switch (context) {
 | 
			
		||||
            case 'contributions':
 | 
			
		||||
              return {
 | 
			
		||||
                authenticated: data.contributionAuthenticated,
 | 
			
		||||
                authRequired: data.contributionAuthRequired,
 | 
			
		||||
                expires: data.expires
 | 
			
		||||
              };
 | 
			
		||||
            case 'ai':
 | 
			
		||||
              return {
 | 
			
		||||
                authenticated: data.aiAuthenticated,
 | 
			
		||||
                authRequired: data.aiAuthRequired,
 | 
			
		||||
                expires: data.expires
 | 
			
		||||
              };
 | 
			
		||||
            default:
 | 
			
		||||
              return {
 | 
			
		||||
                authenticated: data.authenticated,
 | 
			
		||||
                authRequired: data.contributionAuthRequired || data.aiAuthRequired,
 | 
			
		||||
                expires: data.expires
 | 
			
		||||
              };
 | 
			
		||||
          }
 | 
			
		||||
        } catch (error) {
 | 
			
		||||
          console.error('Auth check failed:', error);
 | 
			
		||||
          return {
 | 
			
		||||
            authenticated: false,
 | 
			
		||||
            authRequired: true
 | 
			
		||||
          };
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    function initTheme() {
 | 
			
		||||
      const storedTheme = getStoredTheme();
 | 
			
		||||
      applyTheme(storedTheme);
 | 
			
		||||
      updateThemeToggle(storedTheme);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
      async function requireClientAuth(callback, returnUrl, context = 'general') {
 | 
			
		||||
        const authStatus = await checkClientAuth(context);
 | 
			
		||||
        
 | 
			
		||||
        if (authStatus.authRequired && !authStatus.authenticated) {
 | 
			
		||||
          const targetUrl = returnUrl || window.location.href;
 | 
			
		||||
          window.location.href = `/api/auth/login?returnTo=${encodeURIComponent(targetUrl)}`;
 | 
			
		||||
          return false;
 | 
			
		||||
        } else {
 | 
			
		||||
          if (typeof callback === 'function') {
 | 
			
		||||
            callback();
 | 
			
		||||
          }
 | 
			
		||||
          return true;
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      async function showIfAuthenticated(selector, context = 'general') {
 | 
			
		||||
        const authStatus = await checkClientAuth(context);
 | 
			
		||||
        const element = document.querySelector(selector);
 | 
			
		||||
        
 | 
			
		||||
        if (element) {
 | 
			
		||||
          element.style.display = (!authStatus.authRequired || authStatus.authenticated) 
 | 
			
		||||
            ? 'inline-flex' 
 | 
			
		||||
            : 'none';
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      function setupAuthButtons(selector = '[data-contribute-button]') {
 | 
			
		||||
        document.addEventListener('click', async (e) => {
 | 
			
		||||
          if (!e.target) return;
 | 
			
		||||
          
 | 
			
		||||
          const button = (e.target as Element).closest(selector);
 | 
			
		||||
          if (!button) return;
 | 
			
		||||
          
 | 
			
		||||
          e.preventDefault();
 | 
			
		||||
          
 | 
			
		||||
          console.log('[AUTH] Contribute button clicked:', button.getAttribute('data-contribute-button'));
 | 
			
		||||
          
 | 
			
		||||
          await requireClientAuth(() => {
 | 
			
		||||
            console.log('[AUTH] Navigation approved, redirecting to:', (button as HTMLAnchorElement).href);
 | 
			
		||||
            window.location.href = (button as HTMLAnchorElement).href;
 | 
			
		||||
          }, (button as HTMLAnchorElement).href, 'contributions');
 | 
			
		||||
        });
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      (window as any).checkClientAuth = checkClientAuth;
 | 
			
		||||
      (window as any).requireClientAuth = requireClientAuth;
 | 
			
		||||
      (window as any).showIfAuthenticated = showIfAuthenticated;
 | 
			
		||||
      (window as any).setupAuthButtons = setupAuthButtons;
 | 
			
		||||
 | 
			
		||||
      initTheme();
 | 
			
		||||
      setupAuthButtons('[data-contribute-button]');
 | 
			
		||||
    function toggleTheme() {
 | 
			
		||||
      const current = getStoredTheme();
 | 
			
		||||
      const themes = ['light', 'dark', 'auto'];
 | 
			
		||||
      const currentIndex = themes.indexOf(current);
 | 
			
		||||
      const nextIndex = (currentIndex + 1) % themes.length;
 | 
			
		||||
      const nextTheme = themes[nextIndex];
 | 
			
		||||
      
 | 
			
		||||
      const initAIButton = async () => {
 | 
			
		||||
        await showIfAuthenticated('#ai-view-toggle', 'ai');
 | 
			
		||||
      };
 | 
			
		||||
      initAIButton();
 | 
			
		||||
      
 | 
			
		||||
      console.log('[CONSOLIDATED] All utilities loaded and initialized');
 | 
			
		||||
      localStorage.setItem(THEME_KEY, nextTheme);
 | 
			
		||||
      applyTheme(nextTheme);
 | 
			
		||||
      updateThemeToggle(nextTheme);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', () => {
 | 
			
		||||
      if (getStoredTheme() === 'auto') {
 | 
			
		||||
        applyTheme('auto');
 | 
			
		||||
      }
 | 
			
		||||
    });
 | 
			
		||||
  </script>
 | 
			
		||||
 | 
			
		||||
    (window as any).themeUtils = {
 | 
			
		||||
      initTheme,
 | 
			
		||||
      toggleTheme,
 | 
			
		||||
      getStoredTheme
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    async function checkClientAuth(context = 'general') {
 | 
			
		||||
      try {
 | 
			
		||||
        const response = await fetch('/api/auth/status');
 | 
			
		||||
        const data = await response.json();
 | 
			
		||||
        
 | 
			
		||||
        switch (context) {
 | 
			
		||||
          case 'contributions':
 | 
			
		||||
            return {
 | 
			
		||||
              authenticated: data.contributionAuthenticated,
 | 
			
		||||
              authRequired: data.contributionAuthRequired,
 | 
			
		||||
              expires: data.expires
 | 
			
		||||
            };
 | 
			
		||||
          case 'ai':
 | 
			
		||||
            return {
 | 
			
		||||
              authenticated: data.aiAuthenticated,
 | 
			
		||||
              authRequired: data.aiAuthRequired,
 | 
			
		||||
              expires: data.expires
 | 
			
		||||
            };
 | 
			
		||||
          default:
 | 
			
		||||
            return {
 | 
			
		||||
              authenticated: data.authenticated,
 | 
			
		||||
              authRequired: data.contributionAuthRequired || data.aiAuthRequired,
 | 
			
		||||
              expires: data.expires
 | 
			
		||||
            };
 | 
			
		||||
        }
 | 
			
		||||
      } catch (error) {
 | 
			
		||||
        console.error('Auth check failed:', error);
 | 
			
		||||
        return {
 | 
			
		||||
          authenticated: false,
 | 
			
		||||
          authRequired: true
 | 
			
		||||
        };
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    async function requireClientAuth(callback, returnUrl, context = 'general') {
 | 
			
		||||
      const authStatus = await checkClientAuth(context);
 | 
			
		||||
      
 | 
			
		||||
      if (authStatus.authRequired && !authStatus.authenticated) {
 | 
			
		||||
        const targetUrl = returnUrl || window.location.href;
 | 
			
		||||
        window.location.href = `/api/auth/login?returnTo=${encodeURIComponent(targetUrl)}`;
 | 
			
		||||
        return false;
 | 
			
		||||
      } else {
 | 
			
		||||
        if (typeof callback === 'function') {
 | 
			
		||||
          callback();
 | 
			
		||||
        }
 | 
			
		||||
        return true;
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    async function showIfAuthenticated(selector, context = 'general') {
 | 
			
		||||
      const authStatus = await checkClientAuth(context);
 | 
			
		||||
      const element = document.querySelector(selector);
 | 
			
		||||
      
 | 
			
		||||
      if (element) {
 | 
			
		||||
        element.style.display = (!authStatus.authRequired || authStatus.authenticated) 
 | 
			
		||||
          ? 'inline-flex' 
 | 
			
		||||
          : 'none';
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    function setupAuthButtons(selector = '[data-contribute-button]') {
 | 
			
		||||
      document.addEventListener('click', async (e) => {
 | 
			
		||||
        if (!e.target) return;
 | 
			
		||||
        
 | 
			
		||||
        const button = (e.target as Element).closest(selector);
 | 
			
		||||
        if (!button) return;
 | 
			
		||||
        
 | 
			
		||||
        e.preventDefault();
 | 
			
		||||
        
 | 
			
		||||
        console.log('[AUTH] Contribute button clicked:', button.getAttribute('data-contribute-button'));
 | 
			
		||||
        
 | 
			
		||||
        await requireClientAuth(() => {
 | 
			
		||||
          console.log('[AUTH] Navigation approved, redirecting to:', (button as HTMLAnchorElement).href);
 | 
			
		||||
          window.location.href = (button as HTMLAnchorElement).href;
 | 
			
		||||
        }, (button as HTMLAnchorElement).href, 'contributions');
 | 
			
		||||
      });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    (window as any).checkClientAuth = checkClientAuth;
 | 
			
		||||
    (window as any).requireClientAuth = requireClientAuth;
 | 
			
		||||
    (window as any).showIfAuthenticated = showIfAuthenticated;
 | 
			
		||||
    (window as any).setupAuthButtons = setupAuthButtons;
 | 
			
		||||
 | 
			
		||||
    initTheme();
 | 
			
		||||
    setupAuthButtons('[data-contribute-button]');
 | 
			
		||||
    
 | 
			
		||||
    const initAIButton = async () => {
 | 
			
		||||
      await showIfAuthenticated('#ai-view-toggle', 'ai');
 | 
			
		||||
    };
 | 
			
		||||
    initAIButton();
 | 
			
		||||
    
 | 
			
		||||
    console.log('[CONSOLIDATED] All utilities loaded and initialized');
 | 
			
		||||
  });
 | 
			
		||||
</script>
 | 
			
		||||
</head>
 | 
			
		||||
<body>
 | 
			
		||||
  <Navigation />
 | 
			
		||||
  <main class="container" style="flex: 1; padding: 2rem 1rem;">
 | 
			
		||||
  <main class="container flex-1 py-8 px-4">
 | 
			
		||||
    <slot />
 | 
			
		||||
  </main>
 | 
			
		||||
  <Footer />
 | 
			
		||||
 | 
			
		||||
@ -3,36 +3,36 @@ import BaseLayout from '../layouts/BaseLayout.astro';
 | 
			
		||||
---
 | 
			
		||||
 | 
			
		||||
<BaseLayout title="Über das Projekt" description="ForensicPathways - Ein Projekt für die Seminargruppe CC24-w1">
 | 
			
		||||
  <section style="padding: 2rem 0; max-width: 900px; margin: 0 auto;">
 | 
			
		||||
    <div style="text-align: center; margin-bottom: 3rem; padding: 2rem; background: linear-gradient(135deg, var(--color-bg-secondary) 0%, var(--color-bg-tertiary) 100%); border-radius: 1rem; border: 1px solid var(--color-border);">
 | 
			
		||||
      <h1 style="margin-bottom: 1rem; font-size: 2.5rem; color: var(--color-primary);">ForensicPathways</h1>
 | 
			
		||||
      <p style="font-size: 1.25rem; color: var(--color-text-secondary); margin-bottom: 0.5rem;">
 | 
			
		||||
  <section class="section container-narrow">
 | 
			
		||||
    <div class="header-center header-gradient mb-8">
 | 
			
		||||
      <h1 class="text-primary mb-4 text-2xl">ForensicPathways</h1>
 | 
			
		||||
      <p class="text-xl text-secondary mb-2">
 | 
			
		||||
        Forensik im Dienst der Transparenz
 | 
			
		||||
      </p>
 | 
			
		||||
      <p style="font-size: 1rem; color: var(--color-text-secondary);">
 | 
			
		||||
      <p class="text-base text-secondary">
 | 
			
		||||
        Ein Studienprojekt
 | 
			
		||||
      </p>
 | 
			
		||||
    </div>
 | 
			
		||||
 | 
			
		||||
    <div class="card" style="margin-bottom: 2rem; border-left: 4px solid var(--color-primary);">
 | 
			
		||||
      <div style="display: flex; align-items: center; gap: 0.75rem; margin-bottom: 1rem;">
 | 
			
		||||
      <div class="flex items-center gap-3 mb-4">
 | 
			
		||||
        <svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="var(--color-primary)" stroke-width="2">
 | 
			
		||||
          <path d="M9 11H5a2 2 0 0 0-2 2v7a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7a2 2 0 0 0-2-2h-4"/>
 | 
			
		||||
          <path d="M9 11V7a3 3 0 0 1 6 0v4"/>
 | 
			
		||||
        </svg>
 | 
			
		||||
        <h2 style="margin: 0; color: var(--color-primary);">Das Ziel</h2>
 | 
			
		||||
        <h2 class="m-0 text-primary">Das Ziel</h2>
 | 
			
		||||
      </div>
 | 
			
		||||
      <p style="margin-bottom: 1rem; line-height: 1.7;">
 | 
			
		||||
      <p class="mb-4 leading-relaxed">
 | 
			
		||||
        Diese Seite soll meinen Kommilitonen der Seminargruppe CC24-w1 und anderen interessierten Forensikbegeisterten eine <strong>einfache und übersichtliche Orientierung</strong> 
 | 
			
		||||
        bieten: Welches Werkzeug, welche Methode eignet sich für welchen Zweck in der digitalen Forensik?
 | 
			
		||||
      </p>
 | 
			
		||||
      <p style="margin-bottom: 1rem; line-height: 1.7;">
 | 
			
		||||
      <p class="mb-4 leading-relaxed">
 | 
			
		||||
        Ich stelle euch dafür <strong>Infrastruktur</strong> zur Verfügung, mit der ihr spezialisierte 
 | 
			
		||||
        Forensik-Software direkt einsetzen und erforschen könnt – sei es für <em>Bildungszwecke</em> oder im 
 | 
			
		||||
        <em>begrenzten Produktiveinsatz</em>.
 | 
			
		||||
      </p>
 | 
			
		||||
      <div style="background-color: var(--color-bg-secondary); padding: 1rem; border-radius: 0.5rem; margin-top: 1rem;">
 | 
			
		||||
        <p style="margin: 0; font-size: 0.875rem; color: var(--color-text-secondary);">
 | 
			
		||||
      <div class="bg-secondary p-4 rounded-lg mt-4">
 | 
			
		||||
        <p class="m-0 text-sm text-secondary">
 | 
			
		||||
          <strong>💡 Zugang:</strong> Die meisten Dienste sind bequem über das SSO der CC24-Cloud zugänglich, 
 | 
			
		||||
          mit nur wenigen Ausnahmen. Der Zugang ist entsprechend vorbehaltlich Absprache auf diesen Personenkreis limitiert.
 | 
			
		||||
        </p>
 | 
			
		||||
@ -40,42 +40,42 @@ import BaseLayout from '../layouts/BaseLayout.astro';
 | 
			
		||||
    </div>
 | 
			
		||||
 | 
			
		||||
    <div class="card" style="margin-bottom: 2rem; border-left: 4px solid var(--color-accent);">
 | 
			
		||||
      <div style="display: flex; align-items: center; gap: 0.75rem; margin-bottom: 1rem;">
 | 
			
		||||
      <div class="flex items-center gap-3 mb-4">
 | 
			
		||||
        <svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="var(--color-accent)" stroke-width="2">
 | 
			
		||||
          <path d="M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/>
 | 
			
		||||
          <polyline points="9,22 9,12 15,12 15,22"/>
 | 
			
		||||
        </svg>
 | 
			
		||||
        <h2 style="margin: 0; color: var(--color-accent);">Wissenschaftliche Grundlage</h2>
 | 
			
		||||
      </div>
 | 
			
		||||
      <p style="margin-bottom: 1rem; line-height: 1.7;">
 | 
			
		||||
      <p class="mb-4 leading-relaxed">
 | 
			
		||||
        Bei der vorgestellten Software handelt es sich um eine <strong>kuratierte Auswahl</strong> von Anwendungen, 
 | 
			
		||||
        vor allem aus dem Bereich der Open-Source-Software.
 | 
			
		||||
      </p>
 | 
			
		||||
      <p style="margin-bottom: 1rem; line-height: 1.7;">
 | 
			
		||||
      <p class="mb-4 leading-relaxed">
 | 
			
		||||
        Das zugrunde liegende Modell folgt dem bewährten <strong>NIST-Framework</strong> nach Kent, Chevalier, Grance und Dang 
 | 
			
		||||
        (NIST SP 800-86), das ich wegen seiner <em>Einfachheit</em> und <em>praktischen Adaptierbarkeit</em> schätze:
 | 
			
		||||
      </p>
 | 
			
		||||
      <div style="display: grid; grid-template-columns: repeat(4, 1fr); gap: 0.5rem; margin: 1.5rem 0; text-align: center;">
 | 
			
		||||
        <div style="background-color: var(--color-bg-secondary); padding: 1rem; border-radius: 0.5rem; border: 2px solid var(--color-accent);">
 | 
			
		||||
          <strong style="color: var(--color-accent);">Datensammlung</strong>
 | 
			
		||||
      <div class="grid-4 gap-2 my-6 text-center">
 | 
			
		||||
        <div class="bg-secondary p-4 rounded-lg border-2 border-accent">
 | 
			
		||||
          <strong class="text-accent">Datensammlung</strong>
 | 
			
		||||
        </div>
 | 
			
		||||
        <div style="background-color: var(--color-bg-secondary); padding: 1rem; border-radius: 0.5rem; border: 2px solid var(--color-accent);">
 | 
			
		||||
          <strong style="color: var(--color-accent);">Auswertung</strong>
 | 
			
		||||
        <div class="bg-secondary p-4 rounded-lg border-2 border-accent">
 | 
			
		||||
          <strong class="text-accent">Auswertung</strong>
 | 
			
		||||
        </div>
 | 
			
		||||
        <div style="background-color: var(--color-bg-secondary); padding: 1rem; border-radius: 0.5rem; border: 2px solid var(--color-accent);">
 | 
			
		||||
          <strong style="color: var(--color-accent);">Analyse</strong>
 | 
			
		||||
        <div class="bg-secondary p-4 rounded-lg border-2 border-accent">
 | 
			
		||||
          <strong class="text-accent">Analyse</strong>
 | 
			
		||||
        </div>
 | 
			
		||||
        <div style="background-color: var(--color-bg-secondary); padding: 1rem; border-radius: 0.5rem; border: 2px solid var(--color-accent);">
 | 
			
		||||
          <strong style="color: var(--color-accent);">Bericht</strong>
 | 
			
		||||
        <div class="bg-secondary p-4 rounded-lg border-2 border-accent">
 | 
			
		||||
          <strong class="text-accent">Bericht</strong>
 | 
			
		||||
        </div>
 | 
			
		||||
      </div>
 | 
			
		||||
      <p style="margin: 0; font-size: 0.875rem; color: var(--color-text-secondary);">
 | 
			
		||||
      <p class="m-0 text-sm text-secondary">
 | 
			
		||||
        Diese Prozessphasen werden in der <strong>Matrix-Ansicht</strong> auf der Startseite mit forensischen Wissensdomänen verknüpft.
 | 
			
		||||
      </p>
 | 
			
		||||
    </div>
 | 
			
		||||
 | 
			
		||||
    <div class="card" style="margin-bottom: 2rem; border-left: 4px solid var(--color-warning);">
 | 
			
		||||
      <div style="display: flex; align-items: center; gap: 0.75rem; margin-bottom: 1rem;">
 | 
			
		||||
      <div class="flex items-center gap-3 mb-4">
 | 
			
		||||
        <svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="var(--color-warning)" stroke-width="2">
 | 
			
		||||
          <path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"/>
 | 
			
		||||
          <path d="M9 12l2 2 4-4"/>
 | 
			
		||||
@ -83,9 +83,9 @@ import BaseLayout from '../layouts/BaseLayout.astro';
 | 
			
		||||
        <h2 style="margin: 0; color: var(--color-warning);">Weshalb der Fokus auf Open Source?</h2>
 | 
			
		||||
      </div>
 | 
			
		||||
      
 | 
			
		||||
      <div style="background: linear-gradient(135deg, var(--color-warning) 0%, var(--color-accent) 100%); color: white; padding: 1.5rem; border-radius: 0.75rem; margin-bottom: 1.5rem;">
 | 
			
		||||
        <h3 style="margin: 0 0 1rem 0; font-size: 1.125rem;">💡 Zentrale Hypothese</h3>
 | 
			
		||||
        <p style="margin: 0; line-height: 1.6; font-style: italic;">
 | 
			
		||||
      <div class="card-warning rounded-xl p-6 mb-6">
 | 
			
		||||
        <h3 class="m-0 mb-4 text-lg">💡 Zentrale Hypothese</h3>
 | 
			
		||||
        <p class="m-0 leading-normal italic">
 | 
			
		||||
          "Die IT-Forensik verlangt nach der Anwendung von wissenschaftlichen Methoden und Objektivität. 
 | 
			
		||||
          Die wissenschaftliche Methode verlangt einen transparenten Untersuchungsprozess, der in der Hauptverhandlung 
 | 
			
		||||
          bzw. dem Auftraggeber gegenüber stets nachvollziehbar und erklärbar sein muss. 
 | 
			
		||||
@ -127,13 +127,13 @@ import BaseLayout from '../layouts/BaseLayout.astro';
 | 
			
		||||
    </div>
 | 
			
		||||
 | 
			
		||||
    <div id= "support" class="card" style="margin-bottom: 2rem; border-left: 4px solid var(--color-primary);">
 | 
			
		||||
      <div style="display: flex; align-items: center; gap: 0.75rem; margin-bottom: 1rem;">
 | 
			
		||||
      <div class="flex items-center gap-3 mb-4">
 | 
			
		||||
        <svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="var(--color-primary)" stroke-width="2">
 | 
			
		||||
          <path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/>
 | 
			
		||||
        </svg>
 | 
			
		||||
        <h2 style="margin: 0; color: var(--color-primary);">Unterstützung und Kontakt</h2>
 | 
			
		||||
        <h2 class="m-0 text-primary">Unterstützung und Kontakt</h2>
 | 
			
		||||
      </div>
 | 
			
		||||
      <p style="margin-bottom: 1rem; line-height: 1.7;">
 | 
			
		||||
      <p class="mb-4 leading-relaxed">
 | 
			
		||||
        Falls eine Anwendung nicht wie vorgesehen funktioniert, ihr Unterstützung braucht, der Speicherplatz ausgeht 
 | 
			
		||||
        oder sonstige Probleme auftreten: <strong>Schreibt mir einfach auf Signal</strong> oder an <a href="mailto:mstoeck3@hs-mittweida.de">mstoeck3@hs-mittweida.de</a>.
 | 
			
		||||
      </p>
 | 
			
		||||
@ -172,7 +172,7 @@ import BaseLayout from '../layouts/BaseLayout.astro';
 | 
			
		||||
    </div>
 | 
			
		||||
 | 
			
		||||
    <div class="card" style="margin-bottom: 2rem; border-left: 4px solid var(--color-accent);">
 | 
			
		||||
      <div style="display: flex; align-items: center; gap: 0.75rem; margin-bottom: 1rem;">
 | 
			
		||||
      <div class="flex items-center gap-3 mb-4">
 | 
			
		||||
        <svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="var(--color-accent)" stroke-width="2">
 | 
			
		||||
          <path d="M16 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"/>
 | 
			
		||||
          <circle cx="8.5" cy="7" r="4"/>
 | 
			
		||||
 | 
			
		||||
@ -51,7 +51,7 @@
 | 
			
		||||
    <div class="spinner"></div>
 | 
			
		||||
    <h2>Processing authentication...</h2>
 | 
			
		||||
    <p>Please wait while we complete your login.</p>
 | 
			
		||||
    <div id="error-message" style="display: none;" class="error"></div>
 | 
			
		||||
    <div id="error-message" class="hidden error"></div>
 | 
			
		||||
  </div>
 | 
			
		||||
 | 
			
		||||
  <script>
 | 
			
		||||
 | 
			
		||||
@ -14,8 +14,8 @@ const { authenticated, userEmail, userId } = authResult;
 | 
			
		||||
---
 | 
			
		||||
 | 
			
		||||
<BaseLayout title="Contribute" description="Inhalte zum ForensicPathways beitragen">
 | 
			
		||||
  <section style="padding: 2rem 0;">
 | 
			
		||||
    <div style="text-align: center; margin-bottom: 3rem; padding: 2rem; background: linear-gradient(135deg, var(--color-primary) 0%, var(--color-accent) 100%); color: white; border-radius: 1rem; border: 1px solid var(--color-border);">
 | 
			
		||||
  <section class="section-padding">
 | 
			
		||||
    <div class="header-center header-primary mb-8">
 | 
			
		||||
      <h1 style="margin-bottom: 1rem; font-size: 2.5rem;">
 | 
			
		||||
        <svg width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="margin-right: 0.75rem; vertical-align: middle;">
 | 
			
		||||
          <path d="M16 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"/>
 | 
			
		||||
@ -49,7 +49,7 @@ const { authenticated, userEmail, userId } = authResult;
 | 
			
		||||
<div class="card" 
 | 
			
		||||
     style="padding: 2rem; border-left: 4px solid var(--color-primary); transition: var(--transition-fast);
 | 
			
		||||
            display:flex; flex-direction:column;">
 | 
			
		||||
  <div style="display: flex; align-items: center; gap: 1rem; margin-bottom: 1.5rem;">
 | 
			
		||||
  <div class="flex items-center gap-4 mb-6">
 | 
			
		||||
    <div style="width: 48px; height: 48px; background-color: var(--color-primary); border-radius: 0.5rem; display: flex; align-items: center; justify-content: center;">
 | 
			
		||||
      <svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="white" stroke-width="2">
 | 
			
		||||
        <path d="M14.7 6.3a1 1 0 0 0 0 1.4l1.6 1.6a1 1 0 0 0 1.4 0l3.77-3.77a6 6 0 0 1-7.94 7.94l-6.91 6.91a2.12 2.12 0 0 1-3-3l6.91-6.91a6 6 0 0 1 7.94-7.94l-3.76 3.76z"/>
 | 
			
		||||
@ -108,7 +108,7 @@ const { authenticated, userEmail, userId } = authResult;
 | 
			
		||||
       style="padding: 2rem; border-left: 4px solid var(--color-accent); cursor: pointer; transition: var(--transition-fast);
 | 
			
		||||
              display:flex; flex-direction:column;" 
 | 
			
		||||
       onclick="window.location.href='/contribute/knowledgebase'">
 | 
			
		||||
    <div style="display: flex; align-items: center; gap: 1rem; margin-bottom: 1.5rem;">
 | 
			
		||||
    <div class="flex items-center gap-4 mb-6">
 | 
			
		||||
      <div style="width: 48px; height: 48px; background-color: var(--color-accent); border-radius: 0.5rem; display: flex; align-items: center; justify-content: center;">
 | 
			
		||||
        <svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="white" stroke-width="2">
 | 
			
		||||
          <path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/>
 | 
			
		||||
@ -142,7 +142,7 @@ const { authenticated, userEmail, userId } = authResult;
 | 
			
		||||
  <div class="card" 
 | 
			
		||||
       style="padding: 2rem; border-left: 4px solid var(--color-accent); cursor: pointer; transition: var(--transition-fast);
 | 
			
		||||
              display:flex; flex-direction:column;">
 | 
			
		||||
    <div style="display: flex; align-items: center; gap: 1rem; margin-bottom: 1.5rem;">
 | 
			
		||||
    <div class="flex items-center gap-4 mb-6">
 | 
			
		||||
      <div style="width: 48px; height: 48px; background-color: var(--color-warning); border-radius: 0.5rem; display: flex; align-items: center; justify-content: center;">
 | 
			
		||||
        <svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="white" stroke-width="2">
 | 
			
		||||
          <circle cx="12" cy="12" r="10"/>
 | 
			
		||||
 | 
			
		||||
@ -20,7 +20,7 @@ const sortedTools = data.tools.sort((a: any, b: any) => a.name.localeCompare(b.n
 | 
			
		||||
<BaseLayout title="Contribute Knowledge Base Article">
 | 
			
		||||
  <div class="container" style="max-width: 900px; margin: 0 auto; padding: 2rem 1rem;">
 | 
			
		||||
    
 | 
			
		||||
    <div style="text-align: center; margin-bottom: 2rem; padding: 2rem; background: linear-gradient(135deg, var(--color-primary) 0%, var(--color-accent) 100%); color: white; border-radius: 1rem;">
 | 
			
		||||
    <div class="header-center header-primary rounded-xl">
 | 
			
		||||
      <h1 style="margin-bottom: 1rem; font-size: 2rem;">Knowledgebase-Artikel</h1>
 | 
			
		||||
      <p style="margin: 0.5rem 0; opacity: 0.9;">Danke für deinen Beitrag!</p>
 | 
			
		||||
      {userEmail && <p style="margin: 0.5rem 0; opacity: 0.8;"><strong>Eingeloggt als:</strong> {userEmail}</p>}
 | 
			
		||||
@ -114,7 +114,7 @@ const sortedTools = data.tools.sort((a: any, b: any) => a.name.localeCompare(b.n
 | 
			
		||||
          <div class="form-group">
 | 
			
		||||
            <label class="form-label">Dokumente, Bilder, Videos (Optional)</label>
 | 
			
		||||
            <div class="upload-area" id="upload-area">
 | 
			
		||||
              <input type="file" id="file-input" multiple accept=".pdf,.doc,.docx,.txt,.md,.zip,.png,.jpg,.jpeg,.gif,.mp4,.webm" style="display: none;">
 | 
			
		||||
              <input type="file" id="file-input" multiple accept=".pdf,.doc,.docx,.txt,.md,.zip,.png,.jpg,.jpeg,.gif,.mp4,.webm" class="hidden">
 | 
			
		||||
              <div class="upload-placeholder">
 | 
			
		||||
                <svg width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1">
 | 
			
		||||
                  <path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/>
 | 
			
		||||
@ -125,7 +125,7 @@ const sortedTools = data.tools.sort((a: any, b: any) => a.name.localeCompare(b.n
 | 
			
		||||
                <small>Die Dateien landen in der CC24-Cloud. Keine Malware.</small>
 | 
			
		||||
              </div>
 | 
			
		||||
            </div>
 | 
			
		||||
            <div id="file-list" class="file-list" style="display: none;">
 | 
			
		||||
            <div id="file-list" class="file-list hidden">
 | 
			
		||||
              <h5>Ausgewählte Dateien</h5>
 | 
			
		||||
              <div id="files-container"></div>
 | 
			
		||||
            </div>
 | 
			
		||||
@ -177,7 +177,7 @@ const sortedTools = data.tools.sort((a: any, b: any) => a.name.localeCompare(b.n
 | 
			
		||||
          <a href="/" class="btn btn-secondary">Abbruch</a>
 | 
			
		||||
          <button type="submit" id="submit-btn" class="btn btn-accent">
 | 
			
		||||
            <span id="submit-text">Abschicken</span>
 | 
			
		||||
            <span id="submit-spinner" style="display: none;">⏳</span>
 | 
			
		||||
            <span id="submit-spinner" class="hidden">⏳</span>
 | 
			
		||||
          </button>
 | 
			
		||||
        </div>
 | 
			
		||||
      </form>
 | 
			
		||||
@ -193,7 +193,7 @@ const sortedTools = data.tools.sort((a: any, b: any) => a.name.localeCompare(b.n
 | 
			
		||||
          Your knowledge‑base article has been submitted as an issue for review by maintainers.
 | 
			
		||||
        </p>
 | 
			
		||||
        <div style="display:flex; gap:1rem; justify-content:center;">
 | 
			
		||||
          <a id="issue-link" href="#" target="_blank" class="btn btn-primary" style="display:none;">View Issue</a>
 | 
			
		||||
          <a id="issue-link" href="#" target="_blank" class="btn btn-primary hidden">View Issue</a>
 | 
			
		||||
          <a href="/" class="btn btn-secondary">Back to Home</a>
 | 
			
		||||
        </div>
 | 
			
		||||
      </div>
 | 
			
		||||
 | 
			
		||||
@ -27,7 +27,7 @@ const isEdit = !!editTool;
 | 
			
		||||
<BaseLayout title={isEdit ? `Edit ${editTool?.name}` : 'Contribute Tool'}>
 | 
			
		||||
  <div class="container" style="max-width: 900px; margin: 0 auto; padding: 2rem 1rem;">
 | 
			
		||||
    
 | 
			
		||||
    <div style="text-align: center; margin-bottom: 2rem; padding: 2rem; background: linear-gradient(135deg, var(--color-primary) 0%, var(--color-accent) 100%); color: white; border-radius: 1rem;">
 | 
			
		||||
    <div class="header-center header-primary rounded-xl">
 | 
			
		||||
      <h1 style="margin-bottom: 1rem; font-size: 2rem;">{isEdit ? `Edit: ${editTool?.name}` : 'Tool / Methode / Konzept beitragen'}</h1>
 | 
			
		||||
      <p style="margin: 0.5rem 0; opacity: 0.9;">
 | 
			
		||||
        {isEdit 
 | 
			
		||||
@ -105,7 +105,7 @@ const isEdit = !!editTool;
 | 
			
		||||
        <div style="border: 1px solid var(--color-border); border-radius: 0.5rem; padding: 1.5rem; margin-bottom: 2rem;">
 | 
			
		||||
          <h3 style="margin: 0 0 1.5rem 0; color: var(--color-primary); border-bottom: 1px solid var(--color-border); padding-bottom: 0.5rem;">Kategorien</h3>
 | 
			
		||||
          
 | 
			
		||||
          <div style="display: grid; grid-template-columns: 1fr 1fr; gap: 2rem;">
 | 
			
		||||
          <div class="grid-2 gap-8">
 | 
			
		||||
            <div>
 | 
			
		||||
              <label style="display: block; margin-bottom: 0.75rem; font-weight: 600;">Forensische Domänen</label>
 | 
			
		||||
              <div style="display: grid; gap: 0.5rem;">
 | 
			
		||||
@ -166,7 +166,7 @@ const isEdit = !!editTool;
 | 
			
		||||
            </div>
 | 
			
		||||
          </div>
 | 
			
		||||
 | 
			
		||||
          <div style="display: grid; grid-template-columns: 1fr 1fr; gap: 2rem;">
 | 
			
		||||
          <div class="grid-2 gap-8">
 | 
			
		||||
            <div>
 | 
			
		||||
              <label for="accessType" style="display: block; margin-bottom: 0.75rem; font-weight: 600;">Zugriff über:</label>
 | 
			
		||||
              <select id="accessType" name="accessType">
 | 
			
		||||
@ -257,7 +257,7 @@ const isEdit = !!editTool;
 | 
			
		||||
        <h3 style="margin-bottom: 1rem;">Contribution Submitted!</h3>
 | 
			
		||||
        <p id="success-message" style="margin-bottom: 1.5rem;">Your contribution has been submitted successfully.</p>
 | 
			
		||||
        <div style="display: flex; gap: 1rem; justify-content: center;">
 | 
			
		||||
          <a id="pr-link" href="#" target="_blank" class="btn btn-primary" style="display: none;">View Pull Request</a>
 | 
			
		||||
          <a id="pr-link" href="#" target="_blank" class="btn btn-primary hidden">View Pull Request</a>
 | 
			
		||||
          <a href="/" class="btn btn-secondary">Back to Home</a>
 | 
			
		||||
        </div>
 | 
			
		||||
      </div>
 | 
			
		||||
 | 
			
		||||
@ -3,8 +3,8 @@ import BaseLayout from '../layouts/BaseLayout.astro';
 | 
			
		||||
---
 | 
			
		||||
 | 
			
		||||
<BaseLayout title="Impressum" description="ForensicPathways - Impressum">
 | 
			
		||||
  <section style="padding: 2rem 0; max-width: 900px; margin: 0 auto;">
 | 
			
		||||
    <div style="text-align: center; margin-bottom: 3rem; padding: 2rem; background: linear-gradient(135deg, var(--color-bg-secondary) 0%, var(--color-bg-tertiary) 100%); border-radius: 1rem; border: 1px solid var(--color-border);">
 | 
			
		||||
  <section class="section container-narrow">
 | 
			
		||||
    <div class="header-center header-gradient mb-8">
 | 
			
		||||
      <p>Bei dieser Webseite handelt es sich um ein privates Werk. Keine Bildungseinrichtung, kein Unternehmen sind finanziell oder moderierend Teil dieses Projekts.</p>
 | 
			
		||||
      <h1>Impressum</h1>
 | 
			
		||||
<p>Angaben gemäß § 5 DDG</p>
 | 
			
		||||
@ -16,7 +16,7 @@ Mario Stöckl<br>
 | 
			
		||||
</p>
 | 
			
		||||
<p><strong>Kontakt:</strong> <br>
 | 
			
		||||
Telefon: 01522-7086296<br>
 | 
			
		||||
E-Mail: <a href='mailto:mario.stoeckl@posteo.de'>mario.stoeckl@posteo.de</a>
 | 
			
		||||
E-Mail: <a href='mailto:mstoeck3@hs-mittweida.de'>mstoeck3@hs-mittweida.de</a>
 | 
			
		||||
</p>
 | 
			
		||||
<p><strong>PGP-Schlüssel:</strong><br>
 | 
			
		||||
Zur sicheren Kommunikation per E-Mail können Sie folgenden PGP-Schlüssel verwenden:<br><br>
 | 
			
		||||
 | 
			
		||||
@ -60,7 +60,7 @@ const phases = data.phases;
 | 
			
		||||
      <p class="approach-info">
 | 
			
		||||
        <span class="info-icon">ℹ️</span>
 | 
			
		||||
        Die lila gekennzeichneten Einträge sind über das Single-Sign-On der CC24-Cloud direkt zugänglich. 
 | 
			
		||||
        Teilnehmer der Seminargruppe CC24-w1 können die gehostete Infrastruktur nutzen.
 | 
			
		||||
        Teilnehmer der Seminargruppe CC24-w1 (oder andere Berechtigte) können die gehostete Infrastruktur nutzen.
 | 
			
		||||
        <a href="/about#support">Kontakt bei Problemen</a>
 | 
			
		||||
      </p>
 | 
			
		||||
      
 | 
			
		||||
@ -143,8 +143,8 @@ const phases = data.phases;
 | 
			
		||||
 | 
			
		||||
<TargetedScenarios />
 | 
			
		||||
 | 
			
		||||
  <section id="filters-section" style="padding: 2rem 0;">
 | 
			
		||||
    <div style="text-align: center; margin-bottom: 2rem;">
 | 
			
		||||
  <section id="filters-section" class="section">
 | 
			
		||||
    <div class="content-center-lg">
 | 
			
		||||
      <h3 style="color: var(--color-text); margin-bottom: 0.5rem;">Alle verfügbaren Werkzeuge durchsuchen</h3>
 | 
			
		||||
      <p style="color: var(--color-text-secondary); margin: 0;">
 | 
			
		||||
        Nutzen Sie die erweiterten Filter und Kategorien für eine detaillierte Suche
 | 
			
		||||
 | 
			
		||||
@ -42,7 +42,7 @@ knowledgebaseEntries.sort((a: any, b: any) => a.title.localeCompare(b.title));
 | 
			
		||||
---
 | 
			
		||||
 | 
			
		||||
<BaseLayout title="Knowledgebase" description="Extended documentation and insights for DFIR tools">
 | 
			
		||||
  <section style="padding: 2rem 0;">
 | 
			
		||||
  <section class="section-padding">
 | 
			
		||||
    <div class="text-center mb-8 p-8 bg-secondary rounded-lg border">
 | 
			
		||||
      <h1 class="mb-4 text-2xl text-primary">Knowledgebase</h1>
 | 
			
		||||
      <p class="text-lg text-secondary mb-4">
 | 
			
		||||
@ -209,13 +209,13 @@ knowledgebaseEntries.sort((a: any, b: any) => a.title.localeCompare(b.title));
 | 
			
		||||
      )}
 | 
			
		||||
    </div>
 | 
			
		||||
 | 
			
		||||
    <div id="no-kb-results" class="card text-center p-8" style="display: none;">
 | 
			
		||||
    <div id="no-kb-results" class="card text-center p-8 hidden">
 | 
			
		||||
      <h3 class="text-secondary mb-2">Keine Ergebnisse gefunden</h3>
 | 
			
		||||
      <p class="text-secondary">Versuchen Sie es mit anderen Suchbegriffen.</p>
 | 
			
		||||
    </div>
 | 
			
		||||
  </section>
 | 
			
		||||
  
 | 
			
		||||
  <div id="fab-container" class="fixed bottom-8 right-8 z-50" style="display: none;">
 | 
			
		||||
  <div id="fab-container" class="fixed bottom-8 right-8 z-50 hidden">
 | 
			
		||||
    <ContributionButton 
 | 
			
		||||
      type="write" 
 | 
			
		||||
      variant="primary" 
 | 
			
		||||
 | 
			
		||||
@ -14,8 +14,8 @@ const hostedServices = data.tools.filter((tool: any) => {
 | 
			
		||||
---
 | 
			
		||||
 | 
			
		||||
<BaseLayout title="Service Status">
 | 
			
		||||
  <section style="padding: 2rem 0;">
 | 
			
		||||
    <h1 style="text-align: center; margin-bottom: 1rem;">Service Status</h1>
 | 
			
		||||
  <section class="section-padding">
 | 
			
		||||
    <h1 class="content-center">Service Status</h1>
 | 
			
		||||
    <p class="text-muted" style="text-align: center; max-width: 600px; margin: 0 auto 3rem;">
 | 
			
		||||
      Live-Monitoring zum Onlinestatus der Dienste.
 | 
			
		||||
    </p>
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										2920
									
								
								src/styles/global copy.css
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										2920
									
								
								src/styles/global copy.css
									
									
									
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user