/* ==========================================================================
   Module System - Shared "learning module" page components (Sotis AI)
   --------------------------------------------------------------------------
   Single source of truth for the module LIST + DETAIL pages shared by every
   module-style app (app6 mlformation, app8 maths, future ones). Apps link
   this file in their {% block extra_css %} BEFORE their own app CSS, then add
   only app-specific rules on top.

   Canonical values come from app6 mlformation (the reference design). Do NOT
   re-implement these components inside an app stylesheet — extend or override
   here so the design stays identical across apps by construction.
   ========================================================================== */

/* ==========================================================================
   Sync YAML button (fixed, bottom-right) + result toast
   ========================================================================== */
.sync-btn {
    position: fixed;
    bottom: 2rem;
    right: 2rem;
    padding: 0.5rem 1rem;
    background: var(--bg-secondary);
    color: var(--violet-dark);
    border: 1px solid var(--violet-dark);
    border-radius: var(--radius-sm);
    cursor: pointer;
    font-size: 0.85rem;
    transition: all var(--transition-fast);
    box-shadow: var(--shadow-md);
    z-index: 100;
    opacity: 0.7;
}

.sync-btn:hover {
    background: var(--violet-dark);
    color: white;
    opacity: 1;
}

.sync-btn:disabled {
    opacity: 0.3;
    cursor: not-allowed;
}

.sync-result {
    position: fixed;
    bottom: 4.5rem;
    right: 2rem;
    padding: 0.5rem 1rem;
    border-radius: var(--radius-sm);
    font-size: 0.85rem;
    display: none;
    z-index: 100;
    box-shadow: var(--shadow-md);
}

.sync-result.success {
    display: block;
    background: rgba(76, 175, 80, 0.1);
    color: #4caf50;
    border: 1px solid rgba(76, 175, 80, 0.3);
}

.sync-result.error {
    display: block;
    background: rgba(244, 67, 54, 0.1);
    color: #f44336;
    border: 1px solid rgba(244, 67, 54, 0.3);
}

/* ==========================================================================
   Filters (list page)
   ========================================================================== */
.filters-section {
    display: flex;
    flex-direction: column;
    gap: var(--space-sm);
    margin-bottom: var(--space-lg);
    padding: var(--space-md);
    background: var(--card-bg);
    border-radius: var(--radius-md);
    border: 1px solid var(--border-color);
    max-width: 100%;
    overflow-x: hidden;
}

.filter-group {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: var(--space-xs);
    max-width: 100%;
}

.filter-label {
    font-weight: 600;
    color: var(--text-secondary);
    margin-right: var(--space-xs);
    min-width: 80px;
}

.filter-btn {
    display: inline-flex;
    align-items: center;
    gap: 0.45rem;
    padding: 0.4rem 0.8rem;
    border: 1px solid var(--border-color);
    border-radius: var(--radius-full);
    background: var(--bg-secondary);
    color: var(--text-primary);
    font-size: 0.85rem;
    line-height: 1.4;
    cursor: pointer;
    transition: var(--transition-fast);
    max-width: 100%;
    overflow: hidden;
    white-space: nowrap;
}

/* The category filters carry a Lucide icon, which ships at its own default size
   (24px) unless a stylesheet says otherwise. Nothing did, so the glyph rendered
   at nearly twice the 0.85rem label and, the button not being a flex container,
   sat on the text baseline rather than on its optical centre. Size it in `em` so
   it follows the label down at the 480px breakpoint instead of staying 24px. */
.filter-btn [data-lucide],
.filter-btn svg {
    width: 1.05em;
    height: 1.05em;
    flex-shrink: 0;
    stroke-width: 1.75;
}

.filter-btn:hover {
    background: var(--violet-wash);
    border-color: var(--violet-primary);
}

/* Same reasoning as `.btn-unlock` further down, and the same fix: --violet-fill
   is the only violet dark enough to carry white ink at AA. The muted violet
   left this 0.85rem label, which is normal-size text, under the 4.5:1 floor.
   Hover feedback goes through elevation because there is no violet to step
   down to, and because `:hover` is declared before `.active` at equal
   specificity, so the wash never applied to a selected filter anyway. */
.filter-btn.active {
    background: var(--violet-fill);
    color: var(--white);
    border-color: var(--violet-fill);
}

