Project structure:

DarkHub/
├── courses/
│   └── test.json
├── static/
│   ├── css/
│   │   ├── base.css
│   │   ├── course-content.css
│   │   └── hotbar.css
│   ├── images/
│   │   ├── h_icon.webp
│   │   ├── javascript.png
│   │   ├── section-icon.webp
│   │   ├── subtopic-icon.webp
│   │   └── topic-icon.webp
│   ├── js/
│   │   ├── course_render_manager.js
│   │   └── main.js
├── templates/
│   ├── components/
│   │   └── hotbar.html
│   └── index.html
└── app.py


Project files:

app.py
```py
import json
import os

from flask import Flask, render_template, jsonify

app = Flask(__name__)

@app.route("/")
def index():
    return render_template("index.html")

@app.route('/get_course/<course_id>')
def get_course(course_id):
    course_path = os.path.join('courses', f'{course_id}.json')
    try:
        with open(course_path, 'r', encoding='utf-8') as file:
            course_data = json.load(file)
        return jsonify(course_data)
    except FileNotFoundError:
        return jsonify({"error": "Course not found"}), 404
    except json.JSONDecodeError:
        return jsonify({"error": "Invalid course data"}), 500

if __name__ == "__main__":
    app.run(host='0.0.0.0', port=8081, debug=True)

```


courses/test.json
```json
...
```


static/css/base.css
```css
:root {
    --base-color: rgb(241, 240, 231);
    --second-color: #cecece;
    --font-color: #252525;
    --base-gap: 15px;
}

html {
    font-family: "Roboto", sans-serif;
    font-weight: 500;
    font-size: 20px;
    color: var(--font-color);
}

body {
    margin: 0;
}

.screen {
    display: flex;
    width: 100%;
    /* height: 100vh; */
    background-color: var(--base-color);
    flex-direction: column;
    align-items: center;
}
.screen-content {
    display: flex;
    width: calc(100% - var(--base-gap) * 2);
    height: 100%;
    padding: var(--base-gap);
    max-width: 1200px;
}
.courses-container {
    display: grid;
    grid-template-columns: repeat(auto-fit, 300px);
    grid-template-rows: repeat(auto-fit, 200px);
    grid-gap: var(--base-gap);
    width: 100%;
    height: 100%;

    display: none;
}
.course-tile {
    width: 300px;
    height: 200px;
    /* aspect-ratio: 2 / 3; */
    display: flex;
    align-items: center;
    justify-content: center;
    overflow: hidden;
    border-radius: 10px;
    position: relative;
    cursor: pointer;
}
.course-background {
    height: 100%;
    position: absolute;
    transition: transform 0.3s;
}
.course-title {
    z-index: 2;
    position: absolute;
    left: var(--base-gap);
    bottom: -10px;
    opacity: 0;
    color: white;
    transition: bottom 0.2s, opacity 0.2s;
}
.course-tile:hover {
    .course-background {
        transform: scale(1.1);
    }
    .course-title {
        bottom: var(--base-gap);
        opacity: 1;
    }
}
.course-tile:active {
    .course-background {
        transform: scale(1.05);
        transition: transform 0.05s;
    }
}

button:hover {
    filter: brightness(90%);
}
button:active {
    transform: scale(0.95);
}


pre {
    background-color: #2d2d2d;
    border-radius: 5px;
    padding: 15px;
    position: relative;
    display: inline-block;
    min-width: 50px;
    max-width: 100%;
    overflow-x: auto;
}
pre > code {
    color: #ccc;
    white-space: pre-wrap;
    word-break: break-word;
}
.copy-button {
    position: absolute;
    top: 5px;
    right: 5px;
    background-color: #4CAF50;
    border: none;
    color: white;
    padding: 5px 10px;
    text-align: center;
    text-decoration: none;
    display: inline-block;
    font-size: 12px;
    margin: 4px 2px;
    cursor: pointer;
    border-radius: 3px;
}
/* Добавляем стили для контейнера кода */
.code-container {
    display: flex;
    justify-content: center;
    width: 100%;
}
.code-container pre {
    max-width: 80%; /* или другое значение, которое вам подходит */
}

.topics > .topic-container:first-of-type {
    .topicTitle {
        margin: 10px 0 10px 0;
        border-top: none;
    }
}


/* Ползунок */

::-webkit-scrollbar {
  width: 5px; /* Ширина ползунка с учетом margin */
  background: transparent; /* Прозрачный фон */
}

::-webkit-scrollbar-thumb {
  background: rgb(103 104 114); /* Белый цвет с прозрачностью 70% */
  border-radius: 10px; /* Скругление ползунка */
  margin: 5px; /* Отступ от стенок */
}

```


