/* Copyright (C) 2024-2026 Plabayo
   License: https://github.com/plabayo/homework/blob/main/LICENSE
   Source-available; non-commercial use only. */

/* Cascade layers: declared up front so the order is explicit. Per-exercise
   styles are inlined unlayered (after this file) and therefore always win
   over `base` — no specificity battles, no !important needed. */
@layer base;

@layer base {
    /* ---------- reset ---------- */
    *,
    *::before,
    *::after {
        box-sizing: border-box;
    }
    * {
        margin: 0;
    }
    body {
        line-height: 1.5;
        -webkit-font-smoothing: antialiased;
    }
    img,
    picture,
    video,
    canvas,
    svg {
        display: block;
        max-width: 100%;
    }
    input,
    button,
    textarea,
    select {
        font: inherit;
        color: inherit;
    }
    p,
    h1,
    h2,
    h3,
    h4,
    h5,
    h6 {
        overflow-wrap: break-word;
    }
    button {
        cursor: pointer;
        user-select: none;
        -webkit-user-select: none;
        -webkit-touch-callout: none;
        touch-action: manipulation;
        -webkit-tap-highlight-color: transparent;
    }
    .button-row,
    .button-pair,
    .exercise-actions,
    .exercise-meta {
        user-select: none;
    }
    a {
        color: var(--accent);
    }
    a.nowrap,
    .nowrap {
        white-space: nowrap;
    }

    /* ---------- design tokens ----------
   Single declaration per token via light-dark() — no parallel dark-mode block
   to keep in sync. Tokens are mostly semantic: per-exercise CSS should reach
   for these names rather than re-introducing literal colors. */
    html {
        color-scheme: light dark;
        /* Reserve room for the scrollbar so layout doesn't shift when content
       grows past the viewport. */
        scrollbar-gutter: stable;
    }
    :root {
        --bg: light-dark(#f6f5f1, #14171c);
        --fg: light-dark(#14181f, #f5f7fb);
        --muted: light-dark(#4b5563, #c7cdd9);
        --accent: light-dark(#1d4ed8, #93c5fd);
        --accent-fg: light-dark(#ffffff, #0b1220);
        --good: light-dark(#15803d, #4ade80);
        --bad: light-dark(#b91c1c, #fca5a5);
        --bad-bg: light-dark(#fee2e2, #4b1d1d);
        --warn-bg: light-dark(#fde68a, #facc15);
        --warn-fg: light-dark(#4a3500, #14181f);
        --paper: light-dark(#fffde6, #1f2530);
        --border: light-dark(#14181f, #f5f7fb);
        /* Tertiary palette — for non-error highlights (purple/violet ribbons,
       lenient-match cards, etc.) */
        --info: light-dark(#7c3aed, #a78bfa);

        /* Translucent surface tints, derived from semantic colors so they shift
       automatically with the theme. */
        --accent-tint: color-mix(in srgb, var(--accent) 10%, transparent);
        --good-tint: color-mix(in srgb, var(--good) 12%, transparent);
        --bad-tint: color-mix(in srgb, var(--bad) 12%, transparent);
        --border-soft: color-mix(in srgb, var(--fg) 18%, transparent);

        /* Layered elevation. Soft on top, broad below — the layered-shadow
       pattern that gives "lift" without a harsh edge. */
        --shadow-card:
            0 2px 4px color-mix(in srgb, var(--fg) 8%, transparent),
            0 8px 20px color-mix(in srgb, var(--fg) 12%, transparent),
            0 22px 46px color-mix(in srgb, var(--fg) 14%, transparent);
        --shadow-card-hover:
            0 4px 8px color-mix(in srgb, var(--fg) 9%, transparent),
            0 14px 32px color-mix(in srgb, var(--fg) 14%, transparent),
            0 32px 64px color-mix(in srgb, var(--fg) 18%, transparent);

        --radius: 10px;
        --gap: 12px;
        --space: 16px;
        --content-width: 720px;
        font-size: 16px;
        font-family: system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
        color: var(--fg);
        background: var(--bg);
    }

    /* ---------- base layout (mobile first) ---------- */
    body {
        /* 16px of edge padding on small screens — 12px felt cramped for
           text-heavy pages (about, privacy) where lines run right up to
           the device bezel. Exercise screens still have their own inner
           padding inside `#exercise.box`, so this extra 4px/side doesn't
           shrink the play area noticeably. Tablet+ keeps its 28px below. */
        padding: 16px;
    }
    /* Skip-to-content link. Off-screen until focused via Tab; bringing it
       into view as a regular focus ring lets keyboard users bypass the
       header / banners and land on the main content. */
    .skip-link {
        position: absolute;
        inset-inline-start: 8px;
        inset-block-start: 8px;
        padding: 8px 12px;
        background: var(--accent);
        color: var(--accent-fg);
        border-radius: var(--radius);
        text-decoration: none;
        font-weight: 600;
        z-index: 1000;
        transform: translateY(-200%);
        transition: transform 120ms ease-out;
    }
    .skip-link:focus,
    .skip-link:focus-visible {
        transform: translateY(0);
        outline: 2px solid var(--accent-fg);
        outline-offset: 2px;
    }
    .page {
        max-width: var(--content-width);
        margin-inline: auto;
    }
    main > * + * {
        margin-block-start: var(--space);
    }
    section {
        display: block;
    }
    section + section {
        margin-block-start: calc(var(--space) * 1.5);
    }
    .page-header {
        display: flex;
        align-items: center;
        gap: 8px;
        margin-block-end: calc(var(--space) * 1.25);
        padding-block-end: 8px;
        border-block-end: 1px solid color-mix(in srgb, var(--fg) 18%, transparent);
    }
    .page-header h1 {
        font-size: 1.4rem;
        text-align: center;
        flex: 1;
    }
    .page-header .home-link {
        font-size: 1.4rem;
        text-decoration: none;
        line-height: 1;
        /* WCAG 2.5.5 — 44×44 minimum tap target. */
        min-inline-size: 44px;
        min-block-size: 44px;
        display: inline-flex;
        align-items: center;
        justify-content: center;
    }
    .theme-toggle {
        flex: 0 0 auto;
        background: none;
        border: none;
        padding: 4px;
        font-size: 1.1rem;
        line-height: 1;
        border-radius: 6px;
        /* Use color rather than opacity for the resting state so the icon
           still passes non-text contrast even when not hovered/focused. */
        color: color-mix(in srgb, currentColor 70%, transparent);
        transition:
            color 120ms ease,
            background 120ms ease;
        min-inline-size: 44px;
        min-block-size: 44px;
        display: inline-flex;
        align-items: center;
        justify-content: center;
    }
    .theme-toggle:hover {
        color: currentColor;
        background: color-mix(in srgb, var(--fg) 8%, transparent);
    }
    .theme-toggle:focus-visible {
        outline: 2px solid var(--accent);
        outline-offset: 2px;
        color: currentColor;
    }
    .page-intro {
        margin-block-end: var(--space);
        color: var(--muted);
        font-size: 0.98rem;
    }
    .page-intro + form {
        margin-block-start: var(--space);
    }

    /* ---------- reading sections (about / privacy) ----------
       Long-form text pages used to inherit raw browser margins which
       collapsed paragraphs and bullet lists too tightly together on
       mobile. These rules add deliberate vertical rhythm: more line-
       height for paragraphs, breathing room after headings, a small
       gap between siblings <li>s so a parent can scan the policy
       points without optical noise. */
    .about-section h2 {
        font-size: 1.1rem;
        margin-block: 0 calc(var(--space) * 0.6);
    }
    .about-section p {
        line-height: 1.6;
        margin-block: 0;
    }
    .about-section p + p,
    .about-section p + ul,
    .about-section p + ol,
    .about-section ul + p,
    .about-section ol + p {
        margin-block-start: calc(var(--space) * 0.85);
    }
    .about-section ul,
    .about-section ol {
        margin-block: 0;
        padding-inline-start: 1.5em;
        line-height: 1.55;
    }
    .about-section li + li {
        margin-block-start: 8px;
    }
    /* Inline `<code>` references (e.g. IndexedDB, Cache Storage on the
       privacy page) need a tiny visual lift so they don't blend into
       the surrounding prose. */
    .about-section code {
        font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
        font-size: 0.92em;
        padding: 1px 5px;
        border-radius: 4px;
        background: color-mix(in srgb, var(--fg) 8%, transparent);
    }

    /* ---------- forms ---------- */
    form {
        display: flex;
        flex-direction: column;
        gap: calc(var(--gap) * 1.25);
    }
    .field {
        display: flex;
        flex-direction: column;
        gap: 6px;
    }
    .field-row {
        display: flex;
        align-items: center;
        gap: 8px;
        flex-wrap: wrap;
    }
    fieldset {
        border: 1px solid var(--border);
        border-radius: var(--radius);
        padding: 14px 14px 12px;
        display: flex;
        flex-direction: column;
        gap: 8px;
    }
    legend {
        padding-inline: 8px;
        font-weight: 600;
    }
    .field-hint {
        color: var(--muted);
        font-size: 0.9rem;
    }
    .button-row + form,
    form + .button-row {
        margin-block-start: 8px;
    }

    label {
        font-weight: 500;
    }

    input[type="number"],
    input[type="text"],
    input[inputmode="numeric"] {
        padding: 8px 10px;
        border: 1px solid var(--border);
        border-radius: var(--radius);
        background: var(--paper);
        width: 100%;
        min-width: 0;
    }
    input[type="checkbox"] {
        inline-size: 1.1rem;
        block-size: 1.1rem;
        accent-color: var(--accent);
    }

    .default-button {
        padding: 12px 16px;
        border: 1px solid var(--border);
        border-radius: var(--radius);
        background: var(--bg);
        color: var(--fg);
        font-weight: 600;
        min-height: 44px;
        line-height: 1.1;
    }
    button:disabled {
        opacity: 0.5;
        cursor: not-allowed;
    }
    .default-button.primary {
        background: var(--accent);
        color: var(--accent-fg);
        border-color: var(--accent);
    }
    button:focus-visible,
    input:focus-visible,
    a:focus-visible {
        outline: 3px solid var(--accent);
        outline-offset: 2px;
    }
    .button-row {
        display: flex;
        flex-wrap: wrap;
        gap: 10px;
        justify-content: center;
    }
    /* paired controls (e.g. +/-) — must always sit side-by-side, never wrap */
    .button-pair {
        display: inline-flex;
        gap: 10px;
        flex-wrap: nowrap;
        justify-content: center;
        align-items: stretch;
    }
    .button-pair .default-button {
        flex: 0 0 auto;
        min-width: 64px;
    }

    /* ---------- shared boxes ---------- */
    .box {
        border: 1px solid var(--border);
        padding: calc(var(--space) * 1.1) var(--space);
        border-radius: var(--radius);
        background: var(--paper);
    }
    .box > * + * {
        margin-block-start: 10px;
    }
    .bad,
    .box.bad,
    input.bad {
        background: var(--bad-bg);
    }
    .notice {
        border: 1px solid var(--accent);
        background: color-mix(in srgb, var(--accent) 15%, transparent);
        color: var(--fg);
        border-radius: var(--radius);
        padding: 10px 12px;
        text-align: center;
    }

    /* ---------- exercise frame ---------- */
    [hidden] {
        display: none !important;
    }
    #page-exercises:not([hidden]),
    #page-result:not([hidden]) {
        display: flex;
        flex-direction: column;
        gap: var(--space);
    }
    #exercise {
        margin-block: 12px 0;
        padding: var(--space);
        /* Positioning context for the floating wis chip (see .input-clear). */
        position: relative;
    }
    #exercise-title {
        text-align: center;
        color: var(--muted);
        margin: 0;
    }
    /* Single tight row at the top of the play / result page that holds the
   "begin opnieuw" button and the "oefening N van M" label, so they don't
   stack and eat ~80px of vertical space on a phone. */
    .exercise-meta {
        display: flex;
        align-items: center;
        justify-content: space-between;
        gap: 8px 16px;
        flex-wrap: wrap;
    }
    .exercise-meta .default-button {
        flex: 0 0 auto;
    }
    .exercise-meta #exercise-title {
        flex: 1 1 auto;
        text-align: end;
    }
    .exercise-clock {
        margin: 0;
        flex: 0 0 auto;
        font-variant-numeric: tabular-nums;
        font-weight: 600;
        color: var(--muted);
        font-size: 1.15rem;
        display: inline-flex;
        align-items: center;
        gap: 6px;
        white-space: nowrap;
    }
    .exercise-clock .deadline {
        color: var(--fg);
    }
    .exercise-clock .deadline.danger {
        color: var(--bad);
        font-weight: 700;
    }
    .badge.time {
        background: color-mix(in srgb, var(--accent) 18%, transparent);
        color: var(--accent);
    }
    .cycle-time {
        font-variant-numeric: tabular-nums;
        color: var(--muted);
        font-size: 0.9rem;
    }
    #exercise-feedback {
        text-align: center;
        min-height: 1.6em;
        font-size: 1.1rem;
        margin-block-end: 10px;
    }
    #exercise-feedback.is-bad {
        color: var(--bad);
    }
    #exercise-content {
        display: flex;
        flex-direction: column;
        align-items: center;
        gap: 14px;
        font-size: 1.25rem;
        margin-block: 6px;
        max-width: 100%;
    }
    #exercise-content > p {
        display: flex;
        align-items: center;
        justify-content: center;
        gap: 10px;
        flex-wrap: wrap;
        margin: 0;
    }
    #exercise-content.locked {
        opacity: 0.5;
        filter: saturate(0.6);
        pointer-events: none;
        user-select: none;
    }
    #exercise-content input {
        font-size: 1.25rem;
        text-align: center;
        width: 4em;
        max-width: 6em;
        background: var(--paper);
        flex: 0 0 auto;
    }
    #exercise-content .split-part {
        flex: 0 0 auto;
    }
    .exercise-actions {
        display: flex;
        flex-wrap: wrap;
        gap: 10px;
        justify-content: center;
        margin-block-start: 14px;
    }
    /* ---------- floating clear chip ----------
       The chip floats in the exercise card's top-right corner via absolute
       positioning. It never touches inline layout (so exercises like
       splitsen's two-box grid render exactly as they would without the
       chip). Fully invisible (opacity 0, pointer-events: none) when the
       active question has no typed content; fades in once the kid has put
       something into any input. */
    #exercise > .input-clear {
        position: absolute;
        top: 10px;
        right: 12px;
        appearance: none;
        background: var(--paper);
        border: 1px solid color-mix(in oklab, var(--fg) 35%, transparent);
        border-radius: 999px;
        width: 2.2em;
        height: 2.2em;
        min-width: 2.2em;
        padding: 0;
        font: inherit;
        font-size: 1rem;
        line-height: 1;
        display: inline-flex;
        align-items: center;
        justify-content: center;
        cursor: pointer;
        color: var(--fg);
        opacity: 0;
        pointer-events: none;
        z-index: 2;
        transition:
            opacity 0.18s ease,
            background-color 0.12s ease,
            border-color 0.12s ease,
            transform 0.12s ease;
    }
    #exercise > .input-clear.is-active {
        opacity: 1;
        pointer-events: auto;
    }
    #exercise > .input-clear.is-active:hover {
        background: color-mix(in oklab, var(--fg) 12%, var(--paper));
        border-color: color-mix(in oklab, var(--fg) 60%, transparent);
    }
    #exercise > .input-clear:focus-visible {
        outline: 2px solid var(--fg);
        outline-offset: 2px;
        opacity: 1;
    }
    #exercise > .input-clear.is-active:active {
        transform: scale(0.92);
    }
    /* Touch devices: bump the chip to a comfortable thumb target. */
    @media (pointer: coarse) {
        #exercise > .input-clear {
            width: 2.5em;
            height: 2.5em;
            min-width: 2.5em;
        }
    }
    .split-part {
        width: 3em;
        display: inline-block;
        text-align: center;
    }
    input.split-part {
        line-height: 2em;
        width: 3em;
    }

    /* ---------- fraction display ----------
       Shared between fractions and percentages (and any future exercise
       that wants a stacked num/bar/den). Lives here so the two exercises
       can't drift apart silently. */
    .fraction {
        display: inline-flex;
        flex-direction: column;
        align-items: center;
        vertical-align: middle;
        text-align: center;
        line-height: 1.3;
        gap: 1px;
    }
    .fraction .frac-num,
    .fraction .frac-den {
        padding-inline: 6px;
        min-width: 1.5em;
    }
    .fraction .frac-bar {
        width: 100%;
        min-width: 1.5em;
        height: 2px;
        background: currentColor;
        display: block;
    }
    /* Fraction input: same vertical-stack layout, with text fields. */
    .fraction-input {
        display: inline-flex;
        flex-direction: column;
        align-items: center;
        vertical-align: middle;
        gap: 2px;
    }
    .fraction-input input {
        width: 3em;
        text-align: center;
        padding: 2px 4px;
    }
    .fraction-input .frac-bar {
        width: 100%;
        min-width: 3em;
        height: 2px;
        background: currentColor;
        display: block;
    }

    /* ---------- result ---------- */
    #result {
        text-align: center;
        display: flex;
        flex-direction: column;
        gap: 14px;
        align-items: center;
        width: 100%;
    }
    #result h2 {
        margin-block: 4px;
    }
    #result h3 {
        font-size: 1.6rem;
        margin-block: 4px;
    }
    #result .section-title {
        font-size: 1.05rem;
        font-weight: 600;
        color: var(--muted);
        margin-block-start: 6px;
    }
    #result small.muted {
        color: var(--muted);
        font-size: 0.95rem;
        font-weight: 500;
    }
    .result-cycles {
        width: 100%;
        max-width: 480px;
        display: flex;
        flex-direction: column;
        gap: 8px;
        align-items: stretch;
    }
    .cycle-row {
        display: flex;
        flex-wrap: wrap;
        align-items: center;
        gap: 6px 10px;
        padding: 8px 12px;
        border: 1px solid var(--border);
        border-radius: var(--radius);
        text-align: left;
    }
    .cycle-num {
        font-weight: 600;
        color: var(--muted);
        min-width: 5em;
    }
    .cycle-score {
        font-weight: 700;
    }
    .cycle-mode {
        font-size: 0.85rem;
        color: var(--muted);
        font-style: italic;
    }
    .cycle-perfect {
        color: var(--good);
        font-weight: 600;
    }
    .cycle-detail {
        display: flex;
        flex-wrap: wrap;
        gap: 4px;
        margin-inline-start: auto;
    }
    .result-detail {
        width: 100%;
        max-width: 520px;
        text-align: left;
    }
    .result-detail-list {
        list-style: none;
        padding: 0;
        display: grid;
        grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
        gap: 10px;
    }
    .result-detail-list li {
        padding: 12px 16px;
        border-inline-start: 3px solid var(--border);
        border-start-end-radius: var(--radius);
        border-end-end-radius: var(--radius);
        background: color-mix(in srgb, var(--bg) 96%, var(--fg) 4%);
        display: flex;
        flex-direction: column;
        gap: 6px;
    }
    .result-detail-list li.item-wrong {
        border-inline-start-color: var(--bad);
    }
    .result-detail-list li.item-tricky {
        border-inline-start-color: var(--warn-fg);
    }
    .item-desc {
        font-weight: 600;
        line-height: 1.3;
    }
    .item-meta {
        display: flex;
        align-items: center;
        gap: 6px;
        font-size: 0.85rem;
        color: var(--muted);
    }
    .result-actions {
        display: flex;
        flex-wrap: wrap;
        gap: 10px;
        justify-content: center;
    }
    #review-buttons {
        display: flex;
        justify-content: center;
        gap: 10px;
        flex-wrap: wrap;
    }
    #review {
        width: 100%;
        margin-block-start: 6px;
    }
    #review .exercise-feedback {
        display: flex;
        flex-direction: column;
        align-items: center;
        gap: 10px;
        text-align: center;
    }
    #review .exercise-feedback p {
        display: flex;
        align-items: center;
        justify-content: center;
        gap: 8px;
        flex-wrap: wrap;
        margin: 0;
    }

    /* ---------- catalogue (home) ---------- */
    .exercise-list {
        list-style: none;
        padding: 0;
        display: grid;
        grid-template-columns: 1fr;
        gap: 12px;
        margin-block: var(--space) 0;
    }
    .exercise-list a {
        display: flex;
        align-items: center;
        gap: 10px;
        padding: 12px;
        border: 1px solid var(--border);
        border-radius: var(--radius);
        text-decoration: none;
        color: inherit;
        background: var(--paper);
    }
    .exercise-list .icon {
        font-size: 1.8rem;
        line-height: 1;
        flex: 0 0 auto;
    }
    .exercise-list .exercise-meta {
        display: flex;
        flex-direction: column;
        align-items: flex-start;
        gap: 2px;
        min-width: 0;
    }
    .exercise-list .exercise-label {
        font-weight: 600;
        font-size: 1.1rem;
    }
    .exercise-list .exercise-stats {
        color: var(--muted);
        font-size: 0.85rem;
        min-height: 1.2em;
    }
    .exercise-list .exercise-stats[data-best="true"] {
        color: var(--good);
    }

    /* ---------- footer ---------- */
    .site-footer {
        margin-block-start: calc(var(--space) * 3.5);
        padding-block-start: calc(var(--space) * 1.25);
        border-block-start: 1px solid color-mix(in srgb, var(--fg) 18%, transparent);
        color: var(--muted);
        font-size: 0.88rem;
        display: flex;
        flex-direction: column;
        gap: var(--gap);
    }
    .site-footer h2 {
        font-size: 1rem;
        color: var(--fg);
    }
    .footer-block {
        display: flex;
        flex-direction: column;
        gap: 6px;
    }

    /* ---------- mistake picker dialog ---------- */
    dialog.mistake-picker,
    dialog.leave-guard-dialog {
        border: 1px solid var(--border);
        border-radius: var(--radius);
        background: var(--bg);
        color: var(--fg);
        padding: 0;
        max-width: min(92vw, 480px);
        width: 100%;
        max-height: 85vh;
        overflow: hidden;
    }
    dialog.mistake-picker::backdrop,
    dialog.leave-guard-dialog::backdrop {
        background: rgba(0, 0, 0, 0.45);
    }
    .mistake-picker-form,
    .leave-guard-form {
        display: flex;
        flex-direction: column;
        gap: 10px;
        padding: 18px;
        max-height: 85vh;
        overflow: hidden;
    }
    .mistake-picker h2,
    .leave-guard-dialog h2 {
        font-size: 1.2rem;
    }
    .mistake-picker .muted,
    .leave-guard-dialog .muted {
        color: var(--muted);
        font-size: 0.95rem;
    }
    .leave-guard-form .button-row {
        justify-content: flex-end;
    }
    .mistake-picker .all-toggle {
        display: inline-flex;
        align-items: center;
        gap: 8px;
        font-weight: 600;
        padding: 6px 10px;
        border: 1px solid var(--border);
        border-radius: var(--radius);
        align-self: flex-start;
    }
    .mistake-picker .picker-list {
        list-style: none;
        padding: 4px 0;
        margin: 0;
        display: flex;
        flex-direction: column;
        gap: 4px;
        overflow-y: auto;
        flex: 1 1 auto;
        min-height: 0;
        max-height: 50vh;
        border-block: 1px solid color-mix(in srgb, var(--fg) 18%, transparent);
    }
    .mistake-picker .picker-list label {
        display: flex;
        align-items: center;
        gap: 8px;
        padding: 6px 8px;
        border-radius: var(--radius);
        cursor: pointer;
        font-weight: 500;
    }
    .mistake-picker .picker-list label:hover {
        background: color-mix(in srgb, var(--fg) 6%, transparent);
    }

    /* ---------- language banner ---------- */
    .lang-banner {
        background: color-mix(in srgb, var(--accent) 10%, transparent);
        border: 1px solid color-mix(in srgb, var(--accent) 25%, transparent);
        color: var(--fg);
        padding: 10px 14px;
        border-radius: var(--radius);
        margin-block-end: var(--space);
        display: flex;
        flex-wrap: wrap;
        align-items: center;
        gap: 8px 14px;
        font-size: 0.9rem;
    }
    .lang-banner p {
        flex: 1 1 auto;
        min-width: 0;
    }
    .lang-banner .default-button {
        flex: 0 0 auto;
        padding: 6px 12px;
        min-height: 36px;
        font-size: 0.875rem;
        font-weight: 600;
        white-space: nowrap;
    }

    /* ---------- offline indicator ---------- */
    .offline-banner {
        display: none;
        background: var(--warn-bg);
        color: var(--warn-fg);
        padding: 8px 12px;
        text-align: center;
        font-size: 0.95rem;
        font-weight: 600;
        border-radius: var(--radius);
        margin-block-end: var(--space);
    }
    body.is-offline .offline-banner {
        display: block;
    }

    /* ---------- history (parent view) ---------- */
    .history {
        margin-block-start: calc(var(--space) * 2);
        padding-block-start: var(--space);
        border-block-start: 1px solid color-mix(in srgb, var(--fg) 18%, transparent);
    }
    .history h2 {
        font-size: 1.2rem;
    }
    .history-content {
        display: flex;
        flex-direction: column;
        gap: 10px;
    }
    .history details {
        border: 1px solid var(--border);
        border-radius: var(--radius);
        padding: 10px 14px;
        background: color-mix(in srgb, var(--bg) 85%, var(--fg) 15%);
    }
    .history summary {
        cursor: pointer;
        font-weight: 600;
    }
    .history-list {
        margin-block-start: 4px;
        display: flex;
        flex-direction: column;
        gap: 10px;
    }
    .history-session {
        border: 1px solid var(--border);
        border-radius: var(--radius);
        padding: 10px 12px;
    }
    .history-session-header {
        display: flex;
        justify-content: space-between;
        flex-wrap: wrap;
        gap: 4px;
        font-weight: 600;
    }
    .history-perfect {
        margin-block-start: 6px;
        font-size: 0.9rem;
        color: var(--good);
    }
    .history-empty {
        color: var(--muted);
        font-style: italic;
    }
    .history-detail-list {
        margin-block-start: 10px;
        grid-template-columns: repeat(auto-fill, minmax(160px, 1fr));
        gap: 8px;
    }
    .history-detail-list li {
        padding: 8px 12px;
        gap: 4px;
        font-size: 0.9rem;
    }
    /* Summary line above the recent session cards — a quick "are we
       improving" pulse so the parent doesn't have to scan every card. */
    .history-summary {
        font-size: 0.95rem;
        color: var(--muted);
        margin-block-end: 2px;
    }
    .history-recent {
        display: flex;
        flex-direction: column;
        gap: 10px;
    }
    /* "Toon meer" button — visually lighter than the primary action buttons
       above (oefen recente fouten / geschiedenis wissen) so it reads as
       progressive disclosure, not a primary CTA. */
    .history-show-more {
        align-self: center;
        background: transparent;
        border: 1px dashed color-mix(in srgb, var(--fg) 35%, transparent);
        color: var(--muted);
        font-size: 0.9rem;
        padding: 6px 14px;
    }
    .history-show-more:hover {
        color: var(--fg);
        border-color: color-mix(in srgb, var(--fg) 55%, transparent);
    }
    /* Weekly aggregation block: collapsed <details> rows for older sessions
       that no longer warrant a full card each. Native disclosure widget so
       keyboard + screen reader users get the right semantics for free. */
    .history-weeks {
        margin-block-start: var(--space);
        display: flex;
        flex-direction: column;
        gap: 6px;
    }
    .history-weeks-title {
        font-size: 0.95rem;
        color: var(--muted);
        font-weight: 600;
        margin: 0 0 2px;
    }
    .history-week {
        border: 1px solid color-mix(in srgb, var(--fg) 14%, transparent);
        border-radius: var(--radius);
        padding: 8px 12px;
    }
    .history-week > summary {
        cursor: pointer;
        font-size: 0.92rem;
        color: var(--fg);
        list-style: none;
    }
    .history-week > summary::-webkit-details-marker {
        display: none;
    }
    .history-week > summary::before {
        content: "▸ ";
        display: inline-block;
        margin-inline-end: 4px;
        transition: transform 120ms ease;
    }
    .history-week[open] > summary::before {
        transform: rotate(90deg);
    }
    .history-week[open] > summary {
        margin-block-end: 6px;
    }
    .history-week-detail-title {
        font-size: 0.9rem;
        color: var(--muted);
        margin: 4px 0;
    }
    .history-week-detail-empty {
        font-size: 0.9rem;
        color: var(--good);
        margin: 4px 0;
    }
    .history-week-mistakes {
        list-style: none;
        padding: 0;
        margin: 0;
        display: grid;
        grid-template-columns: 1fr;
        gap: 4px;
    }
    .history-week-mistakes li {
        display: flex;
        justify-content: space-between;
        gap: 8px;
        padding: 4px 8px;
        border-radius: 6px;
        background: color-mix(in srgb, var(--fg) 6%, transparent);
        font-size: 0.9rem;
    }
    .history-week-mistakes .item-meta {
        color: var(--muted);
        font-size: 0.85em;
    }
    @media (prefers-reduced-motion: reduce) {
        .history-week > summary::before {
            transition: none;
        }
    }
    .badge {
        display: inline-block;
        padding: 1px 7px;
        border-radius: 999px;
        font-size: 0.8rem;
        font-weight: 600;
        margin-inline-end: 4px;
        line-height: 1.4;
    }
    .badge.bad {
        background: var(--bad-bg);
        color: var(--bad);
    }
    .badge.tricky {
        background: color-mix(in srgb, var(--warn-bg) 60%, transparent);
        color: var(--warn-fg);
    }

    /* ---------- confetti ---------- */
    #confetti {
        position: fixed;
        inset: 0;
        pointer-events: none;
        z-index: -1;
        width: 100vw;
        height: 0;
        margin: 0;
        overflow: hidden;
    }
    #confetti[data-active="true"] {
        height: 100dvh;
    }

    /* ---------- tablet upgrade ---------- */
    @media (min-width: 600px) {
        body {
            padding: 28px;
        }
        .page-header h1 {
            font-size: 1.8rem;
        }
        #exercise {
            padding: calc(var(--space) * 1.25);
        }
        #exercise-feedback {
            font-size: 1.2rem;
        }
        #exercise-content {
            font-size: 1.8rem;
            gap: 14px;
        }
        #exercise-content input {
            font-size: 1.8rem;
        }
        .exercise-list {
            grid-template-columns: repeat(2, 1fr);
        }
        main > * + * {
            margin-block-start: calc(var(--space) * 1.25);
        }
    }

    /* ---------- desktop upgrade ---------- */
    @media (min-width: 960px) {
        :root {
            --content-width: 820px;
        }
        .exercise-list {
            grid-template-columns: repeat(3, 1fr);
        }
    }

    /* ---------- reduced motion ---------- */
    @media (prefers-reduced-motion: reduce) {
        #confetti {
            display: none;
        }
    }

    /* ---------- micro-animations ---------- */
    @media (prefers-reduced-motion: no-preference) {
        /* Smooth lock/unlock transition when a deadline expires */
        #exercise-content {
            transition:
                opacity 220ms ease,
                filter 220ms ease;
        }

        /* Button: hover lift, press sink */
        .default-button {
            transition:
                transform 90ms ease,
                box-shadow 100ms ease,
                background-color 130ms ease,
                border-color 130ms ease,
                opacity 130ms ease;
        }
        .default-button.btn-lift:not(:disabled):hover {
            transform: translateY(-1px);
            box-shadow: 0 3px 8px color-mix(in srgb, var(--fg) 12%, transparent);
        }
        .default-button.btn-lift:not(:disabled):active {
            transform: translateY(1px) scale(0.98);
            box-shadow: none;
            transition-duration: 50ms;
        }

        /* Input: accent border on focus */
        input[type="number"],
        input[type="text"],
        input[inputmode="numeric"] {
            transition:
                border-color 130ms ease,
                outline-offset 80ms ease;
        }
        input[type="number"]:focus,
        input[type="text"]:focus,
        input[inputmode="numeric"]:focus {
            border-color: var(--accent);
        }

        /* Exercise catalog cards */
        .exercise-list a {
            transition:
                transform 130ms ease,
                box-shadow 130ms ease,
                border-color 130ms ease;
        }
        .exercise-list a:hover {
            transform: translateY(-2px);
            box-shadow: 0 6px 18px color-mix(in srgb, var(--fg) 11%, transparent);
            border-color: var(--accent);
        }

        /* Option buttons: smooth selection */
        .option {
            transition:
                background-color 110ms ease,
                color 110ms ease,
                border-color 110ms ease;
        }

        /* Page-level entrance when switching between setup / play / result.
           Driven by JS adding `.section-enter` with a reflow-restart so the
           animation replays on every transition, not just initial load. */
        .section-enter {
            animation: page-in 220ms ease-out both;
        }

        /* Each new question fades up into view */
        #exercise-content.question-enter {
            animation: question-in 120ms ease-out both;
        }

        /* Answer reveal: content scales slightly in so the transition reads as
           a deliberate state change rather than a snap-replace. */
        #exercise-content.review-enter {
            animation: review-reveal 160ms ease-out both;
        }

        /* Correct answer: brief green glow around the exercise card.
       Target is #exercise (.box) so the rounded corners shape the glow. */
        #exercise.is-correct {
            animation: correct-flash 280ms ease-out both;
        }
        #exercise.is-correct.is-streak-mid {
            animation: correct-flash-mid 320ms ease-out both;
        }
        #exercise.is-correct.is-streak-high {
            animation: correct-flash-high 380ms ease-out both;
        }

        /* Wrong answer: horizontal shake */
        #exercise-content.is-wrong {
            animation: shake 380ms cubic-bezier(0.36, 0.07, 0.19, 0.97) both;
        }

        /* Feedback "try again" message: small pop so it registers */
        #exercise-feedback.is-bad {
            animation: pop-in 160ms ease both;
        }

        /* History sessions: stagger fade so they don't all flash at once */
        .history-session {
            animation: question-in 180ms ease-out both;
        }
        .history-session:nth-child(2) {
            animation-delay: 40ms;
        }
        .history-session:nth-child(3) {
            animation-delay: 80ms;
        }
        .history-session:nth-child(n + 4) {
            animation-delay: 120ms;
        }

        /* Mistake picker / leave-guard dialog entrance. We don't animate
           the *exit* of these dialogs: closed `<dialog>` elements get
           `display: none` from the UA stylesheet, which prevents any CSS
           animation from running. JS removes them immediately on close. */
        dialog.mistake-picker[open],
        dialog.leave-guard-dialog[open] {
            animation: page-in 180ms ease both;
        }

        /* Language banner dismiss — banner is a regular element (not a
           <dialog>), so a CSS exit animation actually runs. JS adds
           `.is-leaving` and waits for animationend before removing. */
        .lang-banner.is-leaving {
            animation: page-out 160ms ease both;
        }

        @keyframes page-in {
            from {
                opacity: 0;
                transform: translateY(8px);
            }
            to {
                opacity: 1;
                transform: translateY(0);
            }
        }
        @keyframes page-out {
            from {
                opacity: 1;
                transform: translateY(0);
            }
            to {
                opacity: 0;
                transform: translateY(-6px);
            }
        }
        @keyframes question-in {
            from {
                opacity: 0;
                transform: translateY(5px);
            }
            to {
                opacity: 1;
                transform: translateY(0);
            }
        }
        @keyframes review-reveal {
            from {
                opacity: 0.2;
                transform: scale(0.96);
            }
            to {
                opacity: 1;
                transform: scale(1);
            }
        }
        @keyframes pop-in {
            from {
                opacity: 0;
                transform: scale(0.88);
            }
            to {
                opacity: 1;
                transform: scale(1);
            }
        }
        @keyframes shake {
            0%,
            100% {
                transform: translateX(0);
            }
            15% {
                transform: translateX(-6px);
            }
            35% {
                transform: translateX(5px);
            }
            52% {
                transform: translateX(-4px);
            }
            68% {
                transform: translateX(3px);
            }
            84% {
                transform: translateX(-1px);
            }
        }
        /* Streak glow strength scales with how well the student is doing — same
       shape, just brighter and wider. Colors derive from --good so dark mode
       and any future palette tweak track automatically. */
        @keyframes correct-flash {
            0%,
            100% {
                box-shadow: 0 0 0 0 transparent;
            }
            40% {
                box-shadow: 0 0 10px 0 color-mix(in srgb, var(--good) 22%, transparent);
            }
        }
        @keyframes correct-flash-mid {
            0%,
            100% {
                box-shadow: 0 0 0 0 transparent;
            }
            35% {
                box-shadow: 0 0 18px 3px color-mix(in srgb, var(--good) 40%, transparent);
            }
        }
        @keyframes correct-flash-high {
            0%,
            100% {
                box-shadow: 0 0 0 0 transparent;
            }
            30% {
                box-shadow: 0 0 26px 5px color-mix(in srgb, var(--good) 55%, transparent);
            }
        }
    }

    /* ---------- inline phrase-flip widget ----------
   Shared between the analog and digital clock exercises. Toggles between
   two Dutch wordings of the same time when clicked. Flat opacity
   crossfade rather than a 3D rotation: relying on `backface-visibility`
   is unreliable across engines (Firefox in particular), and a crossfade
   is a safe baseline. JS (homework.js sizeFlip) measures both faces and
   animates the container width so the layout glides between them. */
    .phrase-flip {
        display: inline-block;
        cursor: pointer;
        vertical-align: bottom;
    }
    .phrase-flip-inner {
        display: inline-block;
        position: relative;
        white-space: nowrap;
    }
    .phrase-flip-front {
        display: inline-block;
        white-space: nowrap;
        text-decoration-line: underline;
        text-decoration-style: dotted;
        text-decoration-color: color-mix(in srgb, currentColor 38%, transparent);
        text-underline-offset: 3px;
    }
    .phrase-flip-back {
        /* Anchored at the inner's left edge but content-sized in both axes —
       sizeFlip() reads back.offsetWidth to drive the width transition of
       the inner container, so surrounding text hugs whichever face is
       currently showing. `inset: 0` would stretch the back to the inner's
       current width and make sizeFlip think both faces are equal-sized. */
        position: absolute;
        inset-block-start: 0;
        inset-inline-start: 0;
        white-space: nowrap;
        color: var(--accent);
        opacity: 0;
        pointer-events: none;
    }
    .phrase-flip.flipped .phrase-flip-front {
        opacity: 0;
    }
    .phrase-flip.flipped .phrase-flip-back {
        opacity: 1;
    }
    .phrase-flip:hover .phrase-flip-front,
    .phrase-flip:focus-visible .phrase-flip-front {
        text-decoration-color: color-mix(in srgb, currentColor 75%, transparent);
    }
    .phrase-flip:focus-visible {
        outline: 2px solid color-mix(in srgb, var(--accent) 60%, transparent);
        outline-offset: 2px;
        border-radius: 2px;
    }

    @media (prefers-reduced-motion: no-preference) {
        .phrase-flip-inner {
            transition: width 0.38s ease;
        }
        .phrase-flip-face {
            transition: opacity 0.22s ease;
        }
        .phrase-flip-front {
            transition:
                text-decoration-color 0.15s,
                opacity 0.22s ease;
        }
    }

    /* ---------- word-choice option with dual-variant peek ----------
   Shared between the analog and digital clock exercises: each option is
   a button containing two label faces stacked over a visibility-hidden
   spacer that sizes the button to fit the wider variant. The `↔` peek
   button toggles `.flipped` on the wrap, crossfading between the two
   faces. Flat 2D crossfade because relying on `backface-visibility` is
   unreliable across engines (Firefox in particular). */
    .word-option-wrap {
        position: relative;
    }
    .word-option-wrap .default-button.option {
        width: 100%;
        height: 100%; /* fill grid-stretched wrap so all row buttons match height */
    }
    .word-option-btn {
        position: relative;
        overflow: hidden;
        /* Reserve a strip below the centered label for the peek (↔)
       button. The face uses the same inset so the label is centered
       above this strip — never crowding the peek icon. Easier on
       younger readers: the label and the affordance to flip it are
       visually distinct rather than mashed together. */
        padding-block-end: 1.8em;
    }
    .word-option-spacer {
        /* Sizing-only ghost: stacks both labels in the same grid cell so
       the button's intrinsic width = max of the two. visibility:hidden
       keeps it invisible but in flow. aria-hidden in markup keeps it
       out of the a11y tree. */
        display: grid;
        grid-template-areas: "stack";
        visibility: hidden;
        pointer-events: none;
    }
    .word-option-spacer > * {
        grid-area: stack;
    }
    .word-option-face {
        /* Insets match the button's own padding so the face occupies the
       *content* box, not the padding box. This is what gives long
       wordings ("vijfentwintig over een") visible breathing room from
       the button border — without it the face would span edge-to-edge
       and the text would wrap right up against the border on its
       longest line. Bottom inset (1.8em) leaves the peek (↔) strip free. */
        position: absolute;
        inset: 0.5em 1.4rem 1.8em 1.4rem;
        display: flex;
        align-items: center;
        justify-content: center;
    }
    .word-option-back {
        color: var(--accent);
        opacity: 0;
        pointer-events: none;
    }
    .word-option-wrap.flipped .word-option-front {
        opacity: 0;
    }
    .word-option-wrap.flipped .word-option-back {
        opacity: 1;
    }
    .option-list .default-button.selected .word-option-back {
        color: var(--accent-fg);
    }
    /* Peek button floats above the option in its own stacking layer and
   stays in place while the option button flips behind it. */
    .word-variant-peek {
        position: absolute;
        bottom: 5px;
        right: 6px;
        background: none;
        border: none;
        /* Larger transparent padding gives a 44×24 hit area without
           enlarging the visible glyph; combined with the 70% tint colour
           the affordance now meets non-text contrast at rest. */
        padding: 10px 14px;
        margin: -8px -10px -3px -10px;
        font-size: 0.75em;
        line-height: 1;
        color: color-mix(in srgb, currentColor 70%, transparent);
        cursor: pointer;
        border-radius: 3px;
        z-index: 1;
    }
    .word-variant-peek:hover,
    .word-variant-peek:focus-visible {
        color: var(--accent);
        background: color-mix(in srgb, var(--accent) 10%, transparent);
        outline: none;
    }

    @media (prefers-reduced-motion: no-preference) {
        .word-option-face {
            transition: opacity 0.22s ease;
        }
        .word-variant-peek {
            transition:
                color 0.15s,
                background 0.15s;
        }
    }

    /* ---------- shared exercise patterns ---------- */
    /* Most exercises render a vertical list of answer choices. Layout is the
   same everywhere; each exercise just picks a column count via the modifier
   class. */
    .option-list {
        display: grid;
        grid-template-columns: 1fr;
        gap: 10px;
        width: 100%;
        max-inline-size: 360px;
        margin-block-start: 10px;
    }
    .option-list.cols-2 {
        grid-template-columns: 1fr 1fr;
    }
    .option-list .default-button {
        font-size: 1.05rem;
    }
    .option-list .default-button.selected {
        background: var(--accent);
        color: var(--accent-fg);
        border-color: var(--accent);
    }
    .option-list .default-button.review-correct {
        background: var(--good-tint);
        border-color: var(--good);
        color: var(--good);
        opacity: 1;
        cursor: default;
    }
    .option-list .default-button.review-wrong {
        background: var(--bad-tint);
        border-color: var(--bad);
        color: var(--bad);
        opacity: 1;
        cursor: default;
    }
    .option-list .default-button.review-dim {
        opacity: 0.4;
        cursor: default;
    }

    /* `.kinds`: stacks checkboxes/radios in 1 column on mobile, 2 on tablet+.
   Used by fractions and multiplications setup screens. */
    .kinds {
        display: grid;
        grid-template-columns: 1fr;
        gap: 4px;
    }
    .kinds label {
        display: inline-flex;
        align-items: center;
        gap: 6px;
    }
    @media (min-width: 600px) {
        .kinds {
            grid-template-columns: repeat(2, 1fr);
        }
    }
    /* ---------- exercise bar ----------
       Single header row on every exercise page: breadcrumb left, theme
       toggle right. Replaces the wider page_header — exercise screens
       benefit from every vertical pixel for the actual exercise UI, and
       the breadcrumb already names the current page (its leaf wraps the
       `<h1>`). Uses logical properties so RTL flips for free. */
    .exercise-bar {
        display: flex;
        align-items: center;
        gap: 8px;
        margin-block-end: calc(var(--space) * 0.75);
        padding-block-end: 8px;
        border-block-end: 1px solid color-mix(in srgb, var(--fg) 18%, transparent);
    }
    .exercise-bar .breadcrumb {
        flex: 1;
        min-width: 0;
    }
    .exercise-bar .theme-toggle {
        flex: 0 0 auto;
    }
    .breadcrumb ol {
        list-style: none;
        padding: 0;
        margin-block: 0;
        margin-inline: 0;
        display: flex;
        flex-wrap: wrap;
        gap: 6px 8px;
        font-size: 0.9rem;
        color: color-mix(in oklab, var(--fg) 70%, transparent);
    }
    .breadcrumb li {
        display: inline-flex;
        align-items: center;
        gap: 6px;
    }
    /* `›` separator inserted before every li except the first. Generated
       via ::before so screen readers (which skip generated content by
       convention) don't read it as part of the link text. */
    .breadcrumb li + li::before {
        content: "›";
        color: color-mix(in oklab, var(--fg) 45%, transparent);
        font-weight: 600;
    }
    .breadcrumb a {
        color: inherit;
        text-decoration: underline;
        text-underline-offset: 2px;
        text-decoration-color: color-mix(in oklab, var(--fg) 25%, transparent);
    }
    .breadcrumb a:hover {
        color: var(--fg);
        text-decoration-color: currentColor;
    }
    .breadcrumb [aria-current="page"] {
        color: var(--fg);
        font-weight: 600;
    }
    /* The leaf `<li>` wraps an `<h1>` (kept for SEO/a11y) — strip the
       default browser heading margins and force-inherit its display
       behaviour so the heading sits inline like any other breadcrumb item
       instead of breaking onto its own line. */
    .breadcrumb [aria-current="page"] h1 {
        font-size: inherit;
        font-weight: inherit;
        margin: 0;
        display: inline;
        line-height: inherit;
    }
    .breadcrumb .icon {
        margin-inline-end: -2px;
    }
} /* end @layer base */

/* ---------- cross-document View Transitions ----------
   Opt every same-origin navigation (home ↔ exercise, exercise ↔ exercise,
   etc.) into the View Transitions API. The default cross-fade gives the
   PWA a native app feel. `@view-transition` is a top-level at-rule
   (CSS Conditional Rules § 2.5 forbids nesting it inside @media), so the
   reduced-motion gate lives separately on the pseudo-elements below.

   Browser support: Chromium 125+, Safari 18.2+. Older browsers ignore
   the at-rule and fall back to instant navigation — pure progressive
   enhancement, nothing breaks. */
@view-transition {
    navigation: auto;
}

/* Honour the user's motion preference: keep the snapshot/swap machinery
   running (so the navigation completes as fast as a default one) but
   disable every animation on the transition pseudo-elements. End result
   under `prefers-reduced-motion: reduce`: indistinguishable from a plain
   navigation. */
@media (prefers-reduced-motion: reduce) {
    ::view-transition-group(*),
    ::view-transition-old(*),
    ::view-transition-new(*) {
        animation: none !important;
    }
}