.filter-btn.active:hover {
    background: var(--violet-fill);
    border-color: var(--violet-fill);
    box-shadow: var(--shadow-sm), var(--ring-inset);
}

/* The list templates put `difficulty-*` on the level filters as well as on the
   card badges, so the same class was asked to mean two opposite things: a
   passive status label and a control. At rest the status fill won (it is
   declared later than `.filter-btn`), so the level row read as three coloured
   pills next to a row of neutral category pills, and the jump to the `.active`
   fill was a second, unrelated colour change. Here the level filters follow the
   filter grammar and keep the semantic hint as a 6px dot. */
.filter-btn.difficulty-beginner,
.filter-btn.difficulty-intermediate,
.filter-btn.difficulty-advanced {
    background: var(--bg-secondary);
    color: var(--text-primary);
    border-color: var(--border-color);
    font-weight: var(--font-weight-normal);
}

.filter-btn.difficulty-beginner::before,
.filter-btn.difficulty-intermediate::before,
.filter-btn.difficulty-advanced::before {
    content: '';
    display: block;
    width: 6px;
    height: 6px;
    flex-shrink: 0;
    border-radius: var(--radius-full);
}

.filter-btn.difficulty-beginner::before     { background: var(--success); }
.filter-btn.difficulty-intermediate::before { background: var(--warning); }
.filter-btn.difficulty-advanced::before     { background: var(--danger); }

.filter-btn.difficulty-beginner:hover,
.filter-btn.difficulty-intermediate:hover,
.filter-btn.difficulty-advanced:hover {
    background: var(--violet-wash);
    border-color: var(--violet-primary);
}

/* Declared after `.filter-btn.active`, so it wins on the level row: fixing the
   contrast above without fixing it here would have left half the filter bar
   below AA, and the two rows sit side by side. */
.filter-btn.difficulty-beginner.active,
.filter-btn.difficulty-intermediate.active,
.filter-btn.difficulty-advanced.active,
.filter-btn.difficulty-beginner.active:hover,
.filter-btn.difficulty-intermediate.active:hover,
.filter-btn.difficulty-advanced.active:hover {
    background: var(--violet-fill);
    color: var(--white);
    border-color: var(--violet-fill);
}

.filter-btn.difficulty-beginner.active:hover,
.filter-btn.difficulty-intermediate.active:hover,
.filter-btn.difficulty-advanced.active:hover {
    box-shadow: var(--shadow-sm), var(--ring-inset);
}

/* ==========================================================================
   Modules grid + cards (list page)
   ========================================================================== */
/* auto-fill, not auto-fit: app8 ships a single module and auto-fit would
   stretch that one card across the whole row. */
.modules-grid {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
    gap: var(--space-sm);
}

.module-card {
    display: flex;
    flex-direction: column;
    gap: 0.45rem;
    padding: var(--space-sm);
    background: var(--card-bg);
    border: 1px solid var(--border-color);
    border-radius: var(--radius-md);
    text-decoration: none;
    color: var(--text-primary);
    transition: var(--transition-base);
}

/* The hover changes the hairline, not the elevation: seven rows of cards
   lifting off the page read as noise. */
.module-card:hover {
    border-color: var(--violet-primary);
    box-shadow: var(--shadow-hover);
}

.module-card-header {
    display: flex;
    justify-content: space-between;
    align-items: flex-start;
    gap: var(--space-xs);
    margin-bottom: 0;
}

.module-categories {
    display: flex;
    flex-wrap: wrap;
    gap: 0.25rem 0.75rem;
}

/* --text-secondary, not --text-tertiary: the category, the duration and the
   section count are the only structured facts on the card and users filter on
   them, so they are content rather than the "decorative metadata ONLY" the
   tertiary ink is documented for (3.6:1, below AA at this size). The monospace
   and the --text-xs step already put them a rank below the title. */
.module-category {
    font-family: var(--font-mono);
    font-size: var(--text-xs);
    color: var(--text-secondary);
}

/* Same unsized-Lucide problem as `.filter-btn`, worse here: the label is
   --text-xs, so the 24px default was twice its size. Deliberately NOT a flex
   container though: this label wraps onto two lines on a narrow card, and flex
   would centre the icon against the whole two-line block instead of leaving it
   on the first line. Inline flow + `vertical-align` keeps it beside the first
   word, which is what wrapping text needs. */