static/css/course-content.css
```css
.course-content-container {
    display: flex;
    /* grid-template-columns: repeat(auto-fit, 300px); */
    /* grid-template-rows: repeat(auto-fit, 200px); */
    grid-gap: var(--base-gap);
    width: 100%;
    height: 100%;
    flex-direction: column;
}

.courseTitle {

}
.courseDescription {

}

.sections {

}

.section-container {
    display: flex;
    flex-direction: column;
    gap: 10px;
}
.sectionTitle {
    display: flex;
    font-size: 30px;
    padding: 2px 8px;
    background-color: #d6d6d6;
    border-radius: 5px;
    border-left: blue 2px solid;
    align-items: center;
}
.course-list-icon {
    width: 20px;
    height: 20px;
}

.topics {
    margin-bottom: 30px;
    /* border-left: 2px solid #d3d3d3; */
    /* padding-left: 10px; */
}

.topic-container {
    display: flex;
    flex-direction: column;
    gap: 10px;
}
.topicTitle {
    font-size: 25px;
    margin: 150px 0 10px 0;
    border-top: 1px solid #00000012;
    padding-top: 5px;
}

.subtopics {
    display: flex;
    margin-bottom: 30px;
    /* border-left: 2px solid #d3d3d3; */
    /* padding-left: 10px; */
    flex-direction: column;
    gap: 30px;
}

.subtopic-container {
    display: flex;
    flex-direction: column;
    gap: 10px;
    /* padding-left: 10px; */
    /* border-left: 1px solid #dbdbdb; */
}
.title {
    display: flex;
    align-items: center;
}
.content {
    font-weight: 400;
    margin-left: 20px;
}
.definitions {
    display: flex;
    flex-direction: column;
    gap: 5px;
    padding: 10px;
    /* border-left: 2px solid blue; */
    border-radius: 5px 5px 15px 15px;
    background-color: #e0e0e0;
}
.definition-container {
    display: flex;
    align-items: center;
    gap: 10px;
}
.definition-a {
    padding: 2px 8px;
    background-color: #2a2a2a;
    border-radius: 20px;
    color: #ffffff;
    font-size: 18px;
}
.definition-b {
    p {
        margin: 0;
    }
}
```


static/css/hotbar.css
```css
.hotbar {
    display: flex;
    flex-direction: row;
    align-items: center;
    justify-content: center;
    width: 100%;
    height: 50px;
    background-color: #2d2d2d;
}

.hotbar-btn {
    width: 30px;
    /* height: 50px; */
    /* margin: 10px; */
    padding: 10px;
    aspect-ratio: 1;
    transition: scale 0.1s;
    cursor: pointer;
}
.hotbar-btn:hover {
    transform: scale(1.05);
}
.hotbar-btn:active {
    transform: scale(0.95);
}

```


static/js/course_render_manager.js
```js
function renderCourse(courseData) {
    const container = $('.course-content-container');
    container.empty(); // Очищаем предыдущее содержимое

    // Настройка marked для использования Prism
    marked.setOptions({
        highlight: function(code, lang) {
            if (Prism.languages[lang]) {
                return Prism.highlight(code, Prism.languages[lang], lang);
            } else {
                return code;
            }
        }
    });

    container.append(`
        <div class="courseTitle">${courseData.courseTitle}</div>
        <div class="courseDescription">${marked.parse(courseData.courseDescription)}</div>
        <div class="sections"></div>
    `);

    const sectionsContainer = container.find('.sections');

    courseData.sections.forEach(section => {
        const sectionElement = $(`
            <div class="section-container">
                <div class="sectionTitle">${section.sectionTitle}</div>
                <div class="topics"></div>
            </div>
        `);

        const topicsContainer = sectionElement.find('.topics');

        section.topics.forEach(topic => {
            const topicElement = $(`
                <div class="topic-container">
                    <div class="topicTitle">
                        <img class="course-list-icon" src="../static/images/topic-icon.webp">
                        ${topic.topicTitle}
                    </div>
                    <div class="subtopics"></div>
                </div>
            `);

            const subtopicsContainer = topicElement.find('.subtopics');

            topic.subtopics.forEach(subtopic => {
                const subtopicElement = $(`
                    <div class="subtopic-container">
                        <div class="title">
                            <img class="course-list-icon" src="../static/images/subtopic-icon.webp">
                            ${subtopic.title}
                        </div>
                        <div class="content">${marked.parse(subtopic.content)}</div>
                    </div>
                `);

                // Проверяем, есть ли definitions и не пустой ли объект
                if (subtopic.definitions && Object.keys(subtopic.definitions).length > 0) {
                    const definitionsContainer = $('<div class="definitions"></div>');

                    for (const [term, definition] of Object.entries(subtopic.definitions)) {
                        definitionsContainer.append(`
                            <div class="definition-container">
                                <div class="definition-a">${term}</div>
                                <div class="definition-b">${marked.parse(definition)}</div>
                            </div>
                        `);
                    }

                    subtopicElement.append(definitionsContainer);
                }

                subtopicsContainer.append(subtopicElement);
            });

            topicsContainer.append(topicElement);
        });

        sectionsContainer.append(sectionElement);
    });

    // Применяем подсветку синтаксиса после рендеринга
    Prism.highlightAll();

    // Добавляем кнопки копирования после рендеринга контента
    addCopyButtons();
}

function addCopyButtons() {
    // Находим все блоки кода
    const codeBlocks = document.querySelectorAll('pre > code');

    codeBlocks.forEach((codeBlock) => {
        // Создаем кнопку
        const copyButton = document.createElement('button');
        copyButton.className = 'copy-button';
        copyButton.textContent = 'Копировать';

        // Добавляем кнопку рядом с блоком кода
        codeBlock.parentNode.insertBefore(copyButton, codeBlock);

        // Добавляем обработчик события для копирования
        copyButton.addEventListener('click', () => {
            const code = codeBlock.textContent;
            navigator.clipboard.writeText(code).then(() => {
                copyButton.textContent = 'Скопировано!';
                setTimeout(() => {
                    copyButton.textContent = 'Копировать';
                }, 2000);
            }).catch(err => {
                console.error('Ошибка при копировании: ', err);
            });
        });
    });
}

function getCourseFromId(courseId) {
    $.ajax({
        url: `/get_course/${courseId}`,
        method: 'GET',
        success: function(response) {
            renderCourse(response);
        },
        error: function(xhr, status, error) {
            console.error("Ошибка при получении курса:", error);
        }
    });
}

$(document).ready(function() {
    getCourseFromId("test")
});

```