.module-category [data-lucide],
.module-category svg {
    width: 1.15em;
    height: 1.15em;
    vertical-align: -0.22em;
    stroke-width: 1.75;
}

/* Ink, not colour: hierarchy inside the card comes from size and weight.
   A grid of violet titles spends the accent colour on every row and leaves
   nothing for the hover state to say. */
.module-title {
    margin: 0;
    font-size: var(--text-lg);
    font-weight: var(--font-weight-medium);
    color: var(--text-primary);
}

/* No line clamp here. The card is a column flex container, so a flex item's
   display value is blockified: `display: -webkit-box` computes to `flow-root`
   and `-webkit-line-clamp` never applies. All that survived was
   `overflow: hidden` on a box the flex layout had already sized, which cut the
   last line through the middle of the glyphs. The length cap belongs upstream
   anyway: both render paths truncate the description before it reaches the
   card. */
.module-description {
    flex: 1;
    color: var(--text-secondary);
    font-size: var(--text-sm);
    line-height: var(--leading-snug);
    margin-bottom: 0;
}

.module-meta {
    display: flex;
    gap: var(--space-sm);
    font-family: var(--font-mono);
    font-size: var(--text-xs);
    color: var(--text-secondary);
    padding-top: 0.6rem;
    border-top: 1px solid var(--border-color);
}

.no-modules {
    grid-column: 1 / -1;
    text-align: center;
    padding: var(--space-lg);
    color: var(--text-secondary);
}

/* ==========================================================================
   Difficulty badges (solid light fills, legible on the violet header too)
   ========================================================================== */
.module-difficulty,
.difficulty-beginner,
.difficulty-intermediate,
.difficulty-advanced {
    padding: 0.2rem 0.6rem;
    border-radius: var(--radius-full);
    font-size: 0.75rem;
    font-weight: 600;
}

.difficulty-beginner {
    background: var(--success-bg);
    color: var(--success-text);
    border: 1px solid rgba(var(--success-rgb), 0.28);
}

.difficulty-intermediate {
    background: var(--warning-bg);
    color: var(--warning-text);
    border: 1px solid rgba(var(--warning-rgb), 0.28);
}

.difficulty-advanced {
    background: var(--danger-bg);
    color: var(--danger-text);
    border: 1px solid rgba(var(--danger-rgb), 0.28);
}

/* ==========================================================================
   Module detail header
   ==========================================================================
   Was a solid --violet-dark card carrying white text. On a near-black canvas
   that was the single most saturated surface on the site, at the top of every
   module page in two apps. It now matches the .page-header used everywhere
   else: left aligned, hairline rule, ink on the canvas.

   The navbar rule in style.css used to be a bare `header {}` and reached this
   element; it is now scoped to `body > header`. The explicit position/box-shadow
   resets below are kept as belt and braces, they no longer undo anything. */
.module-header {
    margin-bottom: var(--space-lg);
    padding: 0 0 var(--space-sm);
    background: none;
    border-bottom: 1px solid var(--border-color);
    border-radius: 0;
    position: static;
    box-shadow: none;
}

.module-header h1 {
    font-size: var(--text-3xl);
    color: var(--text-primary);
    letter-spacing: var(--tracking-tighter);
    margin-bottom: 0.4rem;
    line-height: var(--leading-tight);
}

.module-header .module-description {
    color: var(--text-secondary);
    font-size: var(--text-md);
    margin-bottom: var(--space-sm);
    max-width: 68ch;
}

.module-breadcrumb {
    font-family: var(--font-mono);
    font-size: var(--text-xs);
    color: var(--text-tertiary);
    margin-bottom: var(--space-sm);
}

.module-breadcrumb a {
    color: var(--text-secondary);
    text-decoration: none;
}

.module-breadcrumb a:hover {
    color: var(--text-primary);
    text-decoration: underline;
}

.module-breadcrumb span {
    color: var(--text-tertiary);
}

.module-meta-header {
    display: flex;
    flex-wrap: wrap;
    gap: var(--space-sm);
    margin: var(--space-sm) 0;
    align-items: center;
}

.module-duration,
.module-sections-count {
    font-family: var(--font-mono);
    font-size: var(--text-xs);
    color: var(--text-tertiary);
}

/* ==========================================================================
   Learning objectives & prerequisites
   ========================================================================== */
.learning-objectives,
.prerequisites {
    background: var(--card-bg);
    padding: var(--space-md);
    border-radius: var(--radius-md);
    margin-bottom: var(--space-md);
    border: 1px solid var(--border-color);
}

.learning-objectives h2,
.prerequisites h2 {
    font-size: 1.1rem;
    color: var(--violet-dark);
    margin-bottom: var(--space-sm);
}

/* ==========================================================================
   Notebook cells (section framework)
   ========================================================================== */
.notebook-cells {
    display: flex;
    flex-direction: column;
    gap: var(--space-md);
}

.cell {
    background: var(--card-bg);
    border: 1px solid var(--border-color);
    border-radius: var(--radius-md);
    overflow: hidden;
    border-left-width: 2px;
    border-left-style: solid;
    border-left-color: var(--border-color);
}

/* A vertical rule, the system's accent pattern, so a long page of cells is
   scannable by type without adding a single filled panel. The colour matches
   the badge each type already carries, in both apps. */
.cell-theory        { border-left-color: var(--border-strong); }
.cell-code          { border-left-color: var(--violet-fill); }
.cell-exercise      { border-left-color: var(--warning); }
.cell-advanced      { border-left-color: var(--border-strong); }
.cell-formula       { border-left-color: var(--warning); }
.cell-visualization { border-left-color: var(--info-text); }
.cell-quiz          { border-left-color: var(--success); }

/* --surface-raised, not --bg-secondary: the latter has the same value as
   --card-bg, so the header was literally invisible and every cell type looked
   alike. */
.cell-header {
    display: flex;
    align-items: center;
    gap: var(--space-sm);
    padding: var(--space-sm) var(--space-md);
    background: var(--surface-raised);
    border-bottom: 1px solid var(--border-color);
}

.cell-header h3 {
    margin: 0;
    font-size: var(--text-base);
    font-weight: var(--font-weight-medium);
    color: var(--text-primary);
}

.cell-content {
    padding: var(--space-md);
    line-height: 1.7;
}

/* Cell-type badges — union of all module apps' section types.
   Translucent status fills + light ink, so a badge sits on any surface. */
.cell-type-badge {
    padding: 0.2rem 0.6rem;
    border-radius: var(--radius-sm);
    font-family: var(--font-mono);
    font-size: var(--text-xs);
    font-weight: var(--font-weight-medium);
    text-transform: uppercase;
    letter-spacing: var(--tracking-wide);
}