templates/index.html
```html
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>DarkHub</title>
    <link rel="preconnect" href="https://fonts.googleapis.com">
    <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
    <link href="https://fonts.googleapis.com/css2?family=Inter:ital,opsz,wght@0,14..32,100..900;1,14..32,100..900&family=Roboto+Slab:wght@100..900&family=Roboto:ital,wght@0,100;0,300;0,400;0,500;0,700;0,900;1,100;1,300;1,400;1,500;1,700;1,900&display=swap" rel="stylesheet">
    <script src="https://ajax.googleapis.com/ajax/libs/jquery/3.6.4/jquery.min.js"></script>
    <script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>

    <script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.24.1/prism.min.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.24.1/plugins/autoloader/prism-autoloader.min.js"></script>
    <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/prism/1.24.1/themes/prism-tomorrow.min.css">

    <link rel="stylesheet" href="../static/css/base.css">
    <link rel="stylesheet" href="../static/css/hotbar.css">
    <link rel="stylesheet" href="../static/css/course-content.css">
</head>
<body>
<script src="../static/js/main.js"></script>
<div class="screen">
    {% include "components/hotbar.html" %}
    <div class="screen-content">
        <div class="courses-container">

            <div class="course-tile">
                <div class="course-title">Test course</div>
                <img class="course-background" src="../static/images/javascript.png">
            </div>

        </div>
        <div class="course-content-container">
            <div class="courseTitle">Тестовый курс</div>
            <div class="courseDescription">Это тестовый курс для теста</div>
            <div class="sections">
                <div class="section-container">
                    <div class="sectionTitle">Основы JavaScript</div>
                    <div class="topics">
                        <div class="topic-container">
                            <div class="topicTitle">
                                <img class="course-list-icon" src="../static/images/topic-icon.webp">
                                Введение в JavaScript
                            </div>
                            <div class="subtopics">
                                <div class="subtopic-container">
                                    <div class="title">
                                        <img class="course-list-icon" src="../static/images/subtopic-icon.webp">
                                        Что такое JavaScript?
                                    </div>
                                    <div class="content">Это контент сабтопика</div>
                                    <div class="definitions">
                                        <div class="definition-container">
                                            <div class="definition-a">JavaScript</div>
                                            <div class="definition-b">Язык программирования, добавляющий интерактивность веб-страницам.</div>
                                        </div>
                                        <div class="definition-container">
                                            <div class="definition-a">HTML</div>
                                            <div class="definition-b">Язык разметки гипертекста, используемый для создания структуры веб-страниц.</div>
                                        </div>
                                        <div class="definition-container">
                                            <div class="definition-a">CSS</div>
                                            <div class="definition-b">Каскадные таблицы стилей, используемые для стилизации веб-страниц.</div>
                                        </div>
                                    </div>
                                </div>
                                <div class="subtopic-container">
                                    <div class="title">
                                        <img class="course-list-icon" src="../static/images/subtopic-icon.webp">
                                        Подключение JavaScript к HTML
                                    </div>
                                    <div class="content">Это контент сабтопика</div>
                                </div>
                            </div>
                        </div>
                    </div>
                </div>
            </div>
        </div>
    </div>
</div>
</body>
<script src="../static/js/course_render_manager.js"></script>
</html>
```


templates/components/hotbar.html
```html
<div class="hotbar">
    <img class="hotbar-btn" src="../../static/images/h_icon.webp">
</div>
```


Вот мой проект.
Сделай так, чтобы содержание курса плавно появяллось при прокручивании.
анимация должна быть такой:
Контейнер чуть спущен вниз, уменьшен и прозрачен
Как только мы до него долистываем, то он поднимается обратно, возвращает размер и становится видимым по opacity.