.cell-type-badge.theory        { background: var(--violet-wash); color: var(--violet-primary); }
.cell-type-badge.code          { background: var(--info-bg); color: var(--info-text); }
.cell-type-badge.exercise      { background: var(--warning-bg); color: var(--warning-text); }
.cell-type-badge.advanced      { background: rgba(167, 139, 250, 0.14); color: #C4B5FD; }
.cell-type-badge.formula       { background: var(--warning-bg); color: var(--warning-text); }
.cell-type-badge.visualization { background: var(--info-bg); color: var(--info-text); }
.cell-type-badge.quiz          { background: var(--success-bg); color: var(--success-text); }
.cell-type-badge.locked        { background: rgba(158, 158, 158, 0.2); color: var(--text-secondary); }

/* ==========================================================================
   The is_public lock (shared by every module app)
   --------------------------------------------------------------------------
   Canonical source: it used to live in mlformation.css, with a second, cruder
   copy in maths.css whose unlock button put white ink on --violet-dark (3.5:1,
   under AA) on the only call to action of a locked page. Two hand-synced
   copies drift; this one cannot. Markup: templates/includes/_locked_cell.html.
   ========================================================================== */
/* One accent per zone. This block used to stack five: a 2px brand border, an
   animated brand-to-spark bar, a gradient header, a solid brand badge and a
   solid brand button. Only the button survives, because it is the action. */
.cell-locked {
    border: 1px solid var(--violet-dark);
    background: var(--card-bg);
    position: relative;
    overflow: hidden;
    margin-top: var(--space-md);
}

.cell-locked .cell-header {
    justify-content: space-between;
}

.cell-type-badge.locked {
    display: flex;
    align-items: center;
    gap: 0.5rem;
}

.cell-type-badge.locked svg {
    stroke: currentColor;
}

.locked-content {
    display: flex;
    align-items: center;
    gap: var(--space-lg);
    padding: var(--space-lg);
}

/* Progress Ring */
.progress-ring-container {
    position: relative;
    width: 120px;
    height: 120px;
    flex-shrink: 0;
}

.progress-ring {
    width: 100%;
    height: 100%;
    transform: rotate(-90deg);
}

.progress-ring-bg {
    fill: none;
    stroke: var(--border-color);
    stroke-width: 8;
}

.progress-ring-fill {
    fill: none;
    stroke: var(--violet-primary);
    stroke-width: 8;
    stroke-linecap: round;
    transition: stroke-dashoffset 0.5s ease;
}

.progress-ring-text {
    position: absolute;
    top: 50%;
    left: 50%;
    transform: translate(-50%, -50%);
    text-align: center;
    line-height: 1;
}

.progress-current {
    font-family: var(--font-mono);
    font-size: 1.75rem;
    font-weight: var(--font-weight-bold);
    color: var(--text-primary);
}

.progress-separator {
    font-size: 1rem;
    color: var(--text-secondary);
    margin: 0 2px;
}

.progress-total {
    font-family: var(--font-mono);
    font-size: 1.25rem;
    font-weight: 500;
    color: var(--text-secondary);
}

/* Message */
.locked-message {
    flex: 1;
}

.locked-message h3 {
    font-size: 1.35rem;
    color: var(--text-primary);
    margin-bottom: var(--space-xs);
    font-weight: 600;
}

.locked-message p {
    color: var(--text-secondary);
    line-height: 1.6;
    margin-bottom: var(--space-md);
    font-size: 0.95rem;
}

.locked-message p strong {
    color: var(--text-primary);
}

/* Actions */
.locked-actions {
    display: flex;
    align-items: center;
    gap: var(--space-sm);
    flex-wrap: wrap;
}

/* --violet-fill is the only violet dark enough to carry white text, and this
   is the one accent the block keeps. */
.btn-unlock {
    display: inline-flex;
    align-items: center;
    gap: 0.5rem;
    padding: 0.75rem 1.5rem;
    background: var(--violet-fill);
    color: var(--white);
    border-radius: var(--radius-sm);
    text-decoration: none;
    font-weight: 600;
    font-size: 0.95rem;
    transition: var(--transition-base);
    box-shadow: var(--shadow-sm);
}

/* Feedback through elevation, not through the fill. Repeating --violet-fill
   verbatim left the only call to action on the page with no hover state worth
   the name, but --violet-dark (the .btn-violet hover in style.css) drops white
   ink from 4.8:1 to 3.5:1, and there is no violet darker than --violet-fill to
   step down to. */
.btn-unlock:hover {
    background: var(--violet-fill);
    box-shadow: var(--shadow-md), var(--ring-inset);
    color: var(--white);
}

.btn-unlock svg {
    stroke: currentColor;
}

.or-separator {
    color: var(--text-secondary);
    font-size: 0.9rem;
}

.btn-register-link {
    color: var(--text-secondary);
    font-weight: 500;
    font-size: 0.95rem;
    transition: var(--transition-fast);
    border-bottom: none;
    text-decoration: underline;
    text-underline-offset: 3px;
    padding-bottom: 2px;
}

.btn-register-link:hover {
    color: var(--text-primary);
}

/* Mobile responsiveness */
@media (max-width: 768px) {
    .locked-content {
        flex-direction: column;
        text-align: center;
        padding: var(--space-md);
    }

    .progress-ring-container {
        width: 100px;
        height: 100px;
    }

    .progress-current {
        font-size: 1.5rem;
    }

    .progress-total {
        font-size: 1rem;
    }

    .locked-message h3 {
        font-size: 1.2rem;
    }

    .locked-actions {
        justify-content: center;
        flex-direction: column;
        gap: var(--space-xs);
    }

    .btn-unlock {
        width: 100%;
        justify-content: center;
    }

    .or-separator {
        display: none;
    }

    .btn-register-link {
        margin-top: var(--space-xs);
    }
}

/* ==========================================================================
   Markdown content (rendered by each app's render_markdown filter)
   ========================================================================== */
.markdown-content {
    max-width: 100%;
    overflow-wrap: break-word;
    word-wrap: break-word;
}

.markdown-content p {
    margin: 0 0 1em;
    color: var(--text-primary);
}

.markdown-content p:last-child {
    margin-bottom: 0;
}

/* Subheaders (lines ending with :) */
.markdown-content .md-subheader {
    font-weight: 600;
    color: var(--violet-dark);
    margin: 1.5em 0 0.5em;
    font-size: 1.05em;
    border-bottom: 1px solid var(--border-color);
    padding-bottom: 0.3em;
}

.markdown-content .md-subheader:first-child {
    margin-top: 0;
}

/* ATX headings (# to ######, rendered h2..h6). Primary ink, not violet: the
   overview panel of app6 renders its own `h2` in primary ink right next to
   markdown headings, so a coloured heading here gave one panel two heading
   grammars. Headings earn their rank from size and tracking. */
.markdown-content .md-heading {
    color: var(--text-primary);
    font-weight: 600;
    margin: 1.5em 0 0.6em;
}

.markdown-content .md-heading:first-child {
    margin-top: 0;
}

.markdown-content .md-h2 {
    font-size: 1.35em;
    border-bottom: 1px solid var(--border-color);
    padding-bottom: 0.3em;
}

.markdown-content .md-h3 {
    font-size: 1.2em;
}

.markdown-content .md-h4 {
    font-size: 1.1em;
}

.markdown-content .md-h5,
.markdown-content .md-h6 {
    font-size: 1em;
}

/* Bold text */
.markdown-content strong {
    color: var(--text-primary);
    font-weight: 600;
}

/* Lists */
.markdown-content .md-list {
    margin: 0.5em 0 1em;
    padding-left: 1.5em;
}

.markdown-content .md-list li {
    margin: 0.4em 0;
    color: var(--text-primary);
    position: relative;
}

.markdown-content ul.md-list {
    list-style: none;
}

.markdown-content ul.md-list li::before {
    content: '';
    position: absolute;
    left: -1.2em;
    top: 0.55em;
    width: 6px;
    height: 6px;
    background: var(--violet-dark);
    border-radius: 50%;
}

.markdown-content ol.md-list {
    list-style: decimal;
}

.markdown-content ol.md-list li::marker {
    color: var(--violet-dark);
    font-weight: 600;
}

/* Tables - responsive wrapper */
.markdown-content .md-table-wrapper {
    width: 100%;
    overflow-x: auto;
    margin: 1em 0;
    -webkit-overflow-scrolling: touch;
}

.markdown-content .md-table {
    width: 100%;
    border-collapse: collapse;
    font-size: var(--text-sm);
    margin: 0;
}

/* Horizontal rules only. Boxing every cell turned a dense table into a grid of
   little frames; the row separator is enough to track a line. */
.markdown-content .md-table th,
.markdown-content .md-table td {
    padding: 0.55em 0.9em;
    text-align: left;
    border: none;
    border-bottom: 1px solid var(--border-color);
}

/* Monospace small caps: the system's technical signage, and it replaces the
   most saturated surface left inside body copy. */
.markdown-content .md-table th {
    background: var(--surface-raised);
    color: var(--text-secondary);
    font-family: var(--font-mono);
    font-size: var(--text-xs);
    font-weight: var(--font-weight-medium);
    text-transform: uppercase;
    letter-spacing: var(--tracking-wide);
    border-bottom: 1px solid var(--border-strong);
}

.markdown-content .md-table tbody tr:nth-child(even) {
    background: rgba(255, 255, 255, 0.03);
}

.markdown-content .md-table tbody tr:hover {
    background: var(--violet-wash);
}



/* Horizontal separator */
.markdown-content .md-separator {
    border: none;
    height: 1px;
    background: linear-gradient(90deg, transparent, var(--violet-primary), transparent);
    margin: var(--space-md) 0;
    opacity: 0.6;
}

/* Inline code */
.markdown-content .md-inline-code {
    background: #2d2d2d;
    border: 1px solid var(--border-color);
    padding: 0.15em 0.4em;
    border-radius: 4px;
    font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', 'Consolas', monospace;
    font-size: 0.9em;
    color: var(--violet-dark);
    border-color: #444;
}


/* Code blocks */
.markdown-content .md-code-block {
    background: #1e1e1e;
    border: 1px solid #3a3a3a;
    border-radius: var(--radius-sm);
    padding: 1em 1.5em;
    margin: 1em 0;
    overflow-x: auto;
    font-family: 'JetBrains Mono', 'Fira Code', 'Monaco', 'Menlo', 'Consolas', monospace;
    font-size: 0.9em;
    line-height: 1.5;
    color: #d4d4d4;
    white-space: pre;
    tab-size: 4;
}

.markdown-content .md-code-block code {
    background: transparent;
    border: none;
    padding: 0;
    font-family: inherit;
    font-size: inherit;
    line-height: inherit;
    color: inherit;
    white-space: pre;
    display: block;
}

/* KaTeX math formulas */
.markdown-content .katex-display {
    margin: 1em 0;
    padding: 0.5em;
    background: var(--bg-secondary);
    border-radius: var(--radius-sm);
    overflow-x: auto;
}

.markdown-content .katex {
    font-size: 1.1em;
}

/* ==========================================================================
   Mermaid diagram palette (semantic node classes)
   ==========================================================================
   Mermaid takes colors as literal strings and cannot read CSS custom
   properties, so diagrams used to repeat brand hex values on every
   `style <id> fill:#...,color:#...` line inside the YAML content. Diagrams now
   only tag a node with a meaning:

       flowchart TD
           A["Donnees"] --> B["Modele"]
           class A ml-node-secondary
           class B ml-node-main

   and the palette lives here, expressed in design tokens. The vocabulary
   covers the meanings the diagrams actually carry: main = the pivotal node,
   brand/secondary = supporting steps, accent(-soft) = the highlighted result,
   success/warning/danger = the outcome of a branch, muted = discarded,
   plain = a bare connector glyph, path(-end) = the branch a worked example
   actually walks through.

   Three implementation notes:

   1. `!important` is not decorative. Mermaid injects
      `#mermaid-<id> .node rect { fill: ... }` into every rendered SVG; an id
      selector outranks anything an external stylesheet can write.
   2. Only fill and label color are set, which is exactly what the former
      inline `style` directives did. Node stroke deliberately stays on the
      Mermaid theme default, except for `ml-node-path-end` (see below), which
      replaces a directive that did carry a stroke.
   3. Both variables carry a fallback. A misspelled class (`ml-node-succes`)
      leaves them undefined, which makes the whole declaration invalid and lets
      the value inherit: the label then renders in the colour of its own
      background, in both themes, with nothing in the console. The fallback is
      a deliberately off-palette magenta so the mistake is impossible to miss
      while the label stays readable.

   Every token used below is theme-invariant, so a diagram renders identically
   in light and dark mode, as it did when the hex values were inline. */

.mermaid g[class*="ml-node-"] > rect,
.mermaid g[class*="ml-node-"] > circle,
.mermaid g[class*="ml-node-"] > ellipse,
.mermaid g[class*="ml-node-"] > polygon,
.mermaid g[class*="ml-node-"] > path {
    fill: var(--ml-node-fill, #FF00FF) !important;
}

.mermaid g[class*="ml-node-"] .nodeLabel,
.mermaid g[class*="ml-node-"] .cluster-label,
.mermaid g[class*="ml-node-"] span,
.mermaid g[class*="ml-node-"] p {
    color: var(--ml-node-ink, #1A1A1A) !important;
}

/* htmlLabels: false renders labels as SVG text, which takes fill, not color. */
.mermaid g[class*="ml-node-"] text,
.mermaid g[class*="ml-node-"] tspan {
    fill: var(--ml-node-ink, #1A1A1A) !important;
}

.mermaid g.ml-node-main {
    --ml-node-fill: var(--violet-dark);
    --ml-node-ink: var(--white);
}

.mermaid g.ml-node-brand {
    --ml-node-fill: var(--violet-primary);
    --ml-node-ink: var(--dark-text);
}

.mermaid g.ml-node-secondary {
    --ml-node-fill: var(--violet-light);
    --ml-node-ink: var(--dark-text);
}

.mermaid g.ml-node-accent {
    --ml-node-fill: var(--yellow-primary);
    --ml-node-ink: var(--dark-text);
}

/* Not --yellow-light: that one flips to a dark olive in dark mode and would
   leave --dark-text unreadable on it. --yellow-light-fixed holds the same pale
   value in both themes, which is what a node carrying dark ink needs. */
.mermaid g.ml-node-accent-soft {
    --ml-node-fill: var(--yellow-light-fixed);
    --ml-node-ink: var(--dark-text);
}

/* The three status classes pair a translucent fill with light ink. They used to
   pair the same fill with --dark-text, which worked while the status
   backgrounds were pale solids; once they became translucent veils over a
   near-black canvas, the labels became dark ink on a dark node and stopped
   being readable. The fill is raised to 0.30 so the node still reads as
   coloured through the veil. */
.mermaid g.ml-node-success {
    --ml-node-fill: rgba(var(--success-rgb), 0.3);
    --ml-node-ink: var(--text-primary);
}

.mermaid g.ml-node-warning {
    --ml-node-fill: rgba(var(--warning-rgb), 0.3);
    --ml-node-ink: var(--text-primary);
}

.mermaid g.ml-node-danger {
    --ml-node-fill: rgba(var(--danger-rgb), 0.3);
    --ml-node-ink: var(--text-primary);
}

.mermaid g.ml-node-muted {
    --ml-node-fill: var(--gray-medium);
    --ml-node-ink: var(--white);
}

.mermaid g.ml-node-plain {
    --ml-node-fill: var(--white);
    --ml-node-ink: var(--dark-text);
}

/* Nodes a worked example actually walks through, as opposed to the branches it
   only mentions. Two variants because the diagrams distinguish a step on the
   path from where the path lands, and the terminal node reads the same orange
   with lighter ink. */
.mermaid g.ml-node-path {
    --ml-node-fill: var(--viz-highlight);
    --ml-node-ink: var(--dark-text);
}

.mermaid g.ml-node-path-end {
    --ml-node-fill: var(--viz-highlight);
    --ml-node-ink: var(--white);
}

/* The one class that touches the stroke: the thick contour is what marked the
   end of the traversal before the conversion, so it has to come along. Every
   other class leaves the stroke on the Mermaid theme default. */
.mermaid g.ml-node-path-end > rect,
.mermaid g.ml-node-path-end > circle,
.mermaid g.ml-node-path-end > ellipse,
.mermaid g.ml-node-path-end > polygon,
.mermaid g.ml-node-path-end > path {
    stroke: var(--viz-highlight-stroke) !important;
    stroke-width: 3px !important;
}

/* ==========================================================================
   Bottom navigation (back link)
   ========================================================================== */
.module-nav {
    margin-top: var(--space-lg);
    padding-top: var(--space-md);
    border-top: 1px solid var(--border-color);
}

.nav-back {
    display: inline-flex;
    align-items: center;
    gap: var(--space-xs);
    color: var(--violet-dark);
    text-decoration: none;
    font-weight: 500;
}

.nav-back:hover {
    text-decoration: underline;
}

.nav-back::before {
    content: '<-';
}

/* ==========================================================================
   Responsive
   ========================================================================== */
@media (max-width: 768px) {
    .module-header {
        padding: var(--space-sm);
    }

    .module-header h1 {
        font-size: 1.6rem;
    }

    .filters-section {
        padding: var(--space-sm);
    }

    .filter-label {
        width: 100%;
        margin-bottom: var(--space-xs);
    }

    .modules-grid {
        grid-template-columns: repeat(auto-fill, minmax(240px, 1fr));
    }

    .module-meta-header {
        flex-direction: column;
        align-items: flex-start;
    }
}

@media (max-width: 480px) {
    .filters-section {
        padding: var(--space-xs);
        border-radius: var(--radius-sm);
    }

    .cell-content {
        padding: var(--space-sm);
    }

    .filter-btn {
        padding: 0.3rem 0.5rem;
        font-size: 0.7rem;
    }

    .filter-label {
        min-width: auto;
        font-size: 0.8rem;
    }

    .modules-grid {
        grid-template-columns: 1fr;
    }

    .module-card {
        padding: var(--space-sm);
    }
}
