﻿/* ===========================================================================
   Teleseminar — modernisation layer
   ---------------------------------------------------------------------------
   Loaded after the template stylesheets. It keeps the visual identity (logo,
   palette, typography, page structure) and replaces the desktop-only, float and
   fixed-width parts of the template with mobile-first CSS grid / flexbox,
   accessible focus states and fluid type.
   =========================================================================== */

:root {
    /* --- Identity: aliased to the brand tokens in brand.css, which loads
       before this file and is now the single source of truth for the actual
       values. Kept under these names because the rest of this file — and
       account.css's component rules, folded in below — reference them by
       the hundred; renaming every selector was not worth the risk. The
       spacing scale, radius, shadow and focus ring share their names with
       brand.css exactly, so they are not redeclared here at all and fall
       through from its :root untouched. --------------------------------- */
    --ts-color-primary: var(--ts-blue);
    --ts-color-primary-dark: var(--ts-blue-dark);
    --ts-color-primary-soft: var(--ts-lavender);
    --ts-color-accent: var(--ts-yellow);
    --ts-color-accent-dark: var(--ts-yellow-dark);
    --ts-color-ink: var(--ts-blue);
    --ts-color-ink-soft: rgb(var(--ts-blue-rgb) / 70%);
    --ts-color-ink-faint: rgb(var(--ts-blue-rgb) / 55%);
    --ts-color-surface: var(--ts-white);
    --ts-color-surface-muted: var(--ts-lavender);
    --ts-color-border: var(--ts-periwinkle);
}

/* The six feedback aliases cannot live in the block above, and the reason is the
   one rule about custom properties that is easy to get wrong: a var() inside a
   custom property is substituted **on the element that declares it**, not on the
   element that finally uses it.

   brand.css puts the raw palette on :root but the semantic --ts-feedback-* tokens
   on .ts-mode-public / .ts-mode-learn, i.e. on <body> — that is what inverts the
   whole site on sign-in. Aliased from :root, they resolved against <html>, where
   those tokens do not exist, so each of these computed to the guaranteed-invalid
   value and inherited downwards as invalid. An invalid `color` falls back to the
   inherited one, so every "success" green rendered **Blue**; an invalid
   `background` falls back to transparent, so every soft tint vanished.

   It showed up as the confirmation panel's tick marks being Blue, but it was never
   about that panel: forty rules in this file read these six tokens.

   Declared on the two mode classes instead, they resolve where their source is and
   the mode inversion still reaches them — learn mode's lightened green
   (--ts-green-on-blue) flows through exactly as intended.

   Do not fold these back into :root. */
.ts-mode-public,
.ts-mode-learn {
    --ts-color-success: var(--ts-feedback-success);
    --ts-color-success-soft: var(--ts-feedback-success-bg);
    --ts-color-warning: var(--ts-feedback-warning);
    --ts-color-warning-soft: var(--ts-feedback-warning-bg);
    --ts-color-danger: var(--ts-feedback-error);
    --ts-color-danger-soft: var(--ts-feedback-error-bg);
}

/* ---------------------------------------------------------------- a11y base */

*:focus-visible {
    outline: var(--ts-focus-ring);
    outline-offset: var(--ts-focus-offset);
    border-radius: var(--ts-space-2xs);
}

/* The template hides the outline on several components; put it back. */
a:focus-visible,
button:focus-visible,
input:focus-visible,
select:focus-visible,
textarea:focus-visible,
summary:focus-visible {
    outline: var(--ts-focus-ring);
    outline-offset: var(--ts-focus-offset);
}

.ts-skip-link {
    position: absolute;
    left: var(--ts-space-xs);
    top: -4rem;
    z-index: 2000;
    padding: var(--ts-space-xs) var(--ts-space-md);
    background: var(--ts-color-ink);
    color: var(--ts-white);
    border-radius: var(--ts-radius);
    transition: top 0.15s ease-in-out;
}

    .ts-skip-link:focus {
        top: var(--ts-space-xs);
        color: var(--ts-white);
    }

.ts-visually-hidden {
    position: absolute !important;
    width: 1px;
    height: 1px;
    padding: 0;
    margin: -1px;
    overflow: hidden;
    clip: rect(0, 0, 0, 0);
    white-space: nowrap;
    border: 0;
}

@media (prefers-reduced-motion: reduce) {
    *,
    *::before,
    *::after {
        animation-duration: 0.001ms !important;
        animation-iteration-count: 1 !important;
        transition-duration: 0.001ms !important;
        scroll-behavior: auto !important;
    }
}

/* ------------------------------------------------------------ scroll reveal */

/* AOS's fade-up, once per visitor: reveal-once.js puts .ts-revealed on <html> when its cookie says
   the visitor has already seen it. These are the two declarations aos.css gives a block once it
   has been revealed, applied before it has been — so AOS still runs, finds nothing left to change,
   and the page ends up exactly as it does after the animation, translateZ(0) included.

   From 768 px only, the width below which main.js switches AOS off and strips the attributes. */
@media (min-width: 768px) {
    html.ts-revealed [data-aos^="fade"] {
        opacity: 1;
        transform: translateZ(0);
    }
}

/* --------------------------------------------------------------- page shell */

.ts-page {
    padding-inline: var(--ts-gutter);
    padding-block: clamp(2rem, 5vw, 4rem);
}

.ts-page--muted {
    background-color: var(--ts-color-surface-muted);
}

.ts-shell {
    width: min(100%, 1600px);
    margin-inline: auto;
}

.ts-stack > * + * {
    margin-block-start: var(--ts-space-md);
}

/* ------------------------------------------------------------ page headings */

.ts-page-header {
    display: flex;
    flex-wrap: wrap;
    align-items: end;
    justify-content: space-between;
    gap: var(--ts-space-md);
    margin-block-end: var(--ts-space-xl);
}

.ts-eyebrow {
    display: block;
    font-size: 0.875rem;
    font-weight: 600;
    letter-spacing: 0.2rem;
    text-transform: uppercase;
    color: var(--ts-color-primary);
}

.ts-title {
    margin: var(--ts-space-xs) 0 0;
    font-size: clamp(1.6rem, 1.1rem + 2vw, 2.75rem);
    font-weight: 300;
    line-height: 1.15;
    color: var(--ts-color-ink);
}

.ts-lead {
    max-width: 62ch;
    color: var(--ts-color-ink-soft);
}

.ts-breadcrumb {
    margin: 0;
    padding: 0;
    display: flex;
    flex-wrap: wrap;
    gap: var(--ts-space-xs);
    list-style: none;
    font-size: 0.9rem;
}

    .ts-breadcrumb li + li::before {
        content: "/";
        margin-inline-end: var(--ts-space-xs);
        color: var(--ts-color-ink-soft);
    }

    .ts-breadcrumb [aria-current="page"] {
        color: var(--ts-color-ink-soft);
    }

/* ------------------------------------------------------------------- grids */

.ts-grid {
    display: grid;
    gap: clamp(1.25rem, 3vw, 2.5rem);
    grid-template-columns: repeat(auto-fill, minmax(min(100%, 20rem), 1fr));
    padding: 0;
    margin: 0;
    list-style: none;
}

.ts-grid--wide {
    grid-template-columns: repeat(auto-fill, minmax(min(100%, 26rem), 1fr));
}

/* --------------------------------------------------------------------- card */

.ts-card {
    display: flex;
    flex-direction: column;
    height: 100%;
    background: var(--ts-color-surface);
    border: 1px solid var(--ts-color-border);
    border-radius: var(--ts-radius-lg);
    overflow: hidden;
    box-shadow: var(--ts-shadow);
    transition: transform 0.2s ease, box-shadow 0.2s ease;
}

    .ts-card:focus-within,
    .ts-card:hover {
        transform: translateY(-2px);
        box-shadow: var(--ts-shadow-raised);
    }

.ts-card__media {
    margin: 0;
    aspect-ratio: 16 / 10;
    background: var(--ts-color-surface-muted);
}

    .ts-card__media img {
        width: 100%;
        height: 100%;
        object-fit: cover;
        display: block;
    }

.ts-card__body {
    display: flex;
    flex-direction: column;
    gap: var(--ts-space-xs);
    padding: var(--ts-space-lg);
    flex: 1 1 auto;
}

.ts-card__kicker {
    font-size: 0.85rem;
    font-weight: 600;
    letter-spacing: 0.12rem;
    text-transform: uppercase;
    color: var(--ts-color-primary);
}

.ts-card__title {
    margin: 0;
    font-size: 1.25rem;
    font-weight: 500;
    line-height: 1.3;
}

    .ts-card__title a {
        color: var(--ts-color-ink);
    }

        /* Whole-card click target without nesting interactive elements. */
        .ts-card__title a::after {
            content: "";
            position: absolute;
            inset: 0;
        }

.ts-card {
    position: relative;
}

.ts-card__text {
    margin: 0;
    color: var(--ts-color-ink-soft);
    display: -webkit-box;
    -webkit-line-clamp: 4;
    -webkit-box-orient: vertical;
    overflow: hidden;
}

.ts-card__footer {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    justify-content: space-between;
    gap: var(--ts-space-xs);
    margin-block-start: auto;
    padding-block-start: var(--ts-space-md);
}

.ts-meta {
    font-weight: 600;
    color: var(--ts-color-ink);
}

.ts-price {
    font-weight: 700;
    color: var(--ts-color-accent-dark);
}

/* Buttons inside a card must sit above the stretched title link. */
.ts-card__footer button,
.ts-card__footer a {
    position: relative;
    z-index: 1;
}

/* ------------------------------------------------------------------ badges */

.ts-badge {
    display: inline-flex;
    align-items: center;
    gap: var(--ts-space-2xs);
    padding: 0.15rem 0.6rem;
    border-radius: 999px;
    font-size: 0.8rem;
    font-weight: 600;
}

.ts-badge--granted {
    background: var(--ts-color-success-soft);
    color: var(--ts-color-success);
}

.ts-badge--pending {
    background: var(--ts-color-warning-soft);
    color: var(--ts-color-warning);
}

/* ----------------------------------------------------------------- buttons */

.ts-button {
    display: inline-flex;
    align-items: center;
    gap: var(--ts-space-xs);
    padding: 0.6rem 1.2rem;
    border: 1px solid transparent;
    border-radius: 999px;
    font-weight: 600;
    line-height: 1.2;
    cursor: pointer;
    background: var(--ts-color-primary);
    color: var(--ts-white);
    transition: background-color 0.15s ease-in-out;
}

    .ts-button:hover {
        background: var(--ts-color-primary-dark);
        color: var(--ts-white);
    }

.ts-button--ghost {
    background: transparent;
    color: var(--ts-color-primary);
    border-color: currentColor;
}

    .ts-button--ghost:hover {
        background: var(--ts-color-surface-muted);
        color: var(--ts-color-primary-dark);
    }

.ts-button--icon {
    padding: 0.45rem 0.7rem;
}

/* ------------------------------------------------------------------- tiles */

.ts-tile {
    position: relative;
    display: block;
    margin: 0;
    border-radius: var(--ts-radius-lg);
    overflow: hidden;
    isolation: isolate;
    min-height: 16rem;
}

    .ts-tile img {
        width: 100%;
        height: 100%;
        object-fit: cover;
        display: block;
    }

.ts-tile__caption {
    position: absolute;
    inset-inline: 0;
    inset-block-end: 0;
    padding: var(--ts-space-lg);
    color: var(--ts-white);
    /* Gradient rather than a flat overlay: keeps 4.5:1 on the text without
       washing out the picture. */
    background: linear-gradient(to top, rgb(16 22 60 / 88%), rgb(16 22 60 / 0%));
}

.ts-tile__title {
    margin: 0 0 var(--ts-space-2xs);
    font-size: 1.25rem;
    font-weight: 600;
    color: var(--ts-white);
}

.ts-tile__more {
    font-size: 0.9rem;
    font-weight: 600;
    color: var(--ts-color-accent);
}

/* --------------------------------------------------------------------- hero */

.ts-hero {
    display: grid;
    gap: var(--ts-space-xl);
    grid-template-columns: 1fr;
    align-items: center;
    padding-inline: var(--ts-gutter);
    padding-block: clamp(2rem, 6vw, 5rem);
}

@media (min-width: 62rem) {
    .ts-hero {
        grid-template-columns: minmax(0, 5fr) minmax(0, 7fr);
    }
}

.ts-hero__title {
    font-size: clamp(1.9rem, 1.2rem + 3vw, 3.5rem);
    font-weight: 300;
    line-height: 1.1;
    color: var(--ts-color-ink);
    margin-block: var(--ts-space-xs) var(--ts-space-md);
}

.ts-hero__media {
    margin: 0;
    border-radius: var(--ts-radius-lg);
    overflow: hidden;
    aspect-ratio: 3 / 2;
    background: var(--ts-color-surface-muted);
}

    .ts-hero__media img {
        width: 100%;
        height: 100%;
        object-fit: cover;
        display: block;
    }

/* Slides stack in the flow and are toggled by [hidden] — no layout plugin. */
.ts-slides > [hidden] {
    display: none;
}

.ts-slides__dots {
    display: flex;
    gap: var(--ts-space-xs);
    padding: 0;
    margin-block-start: var(--ts-space-md);
    list-style: none;
}

.ts-slides__dot {
    width: 0.75rem;
    height: 0.75rem;
    border-radius: 50%;
    border: 1px solid var(--ts-color-primary);
    background: transparent;
    padding: 0;
    cursor: pointer;
}

    .ts-slides__dot[aria-current="true"] {
        background: var(--ts-color-primary);
    }

/* -------------------------------------------------------------------- forms */

.ts-field {
    display: flex;
    flex-direction: column;
    gap: var(--ts-space-2xs);
}

    .ts-field label {
        font-weight: 600;
        color: var(--ts-color-ink);
    }

    .ts-field input,
    .ts-field select,
    .ts-field textarea {
        width: 100%;
        padding: 0.65rem 0.85rem;
        border: 1px solid var(--ts-color-border);
        border-radius: var(--ts-radius);
        background: var(--ts-color-surface);
        color: var(--ts-color-ink);
        font: inherit;
    }

    .ts-field input:user-invalid,
    .ts-field textarea:user-invalid,
    .ts-field select:user-invalid {
        border-color: var(--ts-color-danger);
    }

.ts-field__error,
.field-validation-error {
    color: var(--ts-color-danger);
    font-size: 0.875rem;
}

.ts-form-grid {
    display: grid;
    gap: var(--ts-space-md);
    grid-template-columns: repeat(auto-fit, minmax(min(100%, 18rem), 1fr));
}

.ts-form-grid__full {
    grid-column: 1 / -1;
}

.ts-search {
    display: flex;
    flex-wrap: wrap;
    gap: var(--ts-space-xs);
    align-items: end;
    margin-block-end: var(--ts-space-lg);
}

    .ts-search .ts-field {
        flex: 1 1 18rem;
    }

/* --------------------------------------------------------------- pagination */

.ts-pagination {
    display: flex;
    flex-wrap: wrap;
    justify-content: center;
    gap: var(--ts-space-2xs);
    padding: 0;
    margin-block-start: var(--ts-space-xl);
    list-style: none;
}

    .ts-pagination a,
    .ts-pagination span {
        display: inline-flex;
        align-items: center;
        justify-content: center;
        min-width: 2.5rem;
        min-height: 2.5rem;
        padding-inline: var(--ts-space-xs);
        border: 1px solid var(--ts-color-border);
        border-radius: var(--ts-radius);
        color: var(--ts-color-primary);
    }

    .ts-pagination [aria-current="page"] {
        background: var(--ts-color-primary);
        border-color: var(--ts-color-primary);
        color: var(--ts-white);
    }

    .ts-pagination .ts-pagination__disabled {
        color: var(--ts-color-ink-soft);
        opacity: 0.55;
    }

/* ------------------------------------------------------------------ article */

.ts-article {
    display: grid;
    gap: var(--ts-space-lg);
    grid-template-columns: 1fr;
    align-items: start;
    padding-block: var(--ts-space-xl);
    border-block-end: 1px solid var(--ts-color-border);
}

@media (min-width: 48rem) {
    .ts-article {
        grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
    }

    .ts-article--reverse > :first-child {
        order: 2;
    }
}

.ts-article__media {
    margin: 0;
    border-radius: var(--ts-radius-lg);
    overflow: hidden;
    aspect-ratio: 16 / 10;
}

    .ts-article__media img {
        width: 100%;
        height: 100%;
        object-fit: cover;
        display: block;
    }

.ts-prose {
    max-width: 70ch;
    color: var(--ts-color-ink-soft);
}

    .ts-prose h2,
    .ts-prose h3 {
        color: var(--ts-color-ink);
    }

    .ts-prose img {
        max-width: 100%;
        height: auto;
    }

/* ------------------------------------------------------- details / accordion */

.ts-module {
    border: 1px solid var(--ts-color-border);
    border-radius: var(--ts-radius);
    background: var(--ts-color-surface);
    overflow: hidden;
}

.ts-module__summary {
    display: flex;
    align-items: center;
    gap: var(--ts-space-sm);
    padding: var(--ts-space-md);
    cursor: pointer;
    font-weight: 600;
    color: var(--ts-color-ink);
    list-style: none;
}

    .ts-module__summary::-webkit-details-marker {
        display: none;
    }

    .ts-module__summary::after {
        content: "";
        margin-inline-start: auto;
        width: 0.6rem;
        height: 0.6rem;
        border-right: 2px solid currentColor;
        border-bottom: 2px solid currentColor;
        transform: rotate(45deg);
        transition: transform 0.2s ease;
    }

.ts-module[open] .ts-module__summary::after {
    transform: rotate(-135deg);
}

.ts-module__number {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    min-width: 2rem;
    min-height: 2rem;
    border-radius: var(--ts-radius);
    background: var(--ts-color-ink);
    color: var(--ts-white);
    font-variant-numeric: tabular-nums;
}

.ts-module__number--progress {
    background: var(--ts-color-accent-dark);
}

.ts-module__number--complete {
    background: var(--ts-color-success);
}

.ts-module__slides {
    padding: 0 var(--ts-space-md) var(--ts-space-md) 3.5rem;
    margin: 0;
    list-style: none;
    display: grid;
    gap: var(--ts-space-2xs);
}

.ts-slide--done {
    color: var(--ts-color-success);
}

/* --------------------------------------------------------- details two-pane */

.ts-detail {
    display: grid;
    gap: clamp(1.5rem, 4vw, 3rem);
    grid-template-columns: 1fr;
    align-items: start;
}

@media (min-width: 62rem) {
    .ts-detail {
        grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
    }
}

.ts-panel {
    background: var(--ts-color-surface);
    border-radius: var(--ts-radius-lg);
    padding: clamp(1rem, 3vw, 2rem);
    box-shadow: var(--ts-shadow);
}

.ts-media-16x9 {
    aspect-ratio: 16 / 9;
    width: 100%;
    border-radius: var(--ts-radius);
    background: #000;
}

.ts-map {
    width: 100%;
    aspect-ratio: 16 / 9;
    border: 0;
    border-radius: var(--ts-radius-lg);
}

/* ------------------------------------------------------------------- alerts */

.ts-alert {
    padding: var(--ts-space-md);
    border-radius: var(--ts-radius);
    border: 1px solid transparent;
}

.ts-alert--success {
    background: var(--ts-color-success-soft);
    border-color: var(--ts-feedback-success-border);
    color: var(--ts-color-success);
}

.ts-alert--error {
    background: var(--ts-color-danger-soft);
    border-color: var(--ts-feedback-error-border);
    color: var(--ts-color-danger);
}

.ts-empty {
    padding: var(--ts-space-2xl) 0;
    text-align: center;
    color: var(--ts-color-ink-soft);
}

/* ------------------------------------------------------- site chrome (a11y)
   The header, the navigation and the footer are the template's own markup and
   are styled by main.css / responsive.css. Nothing below restyles them; these
   are the three things the template has no rule for, each of them for
   accessibility:

   1. Signing out and the two preference pickers POST with an antiforgery
      token, so their controls are <button>s where the template used <a>s.
      Strip the user-agent button chrome so they still read as top-bar links.
   2. The mobile toggles are <button>s too — the template's bare <i> cannot be
      reached from the keyboard — so they get the same reset plus a tap target.
   3. The template opens a desktop sub-menu on :hover only, which leaves its
      links unreachable by keyboard; :focus-within opens it as well.
   ------------------------------------------------------------------------- */

.header .top-bar form {
    display: inline-block;
}

.header .top-bar button.top-bar-text {
    background: none;
    border: 0;
    font: inherit;
    color: var(--ts-white);
    transition: all 0.15s;
}

    .header .top-bar .my-account button.top-bar-text {
        padding-inline: 0;
    }

.header .top-bar button.top-bar-text:hover,
.header .top-bar button.top-bar-text:focus {
    color: var(--ts-yellow);
}

button.mobile-nav-toggle {
    background: none;
    border: 0;
    padding: 0;
}

/* The template's toggles are inline <i>s with line-height 0, which on a
   <button> collapses the box to zero height. Centre the glyph in a tap target
   instead — 44 x 44 is the smallest a finger can reliably hit. Scoped to the
   range where responsive.css shows them, so it cannot override the
   `display: none` that hides them from 1200px up. */
@media (max-width: 1199.98px) {
    button.mobile-nav-toggle {
        display: inline-flex;
        align-items: center;
        justify-content: center;
        min-width: 44px;
        min-height: 44px;
    }
}

@media (min-width: 1200px) {
    .navbar .dropdown:focus-within > ul {
        opacity: 1;
        top: calc(100%);
        visibility: visible;
    }
}

/* Images never overflow their column, whatever the template says. */
img {
    max-width: 100%;
    height: auto;
}

/* =======================================================================
   Order and sales funnels, account pages
   -----------------------------------------------------------------------
   Added with the port of the Order / Sales / MyAccount views. The old
   markup laid these screens out with Bootstrap rows and fixed pixel
   widths; these are grid components that reflow on a phone.
   ======================================================================= */

/* --------------------------------------------------------------- steps */

.ts-steps ol {
    list-style: none;
    margin: 0;
    padding: 0;
    display: grid;
    gap: var(--ts-space-2xs);
    grid-template-columns: repeat(auto-fit, minmax(min(100%, 11rem), 1fr));
    counter-reset: none;
}

    .ts-steps li {
        display: flex;
        align-items: center;
        gap: var(--ts-space-xs);
        padding: var(--ts-space-sm) var(--ts-space-md);
        background: var(--ts-color-surface);
        border: 1px solid var(--ts-color-border);
        border-radius: var(--ts-radius);
        color: var(--ts-color-ink-soft);
    }

.ts-steps__number {
    display: grid;
    place-items: center;
    inline-size: 1.75rem;
    block-size: 1.75rem;
    flex: 0 0 auto;
    border-radius: 50%;
    background: var(--ts-color-surface-muted);
    color: var(--ts-color-ink);
    font-weight: 700;
}

.ts-steps li.ts-steps__done {
    color: var(--ts-color-ink);
}

    .ts-steps li.ts-steps__done .ts-steps__number {
        background: var(--ts-color-success);
        color: var(--ts-white);
    }

.ts-steps li.ts-steps__current {
    border-color: var(--ts-color-primary);
    color: var(--ts-color-ink);
    font-weight: 600;
}

    .ts-steps li.ts-steps__current .ts-steps__number {
        background: var(--ts-color-primary);
        color: var(--ts-white);
    }

/* ---------------------------------------------------------------- cart */

.ts-cart {
    list-style: none;
    margin: 0;
    padding: 0;
    display: grid;
    gap: var(--ts-space-md);
}

.ts-cart__line {
    display: grid;
    gap: var(--ts-space-md);
    grid-template-columns: 1fr;
    align-items: start;
    padding-block-end: var(--ts-space-md);
    border-block-end: 1px solid var(--ts-color-border);
}

@media (min-width: 40rem) {
    .ts-cart__line {
        grid-template-columns: auto minmax(0, 1fr) auto;
    }
}

.ts-cart__line--recap {
    grid-template-columns: minmax(0, 1fr) auto;
}

.ts-cart__thumb {
    inline-size: 10rem;
    block-size: auto;
    border-radius: var(--ts-radius);
    object-fit: cover;
}

.ts-cart__body {
    display: grid;
    gap: var(--ts-space-2xs);
    align-content: start;
}

.ts-cart__title {
    margin: 0;
    font-size: 1.05rem;
}

.ts-cart__controls {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: var(--ts-space-xs);
}

.ts-quantity {
    display: inline-flex;
    align-items: center;
    gap: var(--ts-space-2xs);
    border: 1px solid var(--ts-color-border);
    border-radius: var(--ts-radius);
    padding: var(--ts-space-2xs);
}

    .ts-quantity output {
        min-inline-size: 2ch;
        text-align: center;
        font-variant-numeric: tabular-nums;
    }

/* -------------------------------------------------------------- totals */

.ts-totals {
    display: grid;
    grid-template-columns: minmax(0, 1fr) auto;
    gap: var(--ts-space-2xs) var(--ts-space-md);
    margin: 0;
}

    .ts-totals dt {
        color: var(--ts-color-ink-soft);
        font-weight: 400;
    }

    .ts-totals dd {
        margin: 0;
        text-align: end;
        font-weight: 600;
    }

/* --------------------------------------------------------------- table */

/* Wide tables scroll inside their own box; the page itself never does. */
.ts-table-scroll {
    overflow-x: auto;
}

.ts-table {
    inline-size: 100%;
    border-collapse: collapse;
}

    .ts-table th,
    .ts-table td {
        padding: var(--ts-space-xs) var(--ts-space-sm);
        text-align: start;
        border-block-end: 1px solid var(--ts-color-border);
    }

    .ts-table th {
        color: var(--ts-color-ink);
        white-space: nowrap;
    }

/* ------------------------------------------------------ player question */

.ts-code--question {
    font-weight: 600;
    background: var(--ts-color-surface-muted);
    padding: var(--ts-space-sm);
    border-radius: var(--ts-radius);
}

/* ===========================================================================
   PART 2 — Design system completion
   ---------------------------------------------------------------------------
   Part 1 above styles the public funnel. The authenticated area (player,
   dashboard, staff, evaluation) was written against a second set of class
   names that never got a stylesheet: 32 selectors were used across 77 views
   with no rule behind them, so those screens rendered as unstyled HTML.

   This part closes that gap and lifts the whole visual language: depth,
   motion and a type scale, on the palette of the template itself.
   =========================================================================== */

/* --ts-font, --ts-shadow-sm/md/lg, --ts-ease, --ts-transition, and the body
   font-family rule that used to live here now come from brand.css, which
   loads before this file and declares them under the same names — including
   the actual @font-face rules for Plus Jakarta Sans, which this stylesheet
   used to fall back to system fonts without. --ts-color-ink-faint and
   --ts-color-primary-soft are aliased in the :root block above. */

.ts-title, .ts-hero__title {
    font-family: var(--ts-font);
    letter-spacing: -0.018em;
}

/* --------------------------------------------------------- layout primitives */

/* Horizontal group that wraps instead of overflowing — button rows, badge rows
   and toolbars. */
.ts-cluster {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: var(--ts-space-xs);
}

.ts-muted {
    color: var(--ts-color-ink-faint);
}

.ts-grid--2 {
    display: grid;
    gap: var(--ts-space-lg);
    grid-template-columns: 1fr;
}

@media (min-width: 48rem) {
    .ts-grid--2 {
        grid-template-columns: repeat(2, minmax(0, 1fr));
    }
}

/* Page header variants used by the authenticated area. They mirror the public
   .ts-page-header / .ts-title / .ts-eyebrow trio. */
.ts-page__inner {
    width: min(100%, 1600px);
    margin-inline: auto;
}

.ts-page__header {
    display: flex;
    flex-wrap: wrap;
    align-items: end;
    justify-content: space-between;
    gap: var(--ts-space-md);
    margin-block-end: var(--ts-space-lg);
}

.ts-page__eyebrow {
    display: block;
    font-size: 0.82rem;
    font-weight: 700;
    letter-spacing: 0.09em;
    text-transform: uppercase;
    color: var(--ts-color-primary);
}

.ts-page__title {
    margin: var(--ts-space-2xs) 0 0;
    font-size: clamp(1.6rem, 1.1rem + 1.8vw, 2.4rem);
    font-weight: 300;
    line-height: 1.15;
    color: var(--ts-color-ink);
}

.ts-page__lead {
    margin: var(--ts-space-xs) 0 0;
    max-width: 62ch;
    color: var(--ts-color-ink-soft);
}

/* ------------------------------------------------------------------ buttons */

/* Seven views in the authenticated area use .ts-btn while the public funnel
   uses .ts-button. Rather than rewrite either set of views, both names share
   one definition — the result is identical whichever name a view picked. */
.ts-btn {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    gap: var(--ts-space-xs);
    padding: 0.6rem 1.2rem;
    border: 1px solid transparent;
    border-radius: 999px;
    font: inherit;
    font-weight: 600;
    line-height: 1.2;
    text-decoration: none;
    cursor: pointer;
    background: var(--ts-color-primary);
    color: var(--ts-white);
    box-shadow: var(--ts-shadow-sm);
    transition: background-color var(--ts-transition),
                box-shadow var(--ts-transition),
                transform var(--ts-transition);
}

.ts-btn:hover,
.ts-button:hover {
    color: var(--ts-white);
    box-shadow: var(--ts-shadow-md);
    transform: translateY(-1px);
}

.ts-btn:active,
.ts-button:active {
    transform: translateY(0);
    box-shadow: var(--ts-shadow-sm);
}

.ts-btn:hover {
    background: var(--ts-color-primary-dark);
}

.ts-btn--primary {
    background: var(--ts-color-primary);
    color: var(--ts-white);
}

.ts-btn--accent {
    background: var(--ts-color-accent);
    color: var(--ts-color-ink);
}

.ts-btn--accent:hover {
    background: var(--ts-color-accent-dark);
    color: var(--ts-white);
}

.ts-btn--ghost {
    background: transparent;
    color: var(--ts-color-primary);
    border-color: var(--ts-color-border);
    box-shadow: none;
}

.ts-btn--ghost:hover {
    background: var(--ts-color-primary-soft);
    color: var(--ts-color-primary-dark);
    border-color: var(--ts-color-primary);
}

.ts-btn--icon {
    padding: 0.5rem;
    min-width: 2.5rem;
    aspect-ratio: 1;
}

/* A disabled control must not look clickable, and aria-disabled links are
   disabled in the accessibility tree only — style them the same way. */
.ts-btn:disabled,
.ts-button:disabled,
.ts-btn[aria-disabled="true"],
.ts-button[aria-disabled="true"] {
    opacity: 0.45;
    cursor: not-allowed;
    transform: none;
    box-shadow: none;
}

/* --------------------------------------------------------------------- forms */

.ts-form {
    display: grid;
    gap: var(--ts-space-md);
}

.ts-fieldset {
    border: 1px solid var(--ts-color-border);
    border-radius: var(--ts-radius);
    padding: var(--ts-space-lg);
    margin: 0;
}

.ts-fieldset > legend {
    padding-inline: var(--ts-space-xs);
    font-weight: 600;
    color: var(--ts-color-ink);
}

.ts-form__actions {
    display: flex;
    flex-wrap: wrap;
    gap: var(--ts-space-xs);
    align-items: center;
    margin-block-start: var(--ts-space-md);
}

.ts-field__hint {
    display: block;
    margin-block-start: var(--ts-space-2xs);
    font-size: 0.85rem;
    color: var(--ts-color-ink-faint);
}

/* -------------------------------------------------------------------- alerts */

.ts-alert--warning {
    background: var(--ts-color-warning-soft);
    border-color: var(--ts-color-warning);
    color: var(--ts-color-accent-dark);
}

/* ------------------------------------------------------------------ progress */

.ts-progress {
    display: block;
    inline-size: 100%;
    block-size: 0.5rem;
    border: 0;
    border-radius: 999px;
    overflow: hidden;
    background: var(--ts-color-surface-muted);
    appearance: none;
}

.ts-progress::-webkit-progress-bar {
    background: var(--ts-color-surface-muted);
}

.ts-progress::-webkit-progress-value {
    background: linear-gradient(90deg, var(--ts-color-primary), var(--ts-color-primary-dark));
    transition: inline-size var(--ts-transition);
}

.ts-progress::-moz-progress-bar {
    background: var(--ts-color-primary);
}

/* --------------------------------------------------------------------- stats */

.ts-stat {
    display: flex;
    align-items: center;
    gap: var(--ts-space-md);
    padding: var(--ts-space-lg);
    border: 1px solid var(--ts-color-border);
    border-radius: var(--ts-radius-lg);
    background: var(--ts-color-surface);
    box-shadow: var(--ts-shadow-sm);
    transition: box-shadow var(--ts-transition), transform var(--ts-transition);
}

.ts-stat:hover {
    box-shadow: var(--ts-shadow-md);
    transform: translateY(-2px);
}

.ts-stat__icon {
    display: grid;
    place-items: center;
    flex: none;
    inline-size: 3rem;
    block-size: 3rem;
    border-radius: 50%;
    font-size: 1.2rem;
    background: var(--ts-color-primary-soft);
    color: var(--ts-color-primary);
}

.ts-stat__value {
    display: block;
    font-size: clamp(1.5rem, 1.2rem + 1vw, 2.1rem);
    font-weight: 700;
    line-height: 1.1;
    color: var(--ts-color-ink);
    font-variant-numeric: tabular-nums;
}

/* Some tiles carry a word rather than a figure; it must not shout as loud. */
.ts-stat__value--text {
    font-size: 1.1rem;
    font-weight: 600;
}

.ts-stat__label {
    display: block;
    margin-block-start: var(--ts-space-2xs);
    font-size: 0.88rem;
    color: var(--ts-color-ink-faint);
}

/* --------------------------------------------------------------------- tiles */

.ts-tiles {
    display: grid;
    gap: var(--ts-space-lg);
    grid-template-columns: repeat(auto-fill, minmax(min(100%, 17rem), 1fr));
}

.ts-tile__media {
    aspect-ratio: 16 / 9;
    overflow: hidden;
    background: var(--ts-color-surface-muted);
}

.ts-tile__media img {
    inline-size: 100%;
    block-size: 100%;
    object-fit: cover;
    transition: transform 0.4s var(--ts-ease);
}

.ts-tile:hover .ts-tile__media img {
    transform: scale(1.04);
}

.ts-tile__body {
    display: grid;
    gap: var(--ts-space-2xs);
    padding: var(--ts-space-lg);
}

.ts-tile__meta {
    display: flex;
    flex-wrap: wrap;
    gap: var(--ts-space-xs);
    font-size: 0.85rem;
    color: var(--ts-color-ink-faint);
}

.ts-tile__footer {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    justify-content: space-between;
    gap: var(--ts-space-xs);
    padding: var(--ts-space-md) var(--ts-space-lg);
    border-block-start: 1px solid var(--ts-color-border);
    margin-block-start: auto;
}

/* ==================================================================== player */

/* Two panes on a desktop: media and table of contents on the left, the slide
   itself on the right. One column below 64rem, media first. */
.ts-player {
    display: grid;
    gap: var(--ts-space-xl);
    grid-template-columns: 1fr;
    align-items: start;
    padding-inline: var(--ts-gutter);
    padding-block: var(--ts-space-lg);
    width: min(100%, 1600px);
    margin-inline: auto;
}

@media (min-width: 64rem) {
    .ts-player {
        grid-template-columns: minmax(0, 5fr) minmax(0, 7fr);
    }

    /* The slide scrolls; the video and contents stay in view beside it. */
    .ts-player > div:first-of-type {
        position: sticky;
        inset-block-start: var(--ts-space-md);
    }
}

.ts-player__video {
    inline-size: 100%;
    display: block;
    aspect-ratio: 16 / 9;
    border-radius: var(--ts-radius-lg);
    background: #000;
    box-shadow: var(--ts-shadow-md);
}

.ts-player__toolbar {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    justify-content: space-between;
    gap: var(--ts-space-sm);
    padding: var(--ts-space-sm) var(--ts-space-md);
    border: 1px solid var(--ts-color-border);
    border-radius: var(--ts-radius);
    background: var(--ts-color-surface);
}

.ts-player__nav {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--ts-space-md);
    margin-block-end: var(--ts-space-md);
}

.ts-player__nav h2 {
    flex: 1;
    margin: 0;
    text-align: center;
    font-size: 1.05rem;
    font-weight: 600;
    color: var(--ts-color-ink-soft);
}

/* ---------------------------------------------------------------- the slide */

.ts-slide {
    border: 1px solid var(--ts-color-border);
    border-radius: var(--ts-radius-lg);
    background: var(--ts-color-surface);
    box-shadow: var(--ts-shadow-sm);
    overflow: hidden;
}

.ts-slide__header {
    padding: var(--ts-space-lg);
    /* The accent bar is what tells the eye where the teaching content starts. */
    border-block-end: 1px solid var(--ts-color-border);
    border-inline-start: 4px solid var(--ts-color-primary);
    background: linear-gradient(180deg, var(--ts-color-primary-soft), transparent);
}

.ts-slide__header h2 {
    margin: 0;
    font-size: clamp(1.2rem, 1rem + 0.8vw, 1.6rem);
    font-weight: 600;
    color: var(--ts-color-ink);
}

.ts-slide__body {
    display: grid;
    gap: var(--ts-space-md);
    padding: var(--ts-space-lg);
}

/* ------------------------------------------------------- slide time codes */

.ts-code {
    margin: 0;
    line-height: 1.65;
    scroll-margin-block-start: var(--ts-space-xl);
    transition: background-color 0.3s var(--ts-ease);
}

.ts-code--heading {
    font-size: 1.1rem;
    font-weight: 700;
    color: var(--ts-color-ink);
    margin-block-start: var(--ts-space-xs);
}

figure.ts-code {
    margin: 0;
}

.ts-code img {
    inline-size: 100%;
    block-size: auto;
    border-radius: var(--ts-radius);
    box-shadow: var(--ts-shadow-sm);
}

/* player.js marks the time code matching the current video position. */
.ts-code[data-active="true"] {
    background: var(--ts-color-primary-soft);
    box-shadow: -0.75rem 0 0 var(--ts-color-primary-soft),
                 0.75rem 0 0 var(--ts-color-primary-soft);
    border-radius: 2px;
}

/* -------------------------------------------------------------------- dialog */

dialog.ts-card {
    border: 1px solid var(--ts-color-border);
    border-radius: var(--ts-radius-lg);
    box-shadow: var(--ts-shadow-lg);
    padding: var(--ts-space-xl);
    max-inline-size: min(92vw, 32rem);
}

dialog.ts-card::backdrop {
    background: rgb(16 22 60 / 45%);
    backdrop-filter: blur(2px);
}

/* --------------------------------------------------------------- hero, cont. */

/* The landing page opens on the hero, so it carries the brand rather than the
   plain white the template left it on. */
.ts-hero {
    background:
        radial-gradient(60rem 30rem at 85% -10%, rgb(var(--ts-blue-rgb) / 12%), transparent 60%),
        radial-gradient(40rem 24rem at 0% 110%, rgb(var(--ts-yellow-rgb) / 9%), transparent 60%),
        var(--ts-color-surface);
}

/* Shown when no illustration could be resolved: the panel keeps its footprint in
   the grid so the headline does not jump, and draws the brand instead of a hole.
   The mark is CSS-drawn — an <img> here would be one more thing that can fail. */
.ts-hero__media--empty {
    position: relative;
    background:
        linear-gradient(135deg, var(--ts-color-primary) 0%, var(--ts-color-ink) 100%);
    box-shadow: var(--ts-shadow-lg);
}

.ts-hero__media--empty::after {
    content: "";
    position: absolute;
    inset: 0;
    background:
        radial-gradient(18rem 18rem at 78% 22%, rgb(255 255 255 / 16%), transparent 62%),
        radial-gradient(12rem 12rem at 22% 82%, rgb(var(--ts-yellow-rgb) / 26%), transparent 62%);
}

.ts-hero__media img {
    transition: transform 0.6s var(--ts-ease);
}

/* ------------------------------------------------------------- hero fallback

   Gaps the template leaves, not restyling of its components.

   The theme's hero background is a .swiper-slide.bg-image whose picture comes from
   data-image-src. When the CMS block has no blob — blob storage unconfigured or
   unreachable — that attribute is empty and the slide paints nothing, so the caption
   sits on bare white. The slide is kept rather than dropped, otherwise the captions
   and the backgrounds fall out of step and Swiper loops over mismatched pairs.       */

.hero-slider-container .swiper-slide.bg-image--empty {
    background-image: linear-gradient(135deg, var(--ts-color-primary) 0%, var(--ts-color-ink) 100%);
    background-size: cover;
}

/* Below 768 px, responsive.css pulls .hero-slider-container 0.75 rem to the left and
   to the right to cancel the padding Bootstrap's .container-fluid gives it. The
   vendored bootstrap.min.css has --bs-gutter-x commented out of that rule, so there
   is no padding to cancel and the pull hangs the picture 12 px past both edges of the
   viewport instead. Same correction as the me-0 / g-0 the views carry for the same
   reason — it cannot be written as a utility class, because it must apply below the
   breakpoint only. */
@media (max-width: 767.98px) {
    .hero .container-fluid .hero-slider-container {
        margin-left: 0;
        margin-right: 0;
    }
}

/* Same treatment for the catalogue thumbnail: a missing picture must not collapse
   the card's image box and shuffle the grid. */
.tour-item .cropped-image:not([src]),
.tour-item .cropped-image[src=""] {
    background: linear-gradient(135deg, var(--ts-color-border) 0%, var(--ts-color-primary) 100%);
    min-height: 12rem;
}

/* And for the domain tile, where a missing picture is not an accident but the
   normal case: the thesaurus carries no illustration of its own, so
   CatalogueProjections sets DomainSummaryDto.ImageUrl to null on purpose. The
   template's .category-item takes its whole height from the <img> it wraps and
   hangs its caption over it, so a tile without a picture collapses into a bar the
   height of its own title. Same gradient as the catalogue thumbnail above, so the
   two grids fall back to the same thing. */
.category-item .category-item-container img:not([src]),
.category-item .category-item-container img[src=""] {
    background: linear-gradient(135deg, var(--ts-color-border) 0%, var(--ts-color-primary) 100%);
    min-height: 12rem;
}

/* ---------------------------------------------------------------------- pager

   The catalogue pager is Bootstrap's own .pagination. Its active-page tile
   was still Bootstrap's stock #0d6efd — `--bs-primary` is repointed at the
   brand Blue in brand.css, but .pagination re-declares its own
   --bs-pagination-* custom properties locally instead of reading --bs-primary,
   so that global override never reached it. Re-pointing them here too.

   The tap-target box growth (34 x 38 px was under the 44 x 44 px minimum a
   finger can reliably hit) is unrelated and was already fixed. */
.pagination {
    --bs-pagination-color: var(--ts-blue);
    --bs-pagination-border-color: var(--ts-border);
    --bs-pagination-hover-color: var(--ts-blue);
    --bs-pagination-hover-bg: var(--ts-lavender);
    --bs-pagination-hover-border-color: var(--ts-border);
    --bs-pagination-focus-color: var(--ts-blue);
    --bs-pagination-active-color: var(--ts-white);
    --bs-pagination-active-bg: var(--ts-blue);
    --bs-pagination-active-border-color: var(--ts-blue);
}

.pagination-wrapper .page-link {
    display: flex;
    align-items: center;
    justify-content: center;
    min-width: 2.75rem;
    min-height: 2.75rem;
}

/* ------------------------------------------------------- evaluation answers

   Gap the template leaves, not restyling of its components.

   The theme has no questionnaire screen, so it styles no radio or checkbox list:
   the evaluation form falls back to Bootstrap's bare .form-check, whose input is
   1em square and whose label is an inline box the height of one line. Neither
   reaches the 44 x 44 px minimum pointer target, and on a phone the answers of one
   question end up a few pixels apart. Scoped to the evaluation form so the rest of
   the site keeps Bootstrap's own metrics.

   With the accordion gone (a required radio inside a collapsed panel is not
   focusable, so the browser refuses to submit and reports nothing), one question
   and the next had nothing between them but white space. Each question is a boxed
   group now, and its answers are selectable rows — the same `.funnel-provider__option`
   the checkout uses for the payment providers, at question scale.

   `.evaluation-form`, a class and no longer the page's form id: the very same fieldsets
   are rendered a second time inside the player (Views/Evaluation/_EvaluationSteps, on
   both), on the White slide card of the Blue page, and they have to look the same
   there. Blue-tinted blocks with Blue ink on a White card: the contrast is the one the
   standalone page has inside its own `.reservation-box` card. */

.evaluation-form .funnel-question {
    min-width: 0;
    margin: 0 0 1rem;
    padding: 1rem;
    border: 1px solid var(--ts-color-primary-soft);
    border-radius: 0.55rem;
    background: var(--ts-color-primary-soft);
}

/* The panel heading is two lines in French ("Vous n'avez pas encore de compte ?"),
   and .funnel-badge is inline-grid, so the second line ran back under the badge
   instead of under the first word. A flex row gives the text its own column; the
   <legend> next door keeps the inline-grid badge, because a legend cannot be made
   a flex container everywhere. */
.funnel-panel > .contact-title {
    display: flex;
    align-items: center;
}

/* The question is the fieldset's <legend>, and a legend that is not floated is drawn by the
   browser on the fieldset's top edge — half in the block, half out of it, the tinted ground
   cutting through the sentence. Floated (Bootstrap's own reboot does the same) it gives up that
   place and sits inside the block like any other line, above the answers; the full width and
   the clear on the answers underneath keep it a line of its own. */
.evaluation-form .funnel-question__text {
    float: left;
    width: 100%;
    margin-bottom: 0.75rem;
    padding: 0;
    font-size: 1rem;
    font-weight: 600;
    color: var(--ts-color-ink);
}

.evaluation-form .funnel-question__answers {
    display: flex;
    flex-wrap: wrap;
    gap: 0.5rem;
    clear: both;
}

.evaluation-form .funnel-provider__option {
    flex: 0 1 auto;
    padding-block: 0.15rem;
    background: var(--ts-white);
}

.evaluation-form .form-check .form-check-label {
    display: inline-block;
    min-height: 2.5rem;
    padding-block: 0.45rem;
    font-weight: 400;
    line-height: 1.5;
    color: var(--ts-color-ink-soft);
}

.evaluation-form .form-check .form-check-input {
    margin-top: 0.75rem;
}

/* A question still unanswered when Continue was pressed (player.js puts the class on): the
   block goes from the Blue tint to a light red, the site's error red mixed down on the White
   card the way the Blue tint is. Not `--ts-feedback-error-bg`: in learn mode — which the player
   is — that token is written with comma channels in the slash syntax, is dropped by the
   browser, and would leave the block white. The class comes off again the moment the question
   gets its answer. */
.evaluation-form .funnel-question--unanswered {
    border-color: color-mix(in srgb, var(--ts-red) 35%, var(--ts-white));
    background: color-mix(in srgb, var(--ts-red) 8%, var(--ts-white));
}

/* The notice that goes with the red blocks — "answer every question before you continue" — in
   the panel only, between the questions and the footer row, hidden until Continue is pressed
   with a question unanswered. The site's own sentence, in the trainee's language, in place of
   the browser's validation bubble. */
.evaluation-warning {
    display: flex;
    align-items: baseline;
    gap: 0.5rem;
    margin: 0 0 0.75rem;
    font-size: 0.95rem;
    font-weight: 600;
    color: var(--ts-red);
}

/* The remarks box, tall enough for a few sentences. Was an inline style on the textarea. */
.evaluation-form .evaluation-remarks {
    height: 150px;
}

/* --------------------------------------------------- sign-in on small screens

   responsive.css hides the whole left half of the top bar below 992 px:

       @media (max-width: 991.98px) {
           .header .top-bar .top-bar-left, … { display: none }
       }

   That half holds Sign in / My account / Log out, so on a phone there was no way
   to reach the account at all — on a site that sells training. The reference site
   behaves the same way; this is a deliberate departure from it, not an oversight.

   The template's own mobile idiom for the right-hand half is kept as it is: it
   strips the labels and leaves the icons. Nothing here touches that.                */

@media (max-width: 991.98px) {
    .header .top-bar .top-bar-left {
        display: block;
    }

    /* The bar is a single flex row. With the left half back in it, 360 px is not
       wide enough for both groups, so the row is allowed to wrap instead of
       pushing the language and profession pickers off the edge. */
    .header .top-bar .top-bar-container {
        flex-wrap: wrap;
        row-gap: 0.25rem;
    }

    /* responsive.css pushes the right group leftwards with `margin-right: auto`,
       which only makes sense while the left group is hidden. With both present
       the desktop rule — right group to the far end — is the correct one again. */
    .header .top-bar .top-bar-right {
        margin-right: 0;
        margin-left: auto;
    }
}

/* Under ~400 px the two groups each take a full row; keeping the account entry on
   the first one puts it where a thumb reaches first. */
@media (max-width: 400px) {
    .header .top-bar .top-bar-left {
        flex: 1 0 100%;
    }

    .header .top-bar .top-bar-right {
        margin-left: 0;
    }
}

/* ============================================================== contact page

   The template ships this page essentially unstyled: .contact-form, .btn-send and
   .form-floating have no rule at all in main.css, .contact-title none either, and
   .contact-info is only styled inside .our-story-box and .spa-wellnes-box — other
   pages. What is left is Bootstrap's defaults on a grey background.

   So this is spec rule 5: the template is silent, not overridden. Everything below
   is scoped to .contact-content and written in the site's own vocabulary — the
   0.55rem radius it uses everywhere, brand Blue, brand Yellow.                  */

.contact-content .contact-info,
.contact-content .contact-form {
    background: var(--ts-white);
    border-radius: 0.55rem;
    padding: clamp(1.5rem, 1rem + 2vw, 2.5rem);
    box-shadow: 0 1px 2px rgb(var(--ts-blue-rgb) / 6%), 0 8px 24px rgb(var(--ts-blue-rgb) / 8%);
}

.contact-content .contact-title {
    font-size: 1.35rem;
    font-weight: 600;
    color: var(--ts-color-ink);
    margin-bottom: 1.25rem;
}

/* --- the two field groups ------------------------------------------------- */

/* <fieldset> defaults to min-width:min-content, which stops a grid inside it from
   ever shrinking — the row would refuse to reflow on a phone. */
.contact-form__group {
    min-width: 0;
    border: 0;
    padding: 0;
    margin: 0 0 0.5rem;
}

.contact-form__group + .contact-form__group {
    padding-top: 1.5rem;
    border-top: 1px solid var(--ts-color-primary-soft);
}

/* A <legend> is not a flex or grid item and ignores width; float:none + width:100%
   is what makes it behave like an ordinary block heading across browsers. */
.contact-form__group > legend {
    float: none;
    width: 100%;
    padding: 0;
}

/* --- fields --------------------------------------------------------------- */

.contact-content .form-control,
.contact-content .form-select {
    border: 1px solid var(--ts-color-border);
    border-radius: 0.55rem;
    background-color: var(--ts-color-primary-soft);
    transition: border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out, background-color 0.15s ease-in-out;
}

.contact-content .form-control:hover:not(:focus),
.contact-content .form-select:hover:not(:focus) {
    border-color: var(--ts-color-border);
}

/* `box-shadow: none` and not a dropped declaration: Bootstrap's own
   `.form-control:focus` carries a 0.25rem blue halo, so removing the line here
   would hand the field a wider glow than the one it replaced, not no glow. A
   focused field changes its border colour and nothing else. */
.contact-content .form-control:focus,
.contact-content .form-select:focus {
    background-color: var(--ts-white);
    border-color: var(--ts-color-primary);
    box-shadow: none;
}

/* A floating label names the value under it, so it has to read as the smaller of
   the two and start on the same left edge. Neither held: a <label> inherits the
   theme's 1.1rem body size while `.form-control` is Bootstrap's 1rem, so once
   Bootstrap shrank it by its `scale(.85)` the label still came out 15 px against a
   16 px value — the same weight, one line above. And the 1rem indent here was 4 px
   wider than the 0.75rem Bootstrap pads the control with, so the two texts started
   at different points.

   0.9rem × Bootstrap's scale lands the floated label at ~0.77rem, unmistakably
   secondary, and 0.75rem matches the control's own inset: at rest the label sits
   exactly on the value's left edge, and floated it lands within 0.1 px of it —
   `translateX(.15rem)` in Bootstrap's transform is tuned to cancel what the scale
   takes off that 0.75rem. Shrinking the label also opens the gap it used to close
   against the value. Bootstrap's transform is left alone, so the shrink still
   animates. */
.contact-content .form-floating > label {
    color: var(--ts-color-ink-faint);
    font-size: 0.9rem;
    padding-left: 0.75rem;
}

/* --- validation ----------------------------------------------------------- */

/* ASP.NET marks a rejected field with .input-validation-error, Bootstrap styles
   .is-invalid — so a failed server-side post came back with the message in place
   but the field itself looking untouched. This is what ties the two together. */
.contact-content .input-validation-error {
    border-color: var(--ts-color-danger);
    background-color: var(--ts-color-danger-soft);
}

.contact-content .input-validation-error:focus {
    border-color: var(--ts-color-danger);
}

/* The submit script adds .was-validated to the form, which switches on Bootstrap's
   own `.was-validated .form-control:valid:focus` / `:invalid:focus` pair — each with
   a 0.25rem coloured halo of its own. A focused field is meant to change border
   colour and nothing else, so the halo goes here rather than in the vendor file.
   Same specificity as Bootstrap's rules (0,4,0); components.css is the last sheet
   the page loads, so it wins on order. The `.is-valid`/`.is-invalid` forms are the
   same rules for a field marked by script instead of by :valid. */
.contact-content .form-control:valid:focus,
.contact-content .form-control:invalid:focus,
.contact-content .form-control.is-valid:focus,
.contact-content .form-control.is-invalid:focus,
.contact-content .form-select:valid:focus,
.contact-content .form-select:invalid:focus,
.contact-content .form-select.is-valid:focus,
.contact-content .form-select.is-invalid:focus {
    box-shadow: none;
}

.contact-content .invalid-feedback {
    font-size: 0.85rem;
    margin-top: 0.35rem;
}

/* --- footer --------------------------------------------------------------- */

.contact-form__footer {
    padding-top: 1.25rem;
    border-top: 1px solid var(--ts-color-primary-soft);
}

.contact-form__required {
    font-size: 0.9rem;
    font-style: italic;
    color: var(--ts-color-ink-faint);
}

.contact-content .btn-send {
    min-height: 2.75rem;
    padding-inline: 2rem;
}

.contact-content .btn-send i {
    margin-left: 0.5rem;
    color: var(--ts-color-accent);
}

/* A button that only half fills its line reads as unfinished on a phone. */
@media (max-width: 767.98px) {
    .contact-content .btn-send {
        width: 100%;
    }
}

/* --- the address column --------------------------------------------------- */

/* The theme draws .icon-box-img only inside the components that use it on other
   pages, so here the icons sat as bare glyphs against the card. */
.contact-content .contact-info .icon-box-img span {
    display: grid;
    place-items: center;
    inline-size: 2.75rem;
    block-size: 2.75rem;
    border-radius: 50%;
    background: var(--ts-color-primary-soft);
    color: var(--ts-color-primary);
}

.contact-content .contact-info .icon-box-title {
    font-size: 0.8rem;
    font-weight: 700;
    letter-spacing: 0.08em;
    text-transform: uppercase;
    color: var(--ts-color-primary);

    /* "Ondernemingsnummer" is a single eighteen-letter compound with no space to break at,
       and at this size, weight and tracking it runs wider than the label column does at the
       lg breakpoint — where the card is at its narrowest — so it pushed past the card
       instead of wrapping. The other three labels never showed it: two are short and the
       French one carries a space. Dutch compounds are the rule rather than the exception,
       so the label is allowed to hyphenate — _Layout writes the culture into <html lang>,
       which is what selects the Dutch patterns — with a hard break kept as the last resort. */
    overflow-wrap: break-word;
    -webkit-hyphens: auto; /* Safari still wants the prefix for this one. */
    hyphens: auto;
}

.contact-content .contact-info .icon-box-desc {
    margin-bottom: 0;
    font-style: normal;
    color: var(--ts-color-ink-soft);
}

.contact-content .contact-info .icon-box + .icon-box {
    margin-top: 0.25rem;
}

/* --- map ------------------------------------------------------------------ */

.contact-content .map-content {
    margin-top: 2.5rem;
}

.contact-content .map-content iframe {
    border-radius: 0.55rem;
    display: block;
}

/* ==================================================== ordering funnel & account

   What the template leaves undefined on these screens
   ---------------------------------------------------
   main.css gives `.reservation-box .room-item` and `.reservation-detail` a radius
   and a *transparent* border and stops there: a basket line and the page behind
   it are the same flat white, so nothing separates one card from the next.
   `.login-box` / `.register-box` get two declarations in total — a radius and a
   border colour on their `.card`. And, exactly as on the contact page, the form
   vocabulary has no rule at all: `.contact-title`, `.form-label`, `.form-check`,
   `.invalid-feedback`, `.btn-send`, `.form-floating` and the *resting* state of
   `.form-control` are all absent from main.css, which leaves Bootstrap's defaults
   on a Lavender background.

   So this is spec rule 5 again — the template is silent, not overridden. What
   follows is scoped to the three shells these screens use (`.reservation-box`,
   `.login-box`, `.register-box`) plus the ids of the authenticated pages that
   borrow `.tour-home-box`, and is written in the site's own vocabulary: the
   0.55rem radius it uses everywhere, brand Blue, brand Yellow. It is the same
   treatment as the contact page block above, so the checkout and /Contact read
   as one site.

   Working rule: a property main.css already sets on a selector is not restated
   here; only what it leaves out is added. The six deliberate exceptions are
   flagged where they appear — the resting border of `.form-control` /
   `.form-select`, the text colour of `.btn-ht-secondary`, `.btn`'s `white-space`,
   `.btn`'s side padding on the delete button of a basket line, the 48 px glyph
   inside a dashboard tile, and the colour of `h3`. Each is a legibility, an
   overflow or a hierarchy defect rather than a taste preference; three of the six
   are contrast ratios below what WCAG asks for.

   Classes that are ours, not the template's (spec rule 2 — they exist because the
   template has no name for the thing) all carry the `funnel-` prefix, and
   `order-steps__*` for the stepper. The prefix is deliberate: the `ts-` namespace
   of Part 1 and Part 2 above is shared with the catalogue, the player and the
   editorial pages, and a second `.ts-badge` or `.ts-panel` here would silently
   restyle their components.                                                      */

/* --- elevation: white cards on the blue-grey page ------------------------- */

.reservation-box .card,
.login-box .card,
.register-box .card,
#dashboard-box .card,
#staff-details .card {
    box-shadow: 0 1px 2px rgb(var(--ts-blue-rgb) / 6%), 0 8px 24px rgb(var(--ts-blue-rgb) / 8%);
}

/* The recap on the sign-in step reads on the page itself rather than in a panel of
   its own: the sign-in and create-account forms beside it are the card there, and a
   second white box competed with them for the same attention.

   The white comes from Bootstrap's .card (--bs-card-bg) and the shadow from the rule
   just above, so both are undone here; .reservation-box .card is what has to be
   outranked, hence the shell in the selector. The horizontal padding goes too, so
   the figures line up with the heading over them instead of being indented by a
   panel that is no longer drawn.

   Deliberately not applied to the other steps: this one only ever renders signed
   out — Sales/Login redirects an authenticated visitor to the billing step — so the
   page behind it is Lavender and the recap's Blue ink still reads on it. The
   authenticated steps sit on the dark learn-mode background, where the same change
   would leave Blue text on navy. */
.reservation-box .card.reservation-detail--plain {
    background-color: transparent;
    box-shadow: none;
}

    .reservation-box .card.reservation-detail--plain > .card-body {
        padding-inline: 0;
    }

/* The basket lines and the offer tiles are the only cards you act on, so they
   are the only ones that answer the pointer. */
.reservation-box .room-list .room-item {
    transition: box-shadow 0.2s ease-in-out;
}

    .reservation-box .room-list .room-item:hover,
    .reservation-box .room-list .room-item:focus-within {
        box-shadow: 0 2px 6px rgb(var(--ts-blue-rgb) / 8%), 0 16px 36px rgb(var(--ts-blue-rgb) / 12%);
    }

/* main.css draws `.card-selected`'s amber corner tick from a 40 px triangle placed
   outside the card, and relies on `.room-item`'s own `overflow: hidden` to clip it
   back to a corner. Any `.card-selected` that is not also a `.room-item` would
   show the whole triangle floating beside the card. */
.reservation-box .room-list .card.card-selected {
    overflow: hidden;
}

/* --- panel headings ------------------------------------------------------- */

/* `.contact-title` is styled in main.css only inside `.our-story-box` and
   `.spa-wellnes-box`. Every card heading on these pages uses it, so it gets the
   same treatment as on the contact page. */
.reservation-box .contact-title,
.login-box .contact-title,
.register-box .contact-title {
    font-size: 1.35rem;
    font-weight: 600;
    color: var(--ts-color-ink);
    margin-bottom: 1.25rem;
}

/* Checkout's section titles — billing details, the order recap, how to pay — sit
   above their white cards rather than inside them, so the learn-mode rules in
   PART 3 paint them White against the dark page. (Inside a card they are caught
   by the `.card .heading` guard there and stay Blue, which is the intended split.)

   Two margins have to come off for that to read as a title *of* the card below:
   `.heading` is a hero block in main.css and carries 2rem, and `.contact-title`
   carries the 1.25rem it needs when it sits over card content. Bootstrap's own
   `.mb-*` utilities cannot do it — bootstrap.min.css loads before main.css, so a
   utility of equal specificity loses to `.heading`.

   Scoped to `.reservation-main` and not to `.reservation-box`, which would also
   catch the page's own hero heading in `.title-box` and close up the space under
   it. */
.reservation-box .reservation-main .heading.heading-sub {
    margin-bottom: 0.75rem;
}

    .reservation-box .reservation-main .heading.heading-sub .contact-title {
        margin-bottom: 0;
    }

/* The template's own card header idiom, `<h2>` + `<hr>` (customer-information.html). */
.reservation-box .card-body > hr,
.login-box .card-body > hr,
.register-box .card-body > hr,
.reservation-box .book-date hr {
    border-top-color: var(--ts-color-primary-soft);
    opacity: 1;
    margin-block: 1.25rem;
}

/* Exception 5. main.css paints every `h3` #8bb6e0 — 2.3:1 against the white card it
   sits on, where even a large heading needs 3:1. Every section heading of these
   screens and every "Total" line is an `h3`, so on the pages that take money the
   labels were the least readable thing on the page. The replacement is the brand's
   own ink, the colour it already gives `h1`, `h2` and `#choose`. */
.reservation-box h3,
.login-box h3,
.register-box h3,
#dashboard-box h3,
#staff-details h3,
#add-training-box h3,
#staff-training-status h3 {
    color: var(--ts-color-ink);
}

/* main.css styles the "choose your courses" heading by id — `#choose { margin:
   60px 20px }` — and the wrapper chain it used to sit in (`.title-box >
   .container-fluid > .pd-right-30pc`) contained that margin. With the chain gone
   the 60 px collapses out of `.heading` and the list below it climbs over the
   heading text. `display: flow-root` contains it again; the template's own
   spacing is unchanged, it just stays where it belongs. */
.funnel-heading {
    display: flow-root;
}

/* The same box, for the headings main.css does not reach. It styles this heading by
   id, and only the first heading of a page carries `#choose`: the "collections" and
   "packages" headings beside it, and the recap heading of the right rail, fell back
   to Bootstrap's bare `h3` and sat some 60 px above the heading they are meant to
   line up with. The figures below are the ones `#choose` itself declares, so every
   heading of the funnel opens on the same line — which is what the previous version
   of the site did (work item #270, point 2). `#choose` wins on specificity, so
   main.css's own rule is untouched; this only fills the gap beside it. */
/* `h2` as well as `h3` since My account's section headings joined this box. The
   funnel's headings are all h3 because they sit under that page's own h1 with a
   `.sub-title` and nothing else between; My account's five name the blocks the page
   is made of, one level under its h1, so they are h2s. Only the level differs — the
   box is the same. */
.funnel-heading > :is(h2, h3) {
/*    margin: 60px 20px;
*/    font-size: 24px;
    font-weight: 600;
}

/* --- field groups --------------------------------------------------------- */

/* A <fieldset> defaults to min-width:min-content, which stops a grid inside it
   from ever shrinking — the row would refuse to reflow on a phone. */
.funnel-fieldset {
    min-width: 0;
    border: 0;
    padding: 0;
    margin: 0 0 0.5rem;
}

.funnel-fieldset + .funnel-fieldset {
    padding-top: 1.5rem;
    border-top: 1px solid var(--ts-color-primary-soft);
}

/* A <legend> is not a flex or grid item and ignores width; float:none + width:100%
   is what makes it behave like an ordinary block heading across browsers. */
.funnel-fieldset > legend {
    float: none;
    width: 100%;
    padding: 0;
}

/* --- fields --------------------------------------------------------------- */

/* Exception 1. main.css writes `.form-control:focus, .form-select { border-color:
   #8bb6e0; border-width: 0.2rem }` — a rule aimed at the booking widget that sits
   on a photograph — and leaves the resting `.form-control` to Bootstrap. The two
   therefore cannot agree: a select would carry a 3.2 px pale-blue border next to a
   1 px grey input in the same row. The contact page block above already made this
   call; the funnel follows it so the whole site has one field. */
.reservation-box .form-control,
.reservation-box .form-select,
.login-box .form-control,
.login-box .form-select,
.register-box .form-control,
.register-box .form-select {
    border: 1px solid var(--ts-color-border);
    border-radius: 0.55rem;
    background-color: var(--ts-color-primary-soft);
    transition: border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out, background-color 0.15s ease-in-out;
}

.reservation-box .form-control:hover:not(:focus),
.reservation-box .form-select:hover:not(:focus),
.login-box .form-control:hover:not(:focus),
.login-box .form-select:hover:not(:focus),
.register-box .form-control:hover:not(:focus),
.register-box .form-select:hover:not(:focus) {
    border-color: var(--ts-color-border);
}

/* No halo, as on the contact page — and `none` rather than a dropped declaration,
   since Bootstrap's own `.form-control:focus` would otherwise glow in its place;
   see the note there. main.css already answers `.form-select:focus` with
   `box-shadow: unset !important`. Either way the focus ring at the top of this
   file makes the focused control unmistakable. */
.reservation-box .form-control:focus,
.reservation-box .form-select:focus,
.login-box .form-control:focus,
.login-box .form-select:focus,
.register-box .form-control:focus,
.register-box .form-select:focus {
    background-color: var(--ts-white);
    border-color: var(--ts-color-primary);
    box-shadow: none;
}

.reservation-box .form-label,
.login-box .form-label,
.register-box .form-label {
    margin-bottom: 0.35rem;
    font-size: 0.85rem;
    font-weight: 600;
    letter-spacing: 0.02em;
    color: var(--ts-color-ink-soft);
}

/* The asterisk that marks a required field, kept off the label's own baseline. */
.reservation-box .form-label .text-danger,
.login-box .form-label .text-danger,
.register-box .form-label .text-danger {
    margin-left: 0.15rem;
}

/* A floating label is positioned and scaled by Bootstrap; the rule above would
   fight it. Defensive: the views no longer put .form-label inside .form-floating.
   Size and indent are the contact page's — see the note there for why 0.9rem and
   0.75rem: they put the floated label at ~0.77rem against a 1rem value and on the
   same left edge as it, at rest and floated alike. */
.reservation-box .form-floating > label,
.login-box .form-floating > label,
.register-box .form-floating > label {
    margin-bottom: 0;
    font-size: 0.9rem;
    font-weight: 400;
    letter-spacing: normal;
    color: var(--ts-color-ink-faint);
    padding-left: 0.75rem;
}

.reservation-box .form-check-label,
.login-box .form-check-label,
.register-box .form-check-label {
    color: var(--ts-color-ink-soft);
}

.reservation-box .form-check-input:checked,
.login-box .form-check-input:checked,
.register-box .form-check-input:checked {
    background-color: var(--ts-color-primary);
    border-color: var(--ts-color-primary);
}

/* A field you may read but not change. The account page shows four — the two names, and the
   company's VAT number — and Bootstrap's own `[readonly]` background never applies to them: the
   resting rule above sets `background-color` on the same specificity and later in the cascade, so
   a read-only input came out looking exactly like an editable one.

   `readonly` and not `disabled` in the markup, which is why this exists at all: a disabled input
   posts nothing, and the profile endpoint assigns the names with no null guard, so a save that
   stayed silent about them would blank the columns. This gives the disabled *look* to a control
   that still posts, still takes focus and is still announced.

   No new colour: the fill is the page's own Lavender mixed toward white and the ink is brand Blue
   at just over half strength — both computed from the tokens with `color-mix()`, the way brand.css
   derives its own shades.

   Deliberately *not* `var(--ts-color-ink-faint)`, which would have been the obvious choice: that
   token is `rgb(var(--ts-blue-rgb) / 55%)`, and `--ts-blue-rgb` is comma-separated, so it expands to
   `rgb(24, 32, 111 / 55%)` — commas and a slash in one function, which is not valid CSS. The whole
   declaration is dropped and the element keeps whatever it inherited. Every token built that way has
   the same problem (see the note in docs/RESTYLING.md); it is not fixed here because doing so would
   change the rendering of some two hundred other selectors at once. */
.reservation-box .form-control.funnel-readonly,
.login-box .form-control.funnel-readonly,
.register-box .form-control.funnel-readonly {
    background-color: color-mix(in srgb, var(--ts-color-primary-soft) 55%, white);
    color: color-mix(in srgb, var(--ts-color-ink) 60%, white);
    cursor: default;
}

    .reservation-box .form-control.funnel-readonly:focus,
    .login-box .form-control.funnel-readonly:focus,
    .register-box .form-control.funnel-readonly:focus {
        border-color: var(--ts-color-border);
        background-color: color-mix(in srgb, var(--ts-color-primary-soft) 55%, white);
    }

/* --- the show/hide control of a password field ---------------------------- */

/* The template has no such thing: none of its password fields could be revealed. The button sits
   inside the field's box rather than beside it, so the field keeps the full width of its column and
   the row stays aligned with the rows above it. */
.funnel-password {
    position: relative;
}

    /* Room for the button, so a long value never runs underneath it. */
    .funnel-password > .form-control {
        padding-right: 3rem;
    }

    .funnel-password__toggle {
        position: absolute;
        inset-block: 0;
        right: 0;
        display: flex;
        align-items: center;
        justify-content: center;
        width: 3rem;
        padding: 0;
        border: 0;
        border-radius: 0 0.55rem 0.55rem 0;
        background: transparent;
        /* Same derivation, and the same reason, as .funnel-readonly above. */
        color: color-mix(in srgb, var(--ts-color-ink) 60%, white);
        cursor: pointer;
        transition: color 0.15s ease-in-out;
    }

        .funnel-password__toggle:hover,
        .funnel-password__toggle[aria-pressed="true"] {
            color: var(--ts-color-primary);
        }

/* --- validation ----------------------------------------------------------- */

/* ASP.NET marks a rejected field with .input-validation-error and its message with
   .field-validation-error; Bootstrap styles .is-invalid and .invalid-feedback. A
   failed post therefore came back with the message in place but the field itself
   looking untouched, and the message inline with the next field. */
.reservation-box .input-validation-error,
.login-box .input-validation-error,
.register-box .input-validation-error {
    border-color: var(--ts-color-danger);
    background-color: var(--ts-color-danger-soft);
}

.reservation-box .input-validation-error:focus,
.login-box .input-validation-error:focus,
.register-box .input-validation-error:focus {
    border-color: var(--ts-color-danger);
}

/* Bootstrap's own valid/invalid focus halo, removed for the same reason as on the
   contact page — see the note there. */
.reservation-box .form-control:valid:focus,
.reservation-box .form-control:invalid:focus,
.reservation-box .form-control.is-valid:focus,
.reservation-box .form-control.is-invalid:focus,
.reservation-box .form-select:valid:focus,
.reservation-box .form-select:invalid:focus,
.reservation-box .form-select.is-valid:focus,
.reservation-box .form-select.is-invalid:focus,
.login-box .form-control:valid:focus,
.login-box .form-control:invalid:focus,
.login-box .form-control.is-valid:focus,
.login-box .form-control.is-invalid:focus,
.login-box .form-select:valid:focus,
.login-box .form-select:invalid:focus,
.login-box .form-select.is-valid:focus,
.login-box .form-select.is-invalid:focus,
.register-box .form-control:valid:focus,
.register-box .form-control:invalid:focus,
.register-box .form-control.is-valid:focus,
.register-box .form-control.is-invalid:focus,
.register-box .form-select:valid:focus,
.register-box .form-select:invalid:focus,
.register-box .form-select.is-valid:focus,
.register-box .form-select.is-invalid:focus {
    box-shadow: none;
}

.reservation-box .field-validation-error,
.login-box .field-validation-error,
.register-box .field-validation-error,
.reservation-box .invalid-feedback,
.login-box .invalid-feedback,
.register-box .invalid-feedback {
    display: block;
    font-size: 0.85rem;
    margin-top: 0.35rem;
}

.reservation-box .validation-summary-errors,
.login-box .validation-summary-errors,
.register-box .validation-summary-errors {
    margin-bottom: 1.25rem;
    padding: 0.85rem 1rem;
    border: 1px solid rgb(var(--ts-red-rgb) / 25%);
    border-radius: 0.55rem;
    background: var(--ts-color-danger-soft);
}

    .reservation-box .validation-summary-errors ul,
    .login-box .validation-summary-errors ul,
    .register-box .validation-summary-errors ul {
        margin: 0;
        padding-left: 1.1rem;
    }

/* --- the footer row of a form --------------------------------------------- */

/* The template has no such thing: its funnel pages end on a bare `<div>` with an
   inline `margin-top:100px` and a floated button. This is the contact page's
   footer — the required-fields note on the left, the primary action on the right,
   stacked on a phone. */
.funnel-actions {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 0.75rem;
/*    margin-top: 1.75rem;
*/    padding-top: 1.25rem;
    border-top: 0px solid var(--ts-color-primary-soft);
}

.funnel-actions--end {
    justify-content: flex-end;
}

/* An action that sits *outside* the white card has the page itself behind it, and
   once the visitor is signed in that page is the Blue of the learning environment
   (_Layout's isLearnMode). `.btn-ht-primary` is that same brand Blue, so the
   forward button of the funnel was a Blue button on a Blue ground — the one place
   it cannot be seen.

   Out there it carries the theme's pale-blue chip instead, which is `.btn-ht-secondary`
   in the markup: Exception 2 further down already pairs that chip with readable Blue
   ink, so nothing new is invented for the fill.

   Both actions would then be the same button, so back steps down to an outline of
   the same pale blue — same colour, no fill — and forward stays the louder of the
   two. Buttons *inside* a white card are untouched: brand Blue is right on white,
   and this rule never reaches them. */
.reservation-box .funnel-actions .btn-ht-ghost {
    --bs-btn-bg: transparent;
    --bs-btn-border-color: var(--ts-color-primary-soft);
    --bs-btn-color: var(--ts-color-primary-soft);
    --bs-btn-hover-bg: var(--ts-color-primary-soft);
    --bs-btn-hover-border-color: var(--ts-color-primary-soft);
    --bs-btn-hover-color: var(--ts-color-ink);
    --bs-btn-active-bg: var(--ts-color-primary-soft);
    --bs-btn-active-border-color: var(--ts-color-primary-soft);
    --bs-btn-active-color: var(--ts-color-ink);
    border-width: 1px;
    border-style: solid;
}

    /* The arrow of a ghost button follows its label. `.btn-ht-primary > span + i` a
       few hundred lines down turns the forward arrow amber on the solid button; the
       outline one has no fill to carry that against. */
    .reservation-box .funnel-actions .btn-ht-ghost i {
        color: inherit;
    }

/* Pushes everything after it to the far end of the row. */
.funnel-actions__spacer {
    margin-left: auto;
}

.funnel-required {
    margin: 0;
    font-size: 0.9rem;
    font-style: italic;
    color: var(--ts-color-ink-faint);
}

.funnel-note {
    margin: 0 0 1.25rem;
    color: var(--ts-color-ink-soft);
}

/* A tinted panel inside a white card, for the secondary half of a screen: "you do
   not have an account yet" next to the sign-in form. The theme has nothing between
   a `.card` and bare text. */
.funnel-panel {
    display: flex;
    flex-direction: column;
    align-items: flex-start;
    padding: clamp(1.25rem, 1rem + 1vw, 1.75rem);
    border: 1px solid var(--ts-color-primary-soft);
    border-radius: 0.55rem;
    background: var(--ts-color-primary-soft);
}

/* "Pourquoi choisir Teleseminar :" — the five reasons to open an account, inside
   the panel above. The shape is .funnel-steps's: a mark and a sentence that wraps
   under itself. The mark is a rounded square rather than .funnel-badge's circle,
   because a circle at this size is the funnel's "step done" tick and a reason is
   not a step.

   align-self: stretch on both: .funnel-panel is a flex column with
   align-items:flex-start, so without it the list is shrink-wrapped to its longest
   unbreakable word and every line wraps early. */
.funnel-benefits__lead {
    align-self: stretch;
    margin: 0 0 1rem;
    font-weight: 700;
    color: var(--ts-color-primary);
}

.funnel-benefits {
    align-self: stretch;
    list-style: none;
    margin: 0 0 1.5rem;
    padding: 0;
}

    /* The gap below the list is the gap before the "create an account" button that
       follows it on the sign-in page. On the sign-up page there is no button — the
       form itself is the action — and the list is the last thing in the panel, where
       that gap is a strip of empty tint. */
    .funnel-benefits:last-child {
        margin-bottom: 0;
    }

.funnel-benefits__item {
    display: flex;
    align-items: flex-start;
    gap: 0.85rem;
}

    .funnel-benefits__item + .funnel-benefits__item {
        margin-top: 1.1rem;
    }

/* White, not the tint .funnel-badge carries: the tile sits on .funnel-panel, which
   is already --ts-color-primary-soft, so a tinted tile would vanish into it. */
.funnel-benefits__icon {
    display: inline-grid;
    place-items: center;
    flex: 0 0 auto;
    inline-size: 2.5rem;
    block-size: 2.5rem;
    border-radius: 0.5rem;
    background: var(--ts-color-surface);
    color: var(--ts-color-primary);
    font-size: 1rem;
}

.funnel-benefits__title {
    display: block;
    font-weight: 700;
    line-height: 1.35;
    color: var(--ts-color-ink);
}

/* color-mix, not --ts-color-ink-soft: that alias is `rgb(var(--ts-blue-rgb) / 70%)`
   and --ts-blue-rgb is a comma-separated triplet, so the slash makes the whole
   declaration invalid and the text silently comes out at full strength. */
.funnel-benefits__text {
    margin: 0.2rem 0 0;
    font-size: 0.9rem;
    line-height: 1.45;
    color: color-mix(in srgb, var(--ts-color-ink) 78%, var(--ts-white));
}

/* The template opened its "forgot my password" and "add a colleague" panels from a
   script toggling display:none, so it never needed a style for the trigger. A
   native <details> does, and its marker is a 44 px target here. */
.funnel-details > summary {
    display: flex;
    align-items: center;
    gap: 0.5rem;
    min-height: 2.75rem;
    padding: 0.5rem 0;
    font-weight: 600;
    color: var(--ts-color-primary);
    cursor: pointer;
    list-style-position: inside;
}

    .funnel-details > summary:hover {
        color: var(--ts-color-primary);
    }

.funnel-details[open] > summary {
    margin-bottom: 0.25rem;
}

/* --- buttons -------------------------------------------------------------- */

/* Bootstrap leaves a `.btn` an inline-block, so a `min-height` big enough for the
   44 px pointer target does not centre the label inside it and an icon next to the
   label sits on the text baseline rather than beside it. */
.reservation-box .btn,
.login-box .btn,
.register-box .btn,
#staff-details .btn,
#add-training-box .btn {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    min-height: 2.75rem;
}

/* `.btn-send` has no rule in main.css at all; this is the contact page's. */
.reservation-box .btn-send,
.login-box .btn-send,
.register-box .btn-send {
    padding-inline: 2rem;
}

/* The icon of a button, whichever side of the label it is on. */
.reservation-box .btn > span + i,
.login-box .btn > span + i,
.register-box .btn > span + i,
#staff-details .btn > span + i,
#add-training-box .btn > span + i,
.reservation-box .btn > i + span,
.login-box .btn > i + span,
.register-box .btn > i + span,
#staff-details .btn > i + span,
#add-training-box .btn > i + span {
    margin-left: 0.5rem;
}

/* The trailing arrow of a primary action, in the theme's amber — the contact page's
   `.btn-send i`. */
.reservation-box .btn-ht-primary > span + i,
.login-box .btn-ht-primary > span + i,
.register-box .btn-ht-primary > span + i,
#staff-details .btn-ht-primary > span + i {
    color: var(--ts-color-accent);
}

/* Exception 2. main.css gives `.btn-ht-secondary` white text on #ADD8E6 — a 1.7:1
   contrast ratio, which no one can read. Only the text colour is changed, so the
   button keeps the theme's pale-blue chip; the ink is brand Blue. */
.reservation-box .btn-ht-secondary,
.login-box .btn-ht-secondary,
.register-box .btn-ht-secondary,
#staff-details .btn-ht-secondary,
#add-training-box .btn-ht-secondary {
    --bs-btn-color: var(--ts-color-ink);
    --bs-btn-hover-color: var(--ts-color-ink);
    --bs-btn-active-color: var(--ts-color-ink);
    --bs-btn-disabled-color: var(--ts-color-ink);
}

/* Exception 3. `.btn { white-space: nowrap }` plus 2 rem of side padding on each
   side is wider than the column the button lives in as soon as its label runs past
   about twenty characters — "Toevoegen aan winkelmandje" in a 28 % rail, or any
   label at all on a 360 px viewport — and the page then scrolls sideways. Inside
   these shells the label wraps instead. Nothing else about the button changes. */
.reservation-box .btn,
.login-box .btn,
.register-box .btn,
#staff-details .btn,
#add-training-box .btn {
    white-space: normal;
}

/* Exception 6. The 2 rem of side padding `.btn` carries is the figure the theme
   wrote for its page-wide calls to action. On the delete button of a basket line it
   is not: with it the button spans its whole column and reads as the main thing to
   do with the training you have just chosen, which is the defect the work item
   reports (#270, point 4). The padding drops to the figure Bootstrap gives a small
   button; the chip, the ink, the icon and the label are unchanged, and no other
   `.btn` on the page is touched. `max-inline-size` keeps it from stretching back
   out on the phone layout, where the column is the width of the card. */
.reservation-box .room-list .room-item .funnel-remove {
    padding-inline: 0.85rem;
    max-inline-size: 100%;
}

/* A button that only half fills its line reads as unfinished on a phone. */
@media (max-width: 575.98px) {
    .reservation-box .funnel-actions .btn,
    .login-box .funnel-actions .btn,
    .register-box .funnel-actions .btn,
    .reservation-box .book-date > .btn,
    .reservation-box .card-body > .btn {
        width: 100%;
    }
}

/* --- the order stepper ---------------------------------------------------- */

/* main.css colours the amber segments and the pill floated to each segment's right
   end, and stops there: the track *behind* them has no colour of its own, so the
   steps still to come read as a gap in the line rather than as part of it.
   This fills the track and rounds it; the segments themselves are untouched. */
/* No `gap` on the track: main.css leaves `.progress` with `overflow: unset` so the
   pills can hang below it, and four segments of 25 % plus any gap would spill out
   of the page. A white inset separates them without changing a single width. */
.order-steps__track {
    border-radius: 0.15rem;
    background-color: var(--ts-color-primary-soft);
}

    .order-steps__track .progress-bar {
        box-shadow: inset -2px 0 0 0 var(--ts-white);
    }

    .order-steps__track .progress-bar:last-child {
        box-shadow: none;
    }

/* --- the basket line ------------------------------------------------------ */

/* main.css has no stepper at all: the −, the figure and the + were three columns of
   a Bootstrap row, so they drifted apart as the column narrowed and nothing said
   they were one control. */
.funnel-qty {
    display: inline-flex;
    align-items: center;
    gap: 0.25rem;
    padding: 0.25rem;
    border: 1px solid var(--ts-color-border);
    border-radius: 0.55rem;
    background: var(--ts-white);
}

.funnel-qty__btn {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    min-inline-size: 2.5rem;
    min-block-size: 2.5rem;
    padding: 0;
    border: 0;
    border-radius: calc(var(--ts-radius) - 0.15rem);
    background: transparent;
    color: var(--ts-color-primary);
    cursor: pointer;
    transition: background-color 0.15s ease-in-out, color 0.15s ease-in-out;
}

    .funnel-qty__btn:hover:not(:disabled) {
        background: var(--ts-color-primary-soft);
        color: var(--ts-color-primary-dark);
    }

    .funnel-qty__btn:disabled {
        opacity: 0.5;
        cursor: default;
    }

.funnel-qty__value {
    min-inline-size: 2.25rem;
    text-align: center;
    font-weight: 700;
    font-size: 1.1rem;
    color: var(--ts-color-ink);
    font-variant-numeric: tabular-nums;
}

/* The price block of a basket line and of an offer tile. */
.funnel-price {
    display: block;
    font-size: 1.5rem;
    font-weight: 700;
    line-height: 1.2;
    color: var(--ts-color-primary);
}

.funnel-price__unit {
    display: block;
    font-size: 0.9rem;
    font-weight: 400;
    color: var(--ts-color-ink-faint);
}

/* The duration is not a footnote to the price, it is the second figure of the line:
   how many hours you are buying. The previous version of the site set it at the
   amount's own size, and the work item asks for it back (#270, point 4). Carried by
   the basket lines and, since the collections started reporting real hours, by the
   offer tiles too — where the figure is the sum of the trainings the tile lists. */
.funnel-price__unit--lg {
    font-size: 1.5rem;
    font-weight: 700;
    line-height: 1.2;
    color: var(--ts-color-ink);
}

/* The same small unit, but leading the column instead of trailing the price: on an offer
   tile the hours now come first, so they need the gap under them that .price used to
   provide from above. */
.funnel-price__unit--lead {
    margin-bottom: 0.25rem;
}

/* The list of trainings a collection groups. */
.funnel-includes {
    margin: 0;
    padding: 0;
    list-style: none;
    display: grid;
    gap: 0.35rem;
}

    .funnel-includes li {
        display: flex;
        gap: 0.6rem;
        align-items: baseline;
        color: var(--ts-color-ink-soft);
    }

    .funnel-includes i {
        color: var(--ts-color-primary);
        font-size: 0.8rem;
    }

/* The hour counts inside the single "buy a package" tile (work item #271, point 2).
   The template has no name for a row of choices inside an offer tile: it drew them
   as a `.container > .row` of `.col-md-4 .col-sm-6` wrappers, each holding a button
   carrying an inline `width:100%` — a 12-column grid used to get three equal boxes.
   `auto-fit` gets the same three columns and reflows to two and then to one without
   the breakpoints, and an 11 rem floor keeps "Package 10 h" and its amount on one
   line at every width. Only the layout is ours; `.btn-ht-primary` and
   `.package-bts-spans` are the template's own and are left untouched. */
.funnel-packages {
    margin: 1rem 0 0;
    padding: 0;
    list-style: none;
    display: grid;
    /* Fixed 3-up, not auto-fit — keeps 3 packages per row regardless of viewport
       width or how many packages the funnel offers. */
    grid-template-columns: repeat(3, 1fr);
    gap: 0.5rem;
}

/* .btn-sm (bootstrap.min.css) already trims the default .btn padding/font-size; this
   pulls the block height down further and lets the label and price share one row
   instead of stacking, so the tile reads as a low, wide pill rather than a square
   tile — same Blue (.btn-ht-primary, untouched), just less of it per package. */
.funnel-packages__btn {
    display: flex;
    flex-wrap: wrap;
    align-items: baseline;
    justify-content: center;
    column-gap: 0.4rem;
    padding-block: 0.4rem;
    padding-inline: 0.75rem;
    font-size: 0.85rem;
}

/* The amount, beside the hour count it belongs to. main.css sizes `.btn` text for a
   single line of label; the price here needs a price's weight without a heading's
   size inside a button. */
.funnel-packages__price {
    display: inline-block;
    margin-top: 0;
    font-size: 0.85rem;
    font-weight: 700;
}

/* The package(s) the basket already holds. Yellow is the brand accent and always
   carries Blue text (--ts-on-accent), never white — Yellow/white is about 1.8:1.

   It overrides .btn-ht-primary by redeclaring the same Bootstrap variables rather
   than by painting background-color: the button reads --bs-btn-bg for its rest
   state and --bs-btn-hover-bg / --bs-btn-active-bg for the others, so setting the
   colour alone would snap back to Blue the moment the pointer touched it. The
   hover/active shades are the same darker Yellow the accent button uses elsewhere,
   so a selected package still answers the pointer.

   Both classes sit on the same element, so this needs the extra .btn to outrank
   .btn-ht-primary rather than depend on source order. */
.btn.funnel-packages__btn--selected {
    --bs-btn-color: var(--ts-on-accent);
    --bs-btn-bg: var(--ts-accent);
    --bs-btn-border-color: var(--ts-accent);
    --bs-btn-hover-color: var(--ts-on-accent);
    --bs-btn-hover-bg: var(--ts-yellow-dark);
    --bs-btn-hover-border-color: var(--ts-yellow-dark);
    --bs-btn-active-color: var(--ts-on-accent);
    --bs-btn-active-bg: var(--ts-yellow-dark);
    --bs-btn-active-border-color: var(--ts-yellow-dark);
    font-weight: var(--ts-weight-heading);
}

/* A chosen package carries the stepper and the delete button under its own button.
   Each cell is a column so the two sit under the one they belong to, and because the
   cells are grid items they stretch to the tallest in the row — the offer buttons
   stay on one line whether or not the package beside them is chosen. */
.funnel-packages > li {
    display: flex;
    flex-direction: column;
    gap: 0.35rem;
}
.funnel-packages__controls,
.funnel-offer__controls {
    display: flex;
    flex-direction: column;
    gap: 0.35rem;
}

.funnel-packages__total {
    display: flex;
    align-items: baseline;
    justify-content: space-between;
    gap: 0.5rem;
    margin: 0;
    font-size: 0.85rem;
}

    .funnel-packages__total strong {
        font-weight: 700;
        color: var(--ts-blue);
    }

/* Here the stepper has to be as wide as the button above it; on a basket line it hugs
   its own content, which is why .funnel-qty stays inline-flex and this is a modifier
   rather than a change to it. */
.funnel-qty--block {
    display: flex;
    inline-size: 100%;
    justify-content: space-between;
}

/* --- the summary panel ---------------------------------------------------- */

/* On a wide screen the recap follows the basket down the page; below 992 px
   responsive.css stacks the two rails and sticky would be meaningless. */
@media (min-width: 992px) {
    .reservation-box .reservation-right .reservation-detail {
        position: sticky;
        top: 6rem;
    }
}

/* `.extra-info strong` is floated right by main.css and nothing contained the
   float, so a long title climbed over the price of the line below it. */
.reservation-box .reservation-detail .select-list .select-item .extra-info {
    display: flow-root;
    color: var(--ts-color-ink-faint);
    font-size: 0.95rem;
}

.reservation-box .reservation-detail .select-list .select-item + .select-item {
    padding-top: 1rem;
    border-top: 1px solid var(--ts-color-primary-soft);
}

/* The label + figure rows of the recap ("hours", "still available", "invoice
   number"): the views wrote these inline as `line-height:0`, which clipped the
   descenders of the text. */
.funnel-line {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    justify-content: space-between;
    gap: 0.5rem 1rem;
    padding-block: 0.35rem;
}

.funnel-line__label {
    display: flex;
    align-items: center;
    margin: 0;
    font-family: var(--ht-font-default);
    font-size: 1rem;
    font-weight: 500;
    color: var(--ts-color-ink-soft);
}

/* The overview's own opening line ("Nombre d'heures") — sized to match the
   training title below it (.item-title, main.css) instead of the smaller
   default every other funnel-line label uses. */
.funnel-line__label--lg {
    font-size: 1.25rem;
    font-weight: 700;
    color: var(--ts-color-ink);
}

/* margin-left:auto rather than `justify-content: space-between` alone: when the
   label is long enough to wrap the figure onto its own line, space-between has
   nothing left to distribute and the figure lands under the label's first word. */
.funnel-line__value {
    margin-left: auto;
    font-weight: 700;
    color: var(--ts-color-ink);
    white-space: nowrap;
}

/* The tinted round badge of the contact page, at recap scale. `inline-grid` rather
   than `grid` so it can also sit inside a heading or a <legend>, neither of which
   can be turned into a flex container in every browser. */
.funnel-badge {
    display: inline-grid;
    place-items: center;
    flex: 0 0 auto;
    vertical-align: middle;
    inline-size: 2.25rem;
    block-size: 2.25rem;
    margin-inline-end: 0.6rem;
    border-radius: 50%;
    background: var(--ts-color-primary-soft);
    color: var(--ts-color-primary);
    font-size: 0.9rem;
}

/* The invoice download on the funnel-end pages: the PDF mark in red, as a PDF is
   recognised, inside the same blue badge as the receipt lines under it, with a
   little more room before the link text than a recap line needs. */
.funnel-download {
    display: flex;
    align-items: center;
}

.funnel-download .funnel-badge {
    margin-inline-end: 0.85rem;
}

.funnel-download__pdf {
    color: var(--ts-red);
    font-size: 1rem;
}

/* The by-line under a recap line: who gives the training. */
.funnel-meta {
    margin: 0 0 0.25rem;
    font-size: 0.9rem;
    color: var(--ts-color-ink-faint);
}

.funnel-family {
    display: block;
    margin-bottom: 0.5rem;
    font-size: 0.78rem;
    font-weight: 700;
    letter-spacing: 0.08em;
    text-transform: uppercase;
    color: var(--ts-color-primary);
}

.funnel-family--banner {
    margin-block: 1.25rem 0.75rem;
    padding: 0.5rem 0.75rem;
    border-radius: var(--ts-radius);
    background: var(--ts-periwinkle);
    color: var(--ts-blue);
    font-size: 1rem;
    letter-spacing: 0.06em;
}

/* What a recap line says where its price would be, when a package or a collection
   has already paid for it (work item #271, point 4). `.extra-info strong` is
   floated right by main.css and inherits the line's own ink, which would read as an
   amount; the template has no "this costs nothing" token, so this is a tinted chip
   on the same green the funnel already uses for a settled state
   (`.funnel-status__icon--ok`), one step darker in the ink because this text is
   0.8 rem where that badge is an icon — 5.3:1 rather than 4.0:1. Only what main.css
   leaves out is set: the float and the position are still the template's. */
.funnel-covered {
    font-size: 0.8rem;
    font-weight: 700;
    letter-spacing: 0.04em;
    text-transform: uppercase;
    padding: 0.15rem 0.5rem;
    border-radius: 1rem;
    background: var(--ts-color-success-soft);
    color: var(--ts-color-success);
}

/* The colleagues a company order can be assigned to. The template drew each of them
   as a `card room-item` — a full basket-line card for a name and an e-mail. */
.funnel-people {
    list-style: none;
    margin: 0;
    padding: 0;
    display: grid;
    gap: 0.75rem;
}

    .funnel-people li {
        display: flex;
        align-items: center;
    }

        .funnel-people li > span:last-child {
            display: grid;
            min-width: 0;
        }

    .funnel-people strong {
        color: var(--ts-color-ink);
        overflow-wrap: anywhere;
    }

    .funnel-people .funnel-meta {
        margin: 0;
        overflow-wrap: anywhere;
    }

/* --- payment method ------------------------------------------------------- */

/* The provider radios were two bare `<label class="form-label">`s with a
   `.form-check-input` loose inside them, so the control and its text did not line
   up and the pair had no hit area worth the name. */
.funnel-provider {
    display: flex;
    flex-wrap: wrap;
    gap: 0.6rem;
    margin-bottom: 1rem;
}

.funnel-provider__option {
/*    flex: 1 1 8rem;
*/    min-width: 0;
    margin: 0;
    padding: 0.6rem 0.85rem 0.6rem 2.25rem;
    border: 1px solid var(--ts-color-border);
    border-radius: 0.55rem;
    background: var(--ts-color-primary-soft);
    transition: border-color 0.15s ease-in-out, background-color 0.15s ease-in-out;
}

    .funnel-provider__option:hover {
        border-color: var(--ts-color-border);
    }

    .funnel-provider__option:has(.form-check-input:checked) {
        border-color: var(--ts-color-primary);
        background: var(--ts-color-primary-soft);
    }

    .funnel-provider__option .form-check-label {
        font-weight: 600;
        color: var(--ts-color-ink);
        cursor: pointer;
    }

/* --- a status page (paid, cancelled, transfer recorded) ------------------- */

.funnel-status {
    display: flex;
    align-items: center;
    gap: 1rem;
    margin-bottom: 1.25rem;
}

.funnel-status__icon {
    display: grid;
    place-items: center;
    flex: 0 0 auto;
    inline-size: 3.25rem;
    block-size: 3.25rem;
    border-radius: 50%;
    font-size: 1.35rem;
}

.funnel-status__icon--ok {
    background: var(--ts-color-success-soft);
    color: var(--ts-color-success);
}

.funnel-status__icon--warn {
    background: var(--ts-color-warning-soft);
    color: var(--ts-color-accent-dark);
}

.funnel-status__text {
    margin: 0;
    color: var(--ts-color-ink-soft);
}

/* --- the processing panel of the two funnel-end pages --------------------- */

/* "Verwerking van je bestelling": one row per step the order went through, in the
   second card of OrderCompleted and ThankYou. A list, not a table and not a form —
   it is a set of statements about one order, and nothing in it is a cell of
   anything.

   Not .funnel-line: that row is a label with a figure pushed to the far edge,
   which is right for "Vervaldatum  02/09/2026" and wrong for a sentence. These
   rows are a mark and a sentence that wraps under itself, hairline-separated. */
.funnel-steps {
    list-style: none;
    margin: 0;
    padding: 0;
}

.funnel-steps__row {
    display: flex;
    align-items: flex-start;
    gap: 0.75rem;
    padding-block: 0.85rem;
}

    /* Between rows only. A rule above the first would read as a second card edge
       under the heading, and one below the last as a footer the card has not got. */
    .funnel-steps__row + .funnel-steps__row {
        border-top: 1px solid var(--ts-color-primary-soft);
    }

/* The round mark: the same green the funnel already settles on
   (.funnel-status__icon--ok), at row scale rather than badge scale. margin-top
   nudges it onto the cap height of the first line, since align-items:flex-start
   lines it up with the top of the text box and not with the text. */
.funnel-steps__mark {
    display: inline-grid;
    place-items: center;
    flex: 0 0 auto;
    margin-top: 0.1rem;
    inline-size: 1.6rem;
    block-size: 1.6rem;
    font-size: 1.5rem;
    line-height: 1;
}

.funnel-steps__mark--done {
    color: var(--ts-color-success);
}

/* A step that has not happened. Amber and nothing else — no reason, no error and
   no retry reaches the buyer; see the header comment of _OrderAftermath.cshtml.
   The darker accent rather than the plain yellow: this sits on white at icon
   size, where the light one does not carry. */
.funnel-steps__mark--pending {
    color: var(--ts-color-accent-dark);
}

.funnel-steps__label {
    min-width: 0;
    color: var(--ts-color-ink-soft);
    overflow-wrap: anywhere;
}

/* --- tables --------------------------------------------------------------- */

.reservation-box .table > thead th {
    font-size: 0.78rem;
    font-weight: 700;
    letter-spacing: 0.06em;
    text-transform: uppercase;
    color: var(--ts-color-primary);
    border-bottom-color: var(--ts-color-border);
}

.reservation-box .table > tbody > tr > * {
    border-bottom-color: var(--ts-color-primary-soft);
}

.reservation-box .price-number {
    font-weight: 700;
    color: var(--ts-color-ink);
    white-space: nowrap;
}

/* --- the dashboard tiles -------------------------------------------------- */

/* Exception 4. main.css draws `.icon-box .icon-box-img span` as a bare inline box
   and its `i` as a 48 px, 3 rem glyph — the size the theme wants on a marketing
   page with three icons, not on a board of eight figures where the icon then
   outweighs the number it labels. The badge is the contact page's, one size up; it
   cannot exist without resizing the glyph inside it, and the icon cannot sit beside
   the figure while `.icon-box-img` keeps the 1 rem bottom margin of a stack. */
#dashboard-box .icon-box .icon-box-container {
    display: flex;
    align-items: flex-start;
    gap: 1rem;
}

#dashboard-box .icon-box .icon-box-img {
    margin-bottom: 0;
}

    #dashboard-box .icon-box .icon-box-img span {
        display: grid;
        place-items: center;
        inline-size: 3rem;
        block-size: 3rem;
        border-radius: 50%;
        background: var(--ts-color-primary-soft);
        color: var(--ts-color-primary);
    }

        #dashboard-box .icon-box .icon-box-img span i {
            font-size: 1.15rem;
            width: auto;
            height: auto;
        }

#dashboard-box .icon-box .icon-box-info .icon-box-title {
    margin-bottom: 0.15rem;
    color: var(--ts-color-ink);
    font-weight: 700;
}

#dashboard-box .icon-box .icon-box-info .icon-box-desc {
    color: var(--ts-color-ink-faint);
    font-weight: 400;
}

#dashboard-box .card {
    transition: box-shadow 0.2s ease-in-out;
}

    #dashboard-box .card:hover,
    #dashboard-box .card:focus-within {
        box-shadow: 0 2px 6px rgb(var(--ts-blue-rgb) / 8%), 0 16px 36px rgb(var(--ts-blue-rgb) / 12%);
    }

/* --- the staff pages ------------------------------------------------------ */

#staff-training-status .accordion-button:not(.collapsed) {
    background-color: var(--ts-color-primary-soft);
    color: var(--ts-color-ink);
}

#staff-training-status .menu-item {
    display: flex;
    align-items: baseline;
    gap: 0.5rem;
    padding-block: 0.3rem;
    color: var(--ts-color-ink-soft);
}

#staff-training-status .menu-item-completed {
    color: var(--ts-color-success);
}

#add-training-box .add-training {
    color: var(--ts-color-primary);
}

    #add-training-box .add-training:hover:not(:disabled) {
        color: var(--ts-color-primary);
    }

/* ===========================================================================
   PUBLIC PAGES — home, catalogue, domains, collections, experts, detail,
   about, FAQ
   ---------------------------------------------------------------------------
   Everything below is spec rule 5: the template is silent, not overridden. The
   theme is a hotel template and its list pages carry tours and rooms, so the
   components are there, but three kinds of thing are simply absent from it:

     · a card that has to hold a price, a progress bar and a control, on a
       background that is not white;
     · any empty state at all — none of its 24 pages has a list that can come
       back empty;
     · any :focus-visible state on the components it does draw.

   Where a declaration already exists in main.css or responsive.css it is not
   restated here, whatever its value. What is written is what nothing writes:
   the catalogue panel's background, for instance, is `background: #ffff` in
   responsive.css and *commented out* in main.css — so above 1200 px there was
   nothing at all, and this fills that hole rather than contradicting the rule.

   Colours are the brand's own: Blue, Yellow accent, 0.55rem radius, and the
   layered shadow the contact page settled on.                              */

/* ------------------------------------------------------------- shared panel

   .ts-panel is the contact page's card, promoted to a class. Four views drew
   it with the same three inline declarations — background-color, padding: 5%
   and border-radius — and 5 % is a percentage of the column, so the padding ran
   backwards: 14 px inside a phone's narrow column, 36 px inside a desktop's
   wide one. */

.ts-panel {
    background: var(--ts-white);
    border-radius: 0.55rem;
    padding: clamp(1.25rem, 0.9rem + 1.4vw, 2rem);
    box-shadow: 0 1px 2px rgb(var(--ts-blue-rgb) / 6%), 0 8px 24px rgb(var(--ts-blue-rgb) / 8%);
}

/* ------------------------------------------------------------- button arrow

   Ten call-to-action buttons carried `style="color:#FAA916"` on their glyph.
   The template styles `.btn.btn-expand i` — an absolutely positioned box in the
   button's left gutter — but writes no rule for an icon inside an ordinary
   .btn, which is what these are now. The margin is logical so the arrow follows
   the label in every writing direction. */

.btn-arrow > i {
    margin-inline-start: 0.55rem;
    color: var(--ts-color-accent);
}

/* A back button leads with its arrow, so the margin swaps sides. */
.btn-arrow--back > i {
    margin-inline-start: 0;
    margin-inline-end: 0.55rem;
}

/* main.css puts white-space:nowrap on every .btn; a label that has to wrap needs
   the line box back as well as Bootstrap's .text-wrap. */
.btn-arrow.text-wrap {
    line-height: 1.4;
}

/* The expert card's call to action reads from the left, not from the middle.
   Bootstrap centres it with `text-align: center` on .btn — display is
   inline-block, so justify-content would have nothing to act on — and this
   class, level with .btn on specificity but later in the cascade, is what
   lands. Scoped to the card: .btn and .btn-arrow are the whole site's.
   Visible wherever the button is wider than its label — the d-grid full-width
   column below lg, and any line the wrapped label does not fill. */
.speaker-card__cta {
    text-align: start;
}

/* ------------------------------------------------------------ status filter */

/* Catalogue.cshtml carries `mb-8` for the gap under the filter form, but
   Bootstrap's spacer scale stops at `mb-5` — `mb-8` was never a real class,
   so the filter sat flush against the results grid below it. */
.ts-status-filter {
    margin-block-end: var(--ts-space-xl);
}

/* ============================================================ catalogue card

   The card is .tour-item, shared by the catalogue, the experts grid, the
   collections on a detail page and the About articles. */

/* --- the picture ---------------------------------------------------------- */

/* main.css gives `.tour-list .tour-item .tour-item-content .image img` a radius
   and a transition and nothing else, and its own `figure .img-fluid { height:
   auto }` outranks the `.cropped-image { height: 200px }` that was meant to
   normalise the thumbnails — so every card was as tall as its own photograph
   and no two cards in a row ended level. A ratio box is what the template never
   writes; height stays `auto`, which is what lets aspect-ratio resolve it. */
.tour-list .tour-item .tour-item-content .image img {
    display: block;
    width: 100%;
    aspect-ratio: 16 / 10;
    object-fit: cover;
    object-position: center top;
}

/* A thumbnail that never arrives must not collapse the box and shuffle the
   grid; the ratio holds the footprint, this paints it.

   The ratio is written here as well as on the img, and for the same reason: a
   card whose model carries no picture writes no img at all — an empty `src` is
   a request for the page itself, not an absent picture — so with the ratio on
   the child alone the figure had no height left to hold. Both boxes resolve to
   the same height, the child filling the parent, so the rule is a no-op on
   every card that does have a photograph. */
.tour-list .tour-item .tour-item-content .image {
    aspect-ratio: 16 / 10;
    background: linear-gradient(135deg, var(--ts-color-border) 0%, var(--ts-color-primary) 100%);
}

/* --- the panel ------------------------------------------------------------ */

/* `background: #ffff` is commented out of this rule in main.css and present in
   responsive.css, so the panel that hangs over the photograph was solid white
   below 1200 px and transparent above it — which is why the same card looked
   finished on a phone and unfinished on a desktop. .transparent-content, the
   0.92-alpha class the views used to paper over it, is gone from the markup.

   The shadow is the contact page's: the panel is a card on a coloured page and
   needs to sit above it, and the template never gives .tour-item a shadow of
   any kind. */
.tour-list .tour-item .tour-item-content .content {
    background-color: var(--ts-white);
    box-shadow: 0 1px 2px rgb(var(--ts-blue-rgb) / 6%), 0 8px 24px rgb(var(--ts-blue-rgb) / 8%);
}

.tour-list .tour-item:hover .tour-item-content .content,
.tour-list .tour-item:focus-within .tour-item-content .content {
    box-shadow: 0 2px 6px rgb(var(--ts-blue-rgb) / 10%), 0 18px 40px rgb(var(--ts-blue-rgb) / 14%);
}

/* The two state tints, on the panel of a card the signed-in trainee holds a licence for: warm
   while the training is under way, green once it is finished or certified. Both are the tokens the
   "Certificate" pill these states replace painted itself with, so the card stays in the family the
   site already had for "in progress" and "done", and no new hex enters the file.

   Painted as a layer *over* White rather than as the background colour itself. In learn mode the
   tokens are a translucent Yellow and Green, and this panel hangs 100 px over the photograph — set
   as the background colour they would let the picture through behind the text. Over White they
   composite to the same tint public mode states outright.

   The full six-class chain, not a shorter selector: responsive.css paints this same element
   `background: var(--ts-white)` below 1200 px through exactly this chain, and components.css only
   wins on load order, which needs equal specificity.

   --ts-card-tint carries the colour so the progress ring inside can punch its hole in it rather
   than in White, which is what makes the hole read as a hole and not as a white disc lying on the
   tint. Custom properties inherit, so the ring picks it up wherever in the panel it is drawn. */
.tour-list .tour-item .tour-item-content .content.ts-tour-state--busy,
.tour-list .tour-item .tour-item-content .content.ts-tour-state--done {
    background-color: var(--ts-white);
    background-image: linear-gradient(var(--ts-card-tint), var(--ts-card-tint));
    --ts-progress-ring-hole: var(--ts-card-tint);

    /* brand.css's hard rule — "text inside a White surface is always Blue" — applies to this
       panel's accents too: it is White in both modes, so the ring's unfilled track has to stay the
       light Periwinkle. Left to --ts-border it would take --ts-blue-rule in learn mode, which is
       88 % Blue: a near-black arc on a pale card. The detail page's own ring is deliberately not
       overridden, because there the block sits straight on the dark page. */
    --ts-progress-ring-track: var(--ts-periwinkle);
}

/* --ts-feedback-*-bg and not the --ts-color-*-soft aliases at the top of this file, although those
   are the names the rest of the file uses. The aliases are declared on `:root` as
   `var(--ts-feedback-warning-bg)`, and the feedback tokens are declared one level down, on
   .ts-mode-public / .ts-mode-learn — i.e. on <body>. A custom property is substituted on the element
   its declaration applies to, so at :root the referenced token does not exist, the alias takes the
   guaranteed-invalid value, and it inherits as invalid to every element below: on the running site
   `getComputedStyle(document.body).getPropertyValue('--ts-color-warning-soft')` is the empty string.
   Every declaration reading one of the four feedback families (success/warning/danger/info, soft and
   solid) is therefore silently dropped today. Fixing that means moving the aliases into the two mode
   blocks, which would light up dormant colours across the whole site at once — a change of its own,
   not a side effect of this one. These three read the mode tokens directly instead. */
.tour-list .tour-item .tour-item-content .content.ts-tour-state--busy {
    --ts-card-tint: var(--ts-feedback-warning-bg);
}

.tour-list .tour-item .tour-item-content .content.ts-tour-state--done {
    --ts-card-tint: var(--ts-feedback-success-bg);

    /* A ring that has come all the way round is not "in progress" any more, so it closes in Blue
       rather than in the Yellow that tracks progress. It is the only full ring on the site — every
       other one is on a training still under way. */
    --ts-progress-ring-fill: var(--ts-blue);
}

/* main.css sets the panel's padding to 15px, where the template it came from
   used 35px and responsive.css still does below 1200 px. 15 px round a title, a
   teaser and a price row is the one measurement that makes the desktop card
   read as cramped next to its own phone version — and it is the padding of the
   block below the fold of the picture, so the extra room costs nothing. */
@media (min-width: 1200px) {
    .tour-list .tour-item .tour-item-content .content {
        padding: 1.75rem;
    }
}

/* --- the title ------------------------------------------------------------ */

/* main.css colours `.heading .title a`, and the card used to wrap the anchor
   round the heading instead of putting it inside — so nothing matched and the
   h3 fell through to the global `h3 { color: #8bb6e0 }`, 2.1:1 on white. The
   markup nests it the template's way now. What is still missing is the hover:
   the theme recolours `.title` when the card is hovered but never the `a`
   inside it, which then keeps its own colour and never moves. */
.tour-list .tour-item .tour-item-content .content .heading .title a {
    transition: color 0.2s ease-in-out;
}

.tour-list .tour-item:hover .tour-item-content .content .heading .title a,
.tour-list .tour-item .tour-item-content .content .heading .title a:focus-visible {
    color: var(--ht-color-primary);
}

.tour-list .tour-item .tour-item-content .content .heading .title {
    margin-bottom: 0.5rem;
}

/* --- the row under the teaser -------------------------------------------- */

/* Duration, price and control sit on one line and wrap together on a narrow
   card. The template's .description is a plain block: it has no rule for the
   line of metadata the catalogue card ends on. */
.tour-list .tour-item .tour-item-content .content .description {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 0.5rem 1rem;
}

/* On the training card the teaser is alone in this row — the basket pill moved down to
   .tour-footer, beside the duration — so it grows to the panel's whole width. `1 1 55%` still
   matters where a control does share the row (the collection cards): it keeps the teaser the wider
   of the two and lets it drop below the pill on a card too narrow to hold both. The old
   `> .progress` half of this rule went with the linear bar the ring replaced. */
.tour-list .tour-item .tour-item-content .content .description > p {
    flex: 1 1 55%;
    margin-bottom: 0.25rem;
}

/* The duration reads as context rather than a headline — lighter weight, softer
   ink — but at the same text size as the price beside it, so the two halves of
   the row sit on one type scale. 1.05rem is what the price actually renders at:
   the pill sets that size and .tour-price inherits it, in place of the theme's
   generic .price-number size.

   `span…:not(.add-training)` rather than the `:first-of-type` this used to
   select on. Every control in this row carries .price-number, and
   :first-of-type counts per element type — so it also matched the certificate,
   the row's only <a>, and painted it ink-soft over its own state colour. The
   two things it must not match are the basket pill (a sibling span) and the
   certificate (an anchor); this excludes each by what it is. */
.tour-list .tour-item .tour-item-content .content .description > span.price-number:not(.add-training) {
    font-size: 1.05rem;
    font-weight: 600;
    color: var(--ts-color-ink-soft);
    margin-left: 0;
}

/* --- the state indicator --------------------------------------------------- */

/* The ring is the dashboard's .ts-progress-ring, instance-sized here: --ts-progress-ring-size is
   declared on the component itself, so it is a local override rather than a second component.
   margin-left: auto pins it to the right of the duration's line in .tour-footer, which is the slot
   the price pill takes on an unlicensed card — the two never appear together, so they share it. */
.tour-list .tour-item .tour-item-content .content .tour-indicator {
    --ts-progress-ring-size: 3.5rem;
    position: relative;
    z-index: 2;
    margin-left: auto;
    flex: none;
}

/* State 5 puts a certificate in the ring's place, at the ring's footprint, so the row keeps its
   height and nothing reflows when a trainee moves from "finished" to "certified". */
.tour-indicator--certificate {
    display: grid;
    place-items: center;
    inline-size: 3.5rem;
    block-size: 3.5rem;
    font-size: 2rem;

    /* The mode token, not --ts-color-success — see the tint block above for why that alias resolves
       to nothing. On the detail page this is the right value: in learn mode that block sits on the
       dark page, which is what --ts-green-on-blue exists for. */
    color: var(--ts-feedback-success);
}

/* On a card it is not. The panel is White in both modes, so the learn-mode green — #58ce95,
   lightened to stay legible *on Blue* — lands at 1.97:1 on the pale green tint. The light-surface
   Green is 5.21:1 on the same tint, and is what public mode already draws. */
.tour-list .tour-item .tour-item-content .content .tour-indicator--certificate {
    color: var(--ts-green);
}

/* --- the action row ------------------------------------------------------- */

/* Duration on the left, the state's one action on the right. A row of its own under the
   description, which is what makes the button line up across a grid of cards whose teasers are of
   different lengths. */
.tour-list .tour-item .tour-item-content .content .tour-footer {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    justify-content: space-between;
    gap: 0.5rem 1rem;
    margin-top: 1rem;
}

/* Same treatment the duration had while it lived in .description: context weight, softer ink, at
   the price's own size. Written through the full chain for the same reason it was there — the
   element carries .price-number, and main.css's five-class rule for that class outranks anything
   shorter.

   --ts-color-ink-soft is kept verbatim from the rule this replaces, although it currently paints
   nothing: its value is `rgb(var(--ts-blue-rgb) / 70%)` and --ts-blue-rgb is comma-separated, so
   mixing the two syntaxes makes the declaration invalid and the ink falls through to the inherited
   Blue. That is exactly what the duration looks like today, and changing it here would be a restyle
   nobody asked for; the fix belongs with the token.

   On .tour-footer__duration rather than on .price-number: the basket pill joins this row when the
   card also offers credits, and it carries .price-number too — six classes here against the pill's
   own five, so a `.price-number` selector would take its `margin-left: auto` away and paint its
   label with the duration's ink. */
.tour-list .tour-item .tour-item-content .content .tour-footer .tour-footer__duration {
    font-size: 1.05rem;
    font-weight: 600;
    color: var(--ts-color-ink-soft);
    margin-left: 0;
}

/* Three controls do not fit one row: with the price or the state indicator beside it the button had
   nowhere to go but a second line anyway, so it takes that line whole. `1 0 100%` is what puts it
   there — a flex item that wide cannot share a line — and margin-left: 0 undoes the `auto`
   .tour-action uses to pin itself right when it is the row's second item.

   The last three declarations are what keeps that line *inside* the card. `.btn` sets
   `white-space: nowrap` and 2rem of padding on each side, and a flex item's default
   `min-width: auto` refuses to shrink below that intrinsic width — so on a three-up grid the
   longest label ("Toevoegen aan mijn opleidingen") pushed the button out through the panel's right
   edge. min-width: 0 lets it shrink to the row, white-space: normal lets the label take a second
   line instead of a second card's worth of width, and the narrower padding-inline buys back enough
   room that it usually does not have to. */
.tour-list .tour-item .tour-item-content .content .tour-footer--stacked .tour-action {
    flex: 1 0 100%;
    margin-left: 0;
    text-align: center;
    min-width: 0;
    white-space: normal;
    padding-inline: 1rem;
}

/* Every control inside .content has to be lifted above the title's .stretched-link, which covers
   the whole panel and would otherwise swallow the click — the same two declarations .add-training
   already carries. margin-left: auto rather than the row's justify-content alone, so the button
   stays at the right edge when the duration wraps away from it. */
.tour-list .tour-item .tour-item-content .content .tour-action {
    position: relative;
    z-index: 2;
    margin-left: auto;
}

.tour-list .tour-item .tour-item-content .content .tour-action:disabled {
    cursor: progress;
    opacity: 0.65;
}

/* --- the detail page's snapshot ------------------------------------------- */

/* The same teaser-beside-indicator row the card draws, so the two pages read alike. */
.ts-snapshot__lead {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 0.5rem 1rem;
}

.ts-snapshot__lead > p {
    flex: 1 1 55%;
    margin-bottom: 0;
}

.ts-snapshot__lead .tour-indicator {
    --ts-progress-ring-size: 4rem;
    margin-left: auto;
    flex: none;
}

/* The action sits under the price rather than beside it: on an unlicensed training both ways to
   the same licence are open — pay for it, or spend the organisation's hours — and stacking them
   is what says the second is an alternative to the first rather than a variant of it. */
.ts-snapshot__actions {
    display: flex;
    flex-wrap: wrap;
    gap: 0.75rem;
    margin-top: 1rem;
}

.ts-snapshot__actions .btn:disabled {
    cursor: progress;
    opacity: 0.65;
}

/* --- add to basket -------------------------------------------------------- */

/* .add-training is ours — teleseminar.js binds the basket on data-add-training
   and the template has no such control — so nothing styled it: it was a bare
   price with a glyph after it, indistinguishable from the duration beside it.

   z-index lifts it back above the title's stretched link, which otherwise
   covers the whole card and would swallow the click.

   The selector spells out the whole .tour-item-content .content chain for one
   reason: the pill's markup also carries .price-number, so main.css's
   `.tour-list .tour-item .tour-item-content .content .price-number` (5 classes)
   was beating a plain `.tour-list .tour-item .add-training` (3) and neither the
   size nor `margin-left: auto` below ever applied — the pill rendered at the
   theme's 1.55rem and at `margin-left: 0.25rem`, i.e. parked next to the
   duration instead of at the card's right edge. Same specificity, later file,
   so this wins on order rather than on !important.

   .ts-cart-pill rides along: the detail page's related-collection cards end on
   the same row with the same "price, then basket glyph" control, only posting
   through _CartScripts' data-cart-op instead of data-add-training. Same slot,
   same look, so it is the same rule rather than a second copy of these twelve
   declarations. :is() takes the specificity of its heaviest argument and both
   are single classes, so everything the paragraphs above weigh up is unchanged. */
.tour-list .tour-item .tour-item-content .content :is(.add-training, .ts-cart-pill) {
    position: relative;
    z-index: 2;
    display: inline-flex;
    align-items: center;
    gap: 0.5rem;
    margin-left: auto;
    padding: 0.4rem 0.85rem;
    border-radius: 999px;
    background: var(--ts-color-primary-soft);
    color: var(--ts-color-primary);
    font-size: 1.05rem;
    line-height: 1.2;
    transition: background-color 0.2s ease-in-out, color 0.2s ease-in-out;
}

/* main.css's `.tour-item-content .content .price-number` (5 classes) outranks
   a plain `.add-training .tour-price` (4) and pins this back to a fixed Blue
   regardless of state — invisible once :hover turns the pill's own
   background that same Blue. Matching its ancestor chain here so this wins
   on specificity instead of needing !important. */
.tour-list .tour-item .tour-item-content .content :is(.add-training, .ts-cart-pill) .tour-price {
    font-size: inherit;
    color: inherit;
    margin-left: 0;
}

.tour-list .tour-item :is(.add-training, .ts-cart-pill):hover,
.tour-list .tour-item :is(.add-training, .ts-cart-pill):focus-visible {
    background: var(--ts-color-primary);
    color: var(--ts-white);
}

/* The collection pill is a real <button> where .add-training is a span with
   role="button", so it starts from the UA's own button: a border, a grey
   background and — the one a shorthand-less reset always misses — the system
   font at the system size, neither of which is inherited. The rule above paints
   and sizes it; this only undoes what the element brings of its own.

   _CartScripts disables the button for the length of its POST, and an
   unstyled :disabled reads as "nothing happened" on a control that keeps its
   colours. */
.ts-cart-pill {
    border: 0;
    font-family: inherit;
    font-weight: inherit;
    cursor: pointer;
}

.ts-cart-pill:disabled {
    cursor: progress;
    opacity: 0.65;
}

/* --- certificate ---------------------------------------------------------- */

/* .tour-certificate — the amber/green "Certificate" pill — is gone with the markup that drew it.
   It said two different things in the two places it appeared: keyed on the licence status on the
   card and on PercentageFollowed on the detail page, so a licence merely watched to the end showed
   an amber pill on one and a green one on the other. The five states replace it: the panel now
   carries the colour and the indicator slot carries the certificate. */

/* --- the card on the expert page ------------------------------------------ */

/* SpeakerDetails/Index.cshtml puts .ts-expert-trainings on both sections that hold a grid — the
   trainings and, under them, the collections those trainings belong to, so the two read the same —
   and these three rules are the whole difference between the card there and the card everywhere else:
   a larger duration and a price pinned to the right edge. They live under that class because
   _Training.cshtml is rendered from six view folders — writing them on the card itself would move
   the price on the catalogue, the home page, the domain and collection pages and the About and
   Staff grids too, which is not what was asked for.

   Each selector carries one class more than main.css's five-class
   `.tour-item-content .content .price-number`, which is what makes them apply at all: the pill and
   the duration both carry .price-number, so the theme's `margin-left: 0.25rem` outranks the
   `margin-left: auto` the shared .add-training block declares, and the pill sits beside the
   duration instead of at the right of the row.

   The duration moved out of .description and into .tour-footer with the five states, so the first
   selector follows it there and names .tour-footer__duration outright: on a card that offers
   credits the price pill shares that row, and any `.price-number` selector here would match it too
   — the one thing this was written to leave alone — and blow it up to 1.25rem on this page only. */
.ts-expert-trainings .tour-list .tour-item .tour-item-content .content .tour-footer .tour-footer__duration {
    font-size: 1.25rem;
}

.ts-expert-trainings .tour-list .tour-item .tour-item-content .content .add-training {
    margin-left: auto;
}

/* --- progress ------------------------------------------------------------- */

/* .tour-progress, the slim linear bar, is gone with the markup that drew it. It was the catalogue
   card's and the detail page's progress indicator, and both draw .ts-progress-ring now — the same
   circle the dashboard has always used, which is what the five states are keyed on. Nothing else
   in the solution referenced the class. */

/* --- a card that is not a link -------------------------------------------- */

/* The About articles use the same component but go nowhere, and the template's
   hover — the panel jumping 50 px up the picture — is movement for its own sake
   on a card with nothing to click. Ours, not the template's: it exists to stop
   a template behaviour on one page, so it is a class on the markup rather than
   a page-scoped override. */
.tour-list .tour-item.ts-tour-item--static:hover .tour-item-content {
    margin-bottom: 0;
}

.tour-list .tour-item.ts-tour-item--static:hover .tour-item-content .content {
    margin-top: -50px;
}

.tour-list .tour-item.ts-tour-item--static .tour-item-content .content .description {
    display: block;
}

/* ========================================================== category tiles

   The domain and collection tiles. */

/* main.css takes the tile's height from the <img> it wraps and hangs the
   caption over it — so a domain without an illustration (the thesaurus has
   none by design) collapsed to a bar the height of its own title, next to
   tiles 250 px tall. The ratio goes on the container, not the picture, so the
   tile keeps its footprint whether or not there is anything to load, and the
   gradient underneath is what shows when there is not. */
.category-item .category-item-container {
    aspect-ratio: 3 / 2;
    background: linear-gradient(135deg, var(--ts-color-primary) 0%, var(--ts-color-ink) 100%);
    box-shadow: 0 1px 2px rgb(var(--ts-blue-rgb) / 6%), 0 8px 24px rgb(var(--ts-blue-rgb) / 8%);
}

.category-item .category-item-container img {
    block-size: 100%;
    object-fit: cover;
    display: block;
}

/* The title is a 2rem heading in a 2rem-padded box; on a narrow tile a long
   domain name filled it edge to edge. */
.category-item .category-item-container .category-item-title {
    font-size: clamp(1.35rem, 1.1rem + 0.8vw, 1.85rem);
    line-height: 1.25;
    text-shadow: 0 1px 12px rgb(9 12 40 / 45%);
}

/* The template reveals the "explore" line on :hover only, which leaves a
   keyboard with no equivalent — it writes no :focus-visible rule anywhere. The
   three declarations mirror its own hover state exactly. */
.category-item .category-item-container:focus-visible .category-item-overlay {
    background: rgba(30, 34, 40, 0.65);
}

.category-item .category-item-container:focus-visible .category-item-title {
    bottom: 5rem;
}

.category-item .category-item-container:focus-visible .category-item-detail {
    bottom: 1.15rem;
    opacity: 1;
}

/* The arrow is Font Awesome now — the hotel-icon set has no right-hand twin for
   its circled left arrow — and needs the spacing the ficon carried itself. */
.category-item .category-item-detail i {
    margin-inline-start: 0.5rem;
    color: var(--ts-color-accent);
}

/* ============================================================= empty states

   The template has none: not one of its 24 pages has a list that can come back
   empty, so a catalogue with no results, a FAQ section with nothing published
   and a training with no programme all rendered as one loose sentence on the
   page background — indistinguishable from a page that had failed. */

.ts-empty-state {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 1rem;
    max-width: 34rem;
    margin-inline: auto;
    padding: clamp(2rem, 1.5rem + 2vw, 3rem) 1.5rem;
    text-align: center;
    background: var(--ts-white);
    border-radius: 0.55rem;
    box-shadow: 0 1px 2px rgb(var(--ts-blue-rgb) / 6%), 0 8px 24px rgb(var(--ts-blue-rgb) / 8%);
}

/* Inside a column that is already a panel — the detail page's programme — the
   state is the message alone, without a second card round it. */
.ts-empty-state--inline {
    max-width: none;
    margin-top: 1rem;
    padding: 1.75rem 1.5rem;
}

.ts-empty-state__badge {
    display: grid;
    place-items: center;
    inline-size: 3.25rem;
    block-size: 3.25rem;
    border-radius: 50%;
    background: var(--ts-color-primary-soft);
    color: var(--ts-color-primary);
    font-size: 1.3rem;
}

.ts-empty-state__text {
    margin-bottom: 0;
    font-size: 1.1rem;
    color: var(--ts-color-ink-soft);
}

/* ================================================================ home page */

/* The profession question is ours — the template asks for nothing on its
   landing page — and it was a label, a select and a button loose on the page
   background. The panel is what makes them read as one question. */
.ts-profession-panel {
    max-width: 46rem;
    padding: clamp(1.5rem, 1.1rem + 1.6vw, 2.25rem);
    background: var(--ts-white);
    border-radius: 0.55rem;
    box-shadow: 0 1px 2px rgb(var(--ts-blue-rgb) / 6%), 0 8px 24px rgb(var(--ts-blue-rgb) / 8%);
}

/* The form itself is Views/Shared/_ProfessionPrompt.cshtml, and it has two
   hosts: this panel, and .hero-profession over the hero carousel (PART 5),
   which is where a new visitor normally meets it. The rules below are the
   form's own, so they hold in both.

   The question is the <label> — it was an <h2> with a label repeating the same
   sentence under it. It keeps the heading's look at the heading's 1.6rem: the
   theme's .title is 2.55rem, louder than the catalogue heading below it. */
.ts-profession-prompt__label {
    display: block;
    margin-bottom: 1rem;
    font-size: clamp(1.25rem, 1.1rem + 0.6vw, 1.6rem);
    font-weight: 300;
    line-height: 1.25;
    color: var(--ts-blue);
}

/* Select and button on one line; the button drops under the select when the
   host is too narrow for both, rather than squeezing the select shut. */
.ts-profession-prompt__controls {
    display: flex;
    flex-wrap: wrap;
    gap: 0.75rem;
}

.ts-profession-prompt .form-select {
    flex: 1 1 12rem;
    width: auto;
    /* A flex item's automatic minimum is its content width — for a select, its
       longest option — which sent the button to the next line with room to
       spare. */
    min-width: 0;
    border: 1px solid var(--ts-color-border);
    border-radius: 0.55rem;
    background-color: var(--ts-color-primary-soft);
}

/* White from the start over the hero: primary-soft on the translucent Lavender
   panel barely reads as a field. */
.hero-profession .form-select {
    background-color: var(--ts-white);
}

.ts-profession-prompt .form-select:focus {
    background-color: var(--ts-white);
    border-color: var(--ts-color-primary);
    box-shadow: 0 0 0 0.2rem rgb(var(--ts-blue-rgb) / 20%);
}

/* ============================================================= experts page */

/* The search box is ours too — the old page had the script that posted the term
   but no markup for the field it read — so nothing styled it. Same card, same
   field treatment as the contact form, which is the only other search-shaped
   thing on the site. */
.ts-search-panel {
    padding: clamp(1.25rem, 1rem + 1.2vw, 1.75rem);
    background: var(--ts-white);
    border-radius: 0.55rem;
    box-shadow: 0 1px 2px rgb(var(--ts-blue-rgb) / 6%), 0 8px 24px rgb(var(--ts-blue-rgb) / 8%);
}

.ts-search-panel .form-control {
    border: 1px solid var(--ts-color-border);
    border-radius: 0.55rem;
    background-color: var(--ts-color-primary-soft);
    transition: border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out, background-color 0.15s ease-in-out;
}

.ts-search-panel .form-control:hover:not(:focus) {
    border-color: var(--ts-color-border);
}

.ts-search-panel .form-control:focus {
    background-color: var(--ts-white);
    border-color: var(--ts-color-primary);
    box-shadow: 0 0 0 0.2rem rgb(var(--ts-blue-rgb) / 20%);
}

.ts-search-panel .form-floating > label {
    color: var(--ts-color-ink-faint);
    padding-left: 1rem;
}

/* Safari and Chrome draw their own clear button inside a type="search"; it
   sits hard against the field edge without this. */
.ts-search-panel input[type="search"]::-webkit-search-cancel-button {
    margin-left: 0.5rem;
}

/* The expert card carries a count between two words, and the collection card
   its playing time before a unit; the theme sizes .price-number for a price
   and leaves .price-title and .price-unit at body size only inside .tour-item,
   which is where they are. */
.tour-list .tour-item .speaker-count,
.tour-list .tour-item .collection-hours {
    flex: 1 0 100%;
    margin-bottom: 0.75rem;
}

.tour-list .tour-item .speaker-count .price-title,
.tour-list .tour-item .speaker-count .price-unit,
.tour-list .tour-item .collection-hours .price-unit {
    color: var(--ts-color-ink-soft);
    font-size: 1rem;
    font-weight: 500;
}

/* =============================================================== detail page */

/* The programme's numbered gutter and its accordion are one control drawn as
   two columns, and the seam between them was four inline declarations repeated
   on every module. */
.ts-programme .ts-programme__number {
    border-right-width: 0;
    border-radius: 0.55rem 0 0 0.55rem;
    border-color: transparent;
}

.ts-programme .accordion-item {
    border-left-width: 0;
}

.ts-programme .accordion-button {
    text-align: left;
    border-top-right-radius: 0.55rem;
    font-weight: 500;
}

/* Bootstrap's open accordion header is its own blue on its own tint; these are
   the theme's. */
.ts-programme .accordion-button:not(.collapsed) {
    background-color: var(--ts-color-primary-soft);
    color: var(--ts-color-ink);
    box-shadow: none;
}

.ts-programme .accordion-button:focus {
    border-color: var(--ts-color-border);
    box-shadow: 0 0 0 0.2rem rgb(var(--ts-blue-rgb) / 20%);
}

.ts-programme .menu-item,
.ts-programme .menu-item-completed {
    padding-block: 0.35rem;
}

.ts-programme .menu-item-completed {
    color: var(--ts-color-success);
}

/* The slide being played, in the player's rail. Only the player passes a current
   slide, so the three pages that do not are untouched. Weight as well as the
   tint, never colour alone. `aria-current` is what carries it, never
   `.is-current`: player.js sweeps that class out of everything inside #player
   on each timeline tick. */
.ts-programme [aria-current="true"] {
    font-weight: 700;
    background: var(--ts-color-primary-soft);
}

/* The tick beside a completed slide. Same green as the title it follows, and it
   is there so the state is not carried by colour alone: a reader who cannot tell
   the green from the grey still sees a mark, and a screen reader gets the word
   next to it. */
.ts-programme__done {
    margin-inline-start: var(--ts-space-2xs);
    color: var(--ts-color-success);
}

/* The snapshot's two price rows were the same six flex declarations written
   twice inline. */
.ts-snapshot__row {
    display: flex;
    align-items: center;
    justify-content: space-between;
    flex-wrap: wrap;
    gap: 0.75rem;
}

.ts-snapshot__duration {
    font-weight: 700;
}

/* White, not --ts-color-primary-soft: the pill left the white card and now sits
   straight on the section's .bg-light, and .bg-light is --ht-background-light,
   which is the same --ts-lavender the soft primary resolves to (main.css:281) —
   the pill would have disappeared into its own background. White on lavender is
   a light-on-light pairing, so the shadow the panels use carries the edge. */
.ts-snapshot__buy {
    display: inline-flex;
    align-items: center;
    gap: 0.5rem;
    padding: 0.45rem 0.95rem;
    border-radius: 999px;
    background: var(--ts-white);
    box-shadow: var(--ts-shadow-sm);
    color: var(--ts-color-primary);
    line-height: 1.2;
    transition: background-color 0.2s ease-in-out, color 0.2s ease-in-out;
}

.ts-snapshot__buy .tour-price {
    color: inherit;
    margin-left: 0;
}

.ts-snapshot__buy:hover,
.ts-snapshot__buy:focus-visible {
    background: var(--ts-color-primary);
    color: var(--ts-white);
}

/* The same row in a collection's hero (Views/Trainings/CollectionDetails.cshtml):
   the playing time of the whole series and the pill that puts it in the basket,
   written with the two classes above so the page makes its offer in the shape
   the training detail page makes its own. The White pill is right there too —
   the caption is --ts-lavender, which is what the soft primary resolves to, so
   a soft-primary pill would disappear into it exactly as it would into
   .bg-light.

   The gap above the row is the caption's own: .hero-desc carries main.css's
   bottom margin, and a collection with no text at all gets the title's instead.
   All this adds is a measure. The caption is a column of running text and is
   some 700px wide on a large screen, where `justify-content: space-between`
   would park the pill a paragraph's width away from the duration. */
.ts-collection-hero__buy {
    max-width: 24rem;
}

/* The section headings below the snapshot carried their top margin inline, and
   the .heading the theme puts 2 rem under was left to collide with what
   followed it. */
.ts-details__toc-heading {
    margin-top: 2.5rem;
}

.ts-details__collections-heading {
    margin-top: 2.5rem;
    margin-bottom: 1rem;
}

/* The offer cards are the catalogue card, and its 16/10 is the ratio of a tile
   a third of a row wide. Here they are one per line inside a col-md-6, so from
   md up the same ratio resolved to a photograph some 550 px tall — more of the
   page than the programme it is offered beneath, for what is a secondary
   proposal. 21/9 across this column lands around the height the same card has
   in the catalogue grid, without the strip a wider ratio cuts.

   Only from md up, which is where the column becomes half the page; on a phone
   the card is the full width of a narrow screen and 16/10 is already right —
   a banner ratio there would squash it.

   Both boxes are restated, the figure that holds the footprint and the img that
   fills it, since the base rules set the ratio on each. */
@media (min-width: 768px) {
    .ts-details__collections .tour-item .tour-item-content .image,
    .ts-details__collections .tour-item .tour-item-content .image img {
        aspect-ratio: 21 / 9;
    }

    /* `center top` is the base rule, and it is right for the catalogue's own
       tiles, whose pictures are framed for it. A collection with no authored
       cover borrows a member training's photograph — CatalogueProjections'
       CoverOf — and those are portrait head-shots: cropped from the top, a
       banner keeps the hair and cuts the face off at the forehead. Centred, the
       same crop takes the middle band, which is where the face is. Only the
       img: the figure has nothing to position. */
    .ts-details__collections .tour-item .tour-item-content .image img {
        object-position: center;
    }
}

/* The pitch on an offer card, one type step under the card's own 1.1 rem. What
   a collection carries is a paragraph of some 250 characters where a training's
   teaser is a line, and at full size it competed with the programme it is
   offered beside — the card is a secondary proposal and has to read like one.
   The extra line-height is what a smaller paragraph of running text needs to
   stay comfortable over four or five lines. */
.ts-collection-pitch {
    font-size: 0.95rem;
    line-height: 1.6;
    color: var(--ts-color-ink-soft);
}

/* Playing time over the price pill on an offer card: one column at the right
   edge of the flex .description, centred the way the funnel's own tile stacks
   its price and hours. margin-left: auto is what the pill carried while it was
   a direct child of the row. */
.ts-collection-buy {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 0.35rem;
    margin-left: auto;
}

/* The duration's look on the catalogue row is selected with `.description >`,
   which the wrapper takes it out of; same declarations, same ancestor chain so
   it still outranks main.css's .price-number. */
.tour-list .tour-item .tour-item-content .content .ts-collection-duration {
    font-size: 1.05rem;
    font-weight: 600;
    color: var(--ts-color-ink-soft);
    margin-left: 0;
}

/* The trainings a collection groups, listed on the offer card itself. The list
   and its label are the funnel's own — .funnel-includes and .funnel-family,
   both already defined above and both unscoped — so all this block owns is the
   gap between the price row it follows and the list, which the flex .description
   above it cannot supply from the outside. */
.ts-collection-contents {
    margin-top: 1rem;
}

    /* Smaller here than at checkout, and scoped so it stays that way there. On
       the funnel's own tile the list is what the page is about; on this card it
       is the evidence under an offer, five lines of speaker-duration-title that
       wrapped to two lines each at the card's 1.1 rem and made the list taller
       than everything above it put together. 0.9 rem keeps most members on one
       line, and the check glyph is scaled with the text so it stays a mark
       beside the line rather than a bullet in front of it. */
    .ts-collection-contents .funnel-includes {
        gap: 0.5rem;
    }

        .ts-collection-contents .funnel-includes li {
            font-size: 0.9rem;
            line-height: 1.45;
        }

        .ts-collection-contents .funnel-includes i {
            font-size: 0.7rem;
        }

/* ------------------------------------------- the tile of a collections grid

   _Collection.cshtml: the same card as the offer above, half a row wide on the
   home page and on the three collections pages. The class is on the .tour-item
   itself rather than on the grid that holds it, so the card carries its own
   corrections wherever a page chooses to put it. */

/* The catalogue card's 16/10 is the ratio of a tile a third of a row wide, and
   these are half of one from lg up: across that column the same ratio resolved
   to a photograph some 400 px tall, more of the page than the title, the pitch
   and the button under it put together. 21/9 lands around the height the card
   has in a three-across grid, without the strip a wider ratio cuts. The offer
   card makes exactly this correction inside the detail page's half-width
   column; this is the same one, at the breakpoint where this grid goes to two.

   Below lg the tile is the full width of a phone or a small tablet, like every
   other card there, and 16/10 is already right — a banner ratio would squash it.

   Both boxes are restated, the figure that holds the footprint and the img that
   fills it, since the base rules set the ratio on each. */
@media (min-width: 992px) {
    .tour-list .tour-item.ts-collection-card .tour-item-content .image,
    .tour-list .tour-item.ts-collection-card .tour-item-content .image img {
        aspect-ratio: 21 / 9;
    }

    /* `center top` is the base rule, and it is right for the catalogue's own
       tiles, whose pictures are framed for it. A collection with no authored
       cover borrows a member training's photograph — CatalogueProjections'
       CoverOf — and those are portrait head-shots: cropped from the top, a
       banner keeps the hair and cuts the face off at the forehead. Centred, the
       same crop takes the middle band, which is where the face is. */
    .tour-list .tour-item.ts-collection-card .tour-item-content .image img {
        object-position: center;
    }
}

/* .description is the catalogue card's metadata row — a flex line that wraps —
   and it gives its paragraphs `flex: 1 1 55%` so a teaser and a control may
   share one line. This card has no control in the row: it has a paragraph and a
   figure, each of which is a line of its own, and at half a row wide there is
   enough width for the playing time and the button to have crept up beside them.
   `1 0 100%` states the three lines instead of leaving them to the width.

   Paragraphs only. The button's wrapper keeps its automatic width, which is what
   leaves it its own line under the duration, at the left edge where the tile it
   replaces drew it — stretched to the full row its `text-lg-end` would take over
   and push it to the right. */
.tour-list .tour-item.ts-collection-card .tour-item-content .content .description > p {
    flex: 1 0 100%;
}

/* The negative inline margin undoes the padding of the .container-fluid the card
   sits in — half the row's gutter, 0 below md where the row is g-0 — so the card
   lines up edge to edge with the table of contents, which sits straight in the
   column. */
.ts-details__summary {
    margin-top: 1rem;
    margin-inline: calc(var(--bs-gutter-x, 0px) * -0.5);
    font-size: 1.05rem;
}

.ts-details__summary p:last-child {
    margin-bottom: 0;
}

/* "Au programme" is one heading over three paragraphs the API sends unlabelled,
   and read flat they were three interchangeable blocks of grey — nothing said
   which one to read to find out what the training is about. The summary is that
   sentence, so it gets the type of a lead: Bold, a step up in size, the panel's
   full Blue ink. The two that elaborate on it step back — same size as before,
   Blue at 72 %.

   Weight is only ever 600 or 700 here. The identity licenses no 300/400/500
   (brand.css:226), so the lighter half of the contrast is carried by colour and
   size, never by a lighter face.

   --ts-text-muted is deliberately not used: the panel is White in both modes,
   but in the learning environment that token is Periwinkle — a colour for
   White-on-Blue text, unreadable here — and a signed-in visitor sees this same
   page. color-mix against White is the same 72 % ink and needs no mode switch,
   the panel being White either way. It is also the one form that works:
   `rgb(var(--ts-blue-rgb) / 72%)` looks like the rest of this stylesheet but
   --ts-blue-rgb is comma-separated, so it expands to the invalid
   `rgb(24, 32, 111 / 72%)` and the whole declaration is dropped. */
.ts-details__summary-lead {
    font-size: 1.15rem;
    font-weight: var(--ts-weight-heading);
    line-height: 1.5;
    color: var(--ts-blue);
}

.ts-details__summary-body {
    color: color-mix(in srgb, var(--ts-blue) 72%, var(--ts-white));
}

/* The speaker panel's name is a .sub-title, which the theme letter-spaces as an
   eyebrow; here it is the panel's heading, and #speaker-name already gives it
   heading size. What is missing is the room under it. */
#details-speaker .heading {
    margin-bottom: 1rem;
}

#details-speaker .info-box-desc p:last-child,
#details-snapshot .info-box-desc p:last-child {
    margin-bottom: 0;
}

/* The portrait and the speaker's card are two blocks that meet on one edge, and
   both round it: the photo's bottom corners and the panel's top corners each
   curve away from the join, so the page background shows through as two small
   wedges — a notch where the two should read as one object.

   Only the touching pair is flattened. The top of the photo and the bottom of
   the card keep the 0.55rem, which is what makes the pair read as one rounded
   rectangle rather than as two squared-off boxes.

   No media query, because the two touch at every width this page has. The
   `image-left h-indent-content-250px col-md-4` on the markup promises a desktop
   layout where the card is indented beside the portrait instead of under it,
   but that layout does not exist here: `h-indent-content-250px` is not defined
   in any stylesheet, main.css pins `.info-box-content .image` to
   `min-width: 100%` (which beats its own max-width, so col-md-4 never narrows
   it), and #details-speaker is a *sibling* of .info-box-content rather than the
   `.content` inside it that the theme's indent rules select. Measured at 360,
   414, 576, 700, 767, 768, 900, 992, 1100, 1200, 1400 and 1600 px: same x, same
   width, gap of exactly 0 at all twelve.

   The image half is a no-op below 768 px — responsive.css zeroes all four of
   the portrait's corners there — and is what actually removes the notch from
   768 px up, where main.css's 0.55rem applies. Both rules are scoped to the two
   ids this block owns: .ts-panel is the contact page's card as well, and
   `.info-box-content .image img` is every info box on the site. */
#details-infobox .image img {
    border-bottom-left-radius: 0;
    border-bottom-right-radius: 0;
}

#details-speaker {
    border-top-left-radius: 0;
    border-top-right-radius: 0;
}

/* ==================================================================== FAQ */

/* .room-item is the theme's own row and it stacks its picture and its answer
   with no space between them; on a page that is a list of questions the gap
   between two answers has to read as bigger than the gap inside one. */
.room-box .room-list .room-item .content.ts-panel {
    margin-top: 1.5rem;
}

.room-box .room-list .room-item .info-box-content .image img {
    border-radius: 0.55rem;
}

.room-box .room-list .room-item .info-box-desc p:last-child {
    margin-bottom: 0;
}

/* ------------------------------------------------- long words, tap targets */

/* main.css writes `overflow-wrap: break-word` on `.heading .sub-title` and not
   on `.heading .title` beside it. Dutch compounds are long — the detail page's
   "Vennootschapsbelasting" is 577 px at the 2.15rem responsive.css gives the
   title under 768 px — so one word pushed the whole document 217 px wider than
   the viewport and put a horizontal scrollbar on a phone. */
.heading .title {
    overflow-wrap: break-word;
}

/* A breadcrumb link is 17 px tall, which is under the 24 px minimum a pointer
   target needs, on the control a visitor uses to get back. The template writes
   no rule for the link inside .breadcrumb-item at all — only a font-size on the
   box around it — so this adds the box without touching the type. */
.breadcrumb-box .breadcrumb-item a {
    display: inline-block;
    padding-block: 0.2rem;
}

/* Below 768 px responsive.css pulls the info box's picture 0.75rem to the left
   and to the right, to cancel the padding Bootstrap's .container-fluid gives
   it. The vendored bootstrap.min.css has --bs-gutter-x commented out of that
   rule, so there is no padding to cancel and the pull hangs the picture 12 px
   past both edges of the viewport instead — the FAQ answers and the training
   detail page's speaker portrait both sat 12 px off the left margin.

   Exactly the same correction, for exactly the same reason, as the
   .hero-slider-container rule further up this file. It cannot be a utility
   class on the markup because it must apply below the breakpoint only. */
@media (max-width: 767.98px) {
    .info-box .info-box-container .info-box-content.image-left .image,
    .info-box .info-box-container .info-box-content.image-right .image {
        margin-left: 0;
        margin-right: 0;
    }

    .info-box .info-box-container .info-box-content .image {
        max-width: 100%;
    }
}


/* ------------------------------------------------- off-canvas menu overflow

   responsive.css parks the mobile menu off the right edge:

       .navbar { position: fixed; right: -100%; width: 100%; max-width: 400px }

   On a 360 px viewport that puts the element at [360 → 720]. A fixed element is
   not supposed to enlarge the scrollable area, but Chrome counts it, so every
   page carried a ~12 px horizontal scrollbar with nothing visible in it. Two
   separate measurements of `scrollWidth - clientWidth` found it on pages that
   have nothing else in common, including ones nobody had edited — the menu is
   the only thing they share.

   `clip` and not `hidden`: `overflow: hidden` turns the element into a scroll
   container, which silently kills `position: sticky` inside it — and sticky is
   what holds the checkout summary rail and the player's video pane in view.
   `clip` cuts the overflow without creating that container.

   Scoped to the widths where the drawer exists; above 1200 px responsive.css
   puts the bar back in the flow and there is nothing to clip.                   */

@media (max-width: 1199.98px) {
    body {
        overflow-x: clip;
    }
}


/* =========================================================== brand contrast

   The theme's blue was #4285c8 (white on it: 3.88:1 — short of the 4.5:1 a
   normal-size label needs) and its hover/active blue was #009aff (white on
   it: 2.94:1, worse still). An earlier fix moved both to a darker step of the
   same off-palette hue; the 2026 identity has one blue, so both now point at
   the brand Blue instead — #18206F on white is 14.5:1, comfortably clear.

   main.css declares the token with !important, so the override needs it too;
   this file is linked last, so at equal specificity and equal importance it
   wins. Everything the theme paints through var(--ht-color-primary) follows
   from this one line. See also the --ht-* → --ts-* shim in main.css. */

:root {
    --ht-color-primary: var(--ts-blue) !important;
    --ht-color-primary-dark: var(--ts-blue-dark) !important;
}

/* The theme hard-codes the old blue here instead of reading its own token. */
#PrintButton {
    color: var(--ts-color-primary);
}

/* ============================================================= the slide player

   Views/Learn/Index, Views/Play/Index and Views/Shared/Account/_Player.

   The two pages are the theme's `reservation-box` funnel shell, so everything the
   funnel block above already defines — card elevation, `.contact-title`, the
   `<h2>` + `<hr>` panel heading, `.funnel-line`, `.funnel-badge` and the button
   sizing — applies here untouched and is not restated. What follows is only the
   things the template has no name for: the hidden site chrome, the heading row
   that stands in for it, the video frame, the module/slide pager, the table of
   contents, and the certificate dialog.

   The `player-` prefix, not `ts-`: the `ts-` namespace is shared with the
   catalogue and the editorial pages, and it is also what the two Blazor question
   components render (`ts-code`, `ts-field`, `ts-button`) — a second `.ts-card` or
   `.ts-badge` declared here would silently restyle them. The time codes keep
   `ts-code` on purpose; PART 3 below (the former account.css) styles those and
   carries `.ts-code.is-current`, the highlight player.js paints.

   Nothing below uses the class `is-current` for anything other than a time code.
   player.js clears every `.is-current` inside #player on each timeline change, so
   the table of contents marks its current entry with `aria-current` instead.      */

/* --- the page chrome ------------------------------------------------------ */

/* _Layout marks Learn and Play `.ts-player-page`. The player reads as a modal over
   the site — the training is all there is, and the back button is the way out — so
   the header and the footer go, hidden rather than removed because main.js
   addresses #header by id. The theme's hero padding goes with them: those are the
   figures for a page that opens on a title block, and !important because that is
   what `pd-top-200` / `pd-bottom-300` carry. */
.ts-player-page #header,
.ts-player-page #footer {
    display: none;
}

.ts-player-page #player-box {
    padding-top: var(--ts-space-md) !important;
    padding-bottom: var(--ts-space-lg) !important;
}

/* The section keeps the theme's `bg-transparent-top`, whose `::before` (main.css) is a
   transparent, absolutely positioned layer 550px tall with `z-index: 0` over the head of the
   section. Positioned, it paints above every non-positioned block beneath it — and the
   topbar's `col-12` is one — so the back, print and certificate controls never got the
   click. The old `title-box` sat above the layer by being `position: relative` and held no
   link, which is why this never showed. The layer draws nothing here, so it goes. */
.ts-player-page #player-box::before {
    content: none;
}

/* --- the page heading ----------------------------------------------------- */

/* One row for the whole heading of the screen: the way back, the training, and
   the readings and actions that belong to the training rather than to the slide.
   It replaces both the theme's `title-box` and the rail rows that repeated the
   completion figure and the length — which is where the screen height comes from.
   The actions are pushed to the far end with `margin-inline-start:auto` and not a
   spacer. One row, never two: the way back, the ring and the buttons keep their
   width, and the title is the one thing that gives — it is clipped with an
   ellipsis when the row runs out of room rather than pushing anything down. */
.player-topbar {
    display: flex;
    flex-wrap: nowrap;
    align-items: center;
    gap: var(--ts-space-sm) var(--ts-space-md);
    margin-bottom: var(--ts-space-xs);
}

.player-topbar__back {
    flex: 0 0 auto;
}

/* min-width:0 is what lets the heading shrink below its text: a flex item
   defaults to min-content, and a nowrap title's min-content is the whole title,
   which would scroll a narrow page sideways instead of clipping. The title and
   the speaker/length line sit on one baseline, not stacked: the whole heading is
   a single line, and the title is the only part of it that gives. */
.player-topbar__heading {
    display: flex;
    flex: 1 1 auto;
    align-items: baseline;
    gap: var(--ts-space-sm);
    min-width: 0;
}

/* Under the 2.1rem it briefly was: the only title on the page, but it shares one
   line with the speaker, the length, a ring and three buttons, and at 2.1rem a
   title of four words did not fit beside them on a wide screen. One line, always:
   the title is the one thing in the row that gives, and it gives with an
   ellipsis — the full text stays in the tab title and on the certificate — never
   by wrapping, so the row keeps one height whatever the training is called. */
.player-topbar__title {
    flex: 0 1 auto;
    min-width: 0;
    margin: 0;
    font-size: clamp(1.25rem, 0.9rem + 0.8vw, 1.6rem);
    font-weight: var(--ts-weight-display);
    letter-spacing: var(--ts-tracking-tight);
    line-height: 1.15;
    color: var(--ts-white);
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
    /* The ellipsis needs overflow:hidden, and hidden clips vertically too: at 1.15
       the line box is shorter than Plus Jakarta Sans's glyph area, so a g, p or y
       lost its tail and an accented capital its accent. Padding gives the glyphs
       room to overhang; the equal negative margin hands the space straight back,
       so the row is no taller and the baseline the meta line sits on is unmoved. */
    padding-block: 0.2em;
    margin-block: -0.2em;
}

/* Who gives the training and how long it runs. Same shape as the dashboard's
   .ts-panel-list__meta — a baseline-aligned flex row with the separator drawn
   rather than written, so no screen reader reads it out as a word and it is
   absent by construction when the training credits no speaker. Periwinkle is
   learn mode's own --ts-text-muted: this line sits on the Blue page, not on a
   card. */
.player-topbar__meta {
    display: flex;
    flex: 0 0 auto;
    flex-wrap: nowrap;
    align-items: baseline;
    gap: var(--ts-space-2xs) var(--ts-space-sm);
    margin: 0;
    white-space: nowrap;
    font-size: 0.95rem;
    color: var(--ts-periwinkle);
}


.player-topbar__speaker + .player-topbar__duration::before {
    content: "\00B7";
    margin-inline-end: var(--ts-space-xs);
}

.player-topbar__actions {
    display: flex;
    flex-wrap: nowrap;
    flex: 0 0 auto;
    align-items: center;
    gap: var(--ts-space-xs);
    margin-inline-start: auto;
}

/* `.reservation-box .btn` is sized for a call to action alone in a 28 % rail —
   2 rem of padding on each side. Four of them on one line with a title wrap onto a
   second line on a laptop, so this row takes that back down. */
.player-topbar__back,
.player-topbar__actions .btn {
    padding-inline: var(--ts-space-md);
}

/* The ring sits straight on the Blue page, not on a White card, so Blue on Blue
   would be nothing: it takes the dashboard's Yellow, the page's own rule as its
   unfilled track, and punches its hole in the page colour instead of --ts-surface.
   Doubled up with `.ts-progress-ring` because this block sits above that one in
   the file, and size and thickness are declared there too. */
.player-topbar__ring.ts-progress-ring {
    --ts-progress-ring-size: 3rem;
    --ts-progress-ring-thickness: 0.28rem;
    --ts-progress-ring-fill: var(--ts-yellow);
    --ts-progress-ring-track: var(--ts-blue-rule);
    --ts-progress-ring-hole: var(--ts-blue);
}

.player-topbar__ring .ts-progress-ring__label {
    font-size: 0.8rem;
    color: var(--ts-white);
}

/* Below the desktop breakpoints the same single row stays — back, title, speaker,
   length, ring, print, certificate — and nothing drops under the title. The words
   on the three buttons are what does not fit beside a title on a tablet or a
   phone, so they go visually and stay for a screen reader (same recipe as
   .ts-visually-hidden), the ring loses a size and the gaps close up. The title is
   still the one thing that gives — by clipping; the speaker's name is capped so a
   long one cannot take the whole row from it, and the heading clips rather than
   run under the ring should a phone be narrower than the floors below allow. */
@media (max-width: 991.98px) {
    .player-topbar {
        gap: var(--ts-space-xs);
    }

    .player-topbar__heading {
        gap: var(--ts-space-xs);
        overflow: hidden;
    }

    /* Flex shrinks in proportion to size, which would take the short speaker line
       down to nothing while the long title still had room. The 10 makes the
       title give first; the meta only shrinks once the title is at its floor. */
    .player-topbar__title {
        flex-shrink: 10;
    }

    .player-topbar__meta {
        gap: var(--ts-space-2xs);
        min-width: 0;
        flex-shrink: 1;
    }

    .player-topbar__speaker {
        flex: 0 1 auto;
        max-width: 7rem;
        overflow: hidden;
        text-overflow: ellipsis;
    }

    .player-topbar__duration {
        flex: 0 0 auto;
    }

    .player-topbar__speaker + .player-topbar__duration::before {
        margin-inline-end: var(--ts-space-2xs);
    }

    .player-topbar__back,
    .player-topbar__actions .btn {
        padding-inline: var(--ts-space-sm);
    }

    .player-topbar__back > span,
    .player-topbar__actions .btn > span {
        position: absolute !important;
        width: 1px;
        height: 1px;
        padding: 0;
        margin: -1px;
        overflow: hidden;
        clip: rect(0, 0, 0, 0);
        white-space: nowrap;
        border: 0;
    }

    .player-topbar__actions {
        gap: var(--ts-space-2xs);
    }

    .player-topbar__ring.ts-progress-ring {
        --ts-progress-ring-size: 2.25rem;
        --ts-progress-ring-thickness: 0.22rem;
    }

    .player-topbar__ring .ts-progress-ring__label {
        font-size: 0.65rem;
    }
}


/* A phone: the type comes down, and the speaker and the length go under the
   title rather than beside it. Beside it, the four controls leave the heading
   about 140px, and a title sharing that with a name is down to a few letters
   while the name keeps its 7rem — the wrong thing to keep. Under it, the title
   has the heading's whole width for its one line, and the meta line takes what
   it needs below, the name still clipped past its cap. */
@media (max-width: 575.98px) {
    .player-topbar__heading {
        flex-direction: column;
        align-items: flex-start;
        gap: 0.125rem;
    }

    .player-topbar__title {
        flex: 0 0 auto;
        max-width: 100%;
        font-size: 1.1rem;
    }

    .player-topbar__meta {
        flex: 0 0 auto;
        max-width: 100%;
        font-size: 0.8rem;
    }
}


/* --- the video frame ------------------------------------------------------ */

/* The video is a control of its own, above the panel below it rather than inside
   it: a card whose only content is the picture keeps that picture in the same
   place on every slide, whatever the panel below it grows to.

   The card is the frame, so it takes the media's own radius rather than
   Bootstrap's 0.375rem — two radii on the same rectangle leave a white crescent
   in each corner — and drops the border for the same reason. `overflow` because
   Plyr replaces the child it is given: whatever ends up inside is clipped back
   to the frame. */
.player-video-card {
    overflow: hidden;
    border: 0;
    border-radius: 0.55rem;
    background: #000;
}

.player-media {
    border-radius: 0.55rem;
    overflow: hidden;
    background: #000;

    /* Plyr's control bar at its shipped size — 18 px glyphs, a 12 px clock, 10 px
       of spacing — is hard to read on the rail's picture. The plugin sizes the
       whole bar from these variables, so raising them here grows the buttons,
       the clock, the tooltips and the speed menu together and keeps its rhythm. */
    --plyr-control-icon-size: 22px;
    --plyr-control-spacing: 12px;
    --plyr-font-size-base: 15px;
    --plyr-font-size-small: 14px;
    --plyr-font-size-time: 15px;
    --plyr-range-track-height: 6px;
    --plyr-range-thumb-height: 15px;
}

/* The ratio is reserved before the metadata arrives, so the page does not jump.
   `>` on purpose: once Plyr has run, the <video> is no longer a direct child —
   Plyr moves it into a wrapper that sizes it itself, and a competing
   aspect-ratio here would fight it. */
.player-media > .player-video {
    aspect-ratio: 16 / 9;
}

.player-video {
    display: block;
    inline-size: 100%;
    block-size: auto;
}

/* That wrapper takes over the shape, so the radius has to move onto it or the
   corners come back square. */
.player-media .plyr {
    border-radius: 0.55rem;
}

/* The ten-second rewind is the one button on the bar that trainees reach for during the
   talk, and its glyph carries a number that the other icons do not: at the bar's 22 px it
   reads as a smudge. So it alone is drawn larger. The padding gives back what the glyph
   takes — Plyr pads every control by 0.7 × the spacing — so the button keeps the height
   of its neighbours and the bar does not grow. */
.player-media .plyr__control[data-plyr="rewind"] {
    padding: calc(var(--plyr-control-spacing) * 0.7 - 4px);
}

.player-media .plyr__control[data-plyr="rewind"] svg {
    inline-size: 30px;
    block-size: 30px;
}

/* The big play button over the poster. The vendor stylesheet caps it at 80 × 80 px but pads
   it by 2.8 × the spacing — 33.6 px a side at the 12 px above — which with the 22 px glyph
   asks for 89 px. The cap wins, the content box shrinks to 13 px, and the glyph spills out
   of it to the right and downward: the triangle sat off the circle's centre. The padding is
   derived from the cap and the glyph instead, so the two always add up to the circle. The
   vendor's own 3 px nudge of the glyph stays — that is the usual optical centring of a
   triangle, which reads as centred only when it sits a touch right of the geometric middle. */
.player-media .plyr__control--overlaid {
    padding: calc((80px - var(--plyr-control-icon-size)) / 2);
}

/* A link that is present but refused (the certificate before 100 %) reads as
   available otherwise. Not `.disabled`: the target still exists and the page
   behind it explains why it is refused. */
.player .btn[aria-disabled="true"] {
    opacity: 0.65;
}

/* Except in the heading row, which is the one place such a link actually sits
   today. There the button stands on the Blue page rather than on a White card, and
   fading it mixes both the Periwinkle chip AND the Blue ink on it down into the
   page colour: the label lands at about 1.3:1 against its own chip and is simply
   not readable — which is what the screenshot of the finished screen shows.

   So the refused state is drawn instead of faded. The chip goes, a rule of the same
   Periwinkle at a quarter strength takes its place, and the ink is the Periwinkle
   the meta line under the title already uses — 9:1 on the page, legible, and still
   plainly the quieter of the two actions beside it. */
.player-topbar .btn[aria-disabled="true"] {
    opacity: 1;
    --bs-btn-bg: transparent;
    --bs-btn-hover-bg: transparent;
    --bs-btn-active-bg: transparent;
    --bs-btn-border-color: color-mix(in srgb, var(--ts-periwinkle) 25%, transparent);
    --bs-btn-hover-border-color: color-mix(in srgb, var(--ts-periwinkle) 45%, transparent);
    --bs-btn-active-border-color: color-mix(in srgb, var(--ts-periwinkle) 45%, transparent);
    --bs-btn-color: var(--ts-periwinkle);
    --bs-btn-hover-color: var(--ts-white);
    --bs-btn-active-color: var(--ts-white);
}

/* --- the attached documents ----------------------------------------------- */

.player-document {
    display: flex;
    align-items: center;
    gap: 0.6rem;
    color: var(--ts-color-ink);
}

.player-document > span {
    min-width: 0;
    overflow-wrap: anywhere;
}

.player-document__icon {
    flex: 0 0 auto;
    color: var(--ts-color-primary);
}

.player-document__out {
    flex: 0 0 auto;
    margin-left: auto;
    font-size: 0.8rem;
    color: var(--ts-color-ink-faint);
}

/* --- the module block and its pager --------------------------------------- */

/* The module the slide belongs to, drawn as the very same tile the table of
   contents gives it: a state-coloured number against a White bar, the two
   touching and rounded as one. It used to be the first row inside the slide
   card; a block of its own is what separates "which chapter" from "what is on
   this slide", and it is the shape the list already taught the trainee to read.

   `stretch` and not `center`: the coloured number is the tile's left edge and
   has to run its full height, whatever a wrapped title grows it to. */
.player-module {
    display: flex;
    align-items: stretch;
}

/* Same three utilities the gutter takes in _TrainingToc — .bg-green, .bg-orange
   and .bg-darkblue — so the colour is set in the view, once, from the one
   definition of module progress. Only the shape is here. */
.player-module__number {
    flex: 0 0 auto;
    display: grid;
    place-items: center;
    min-inline-size: 3rem;
    padding-inline: var(--ts-space-xs);
    border-start-start-radius: var(--ts-radius);
    border-end-start-radius: var(--ts-radius);
    /* The seam the table of contents draws between its number and its White row:
       the gutter there carries Bootstrap's `.border` (`!important`, so the rule
       that would clear its right edge never wins), a 1px line in the page's
       border colour. The same line here, so the two tiles keep reading as one. */
    border-inline-end: 1px solid var(--bs-border-color);
    color: var(--ts-white);
    font-weight: var(--ts-weight-heading);
}

.player-module__bar {
    flex: 1 1 auto;
    min-width: 0;
    display: flex;
    align-items: center;
    gap: 0.75rem;
    padding: var(--ts-space-xs) var(--ts-space-sm);
    border-start-end-radius: var(--ts-radius);
    border-end-end-radius: var(--ts-radius);
    background: var(--ts-surface);
}

/* min-width:0 and a wrapping title: a module name is editorial, it can be long,
   and a flex item defaults to min-content — which is what makes a 360 px page
   scroll sideways. */
.player-module__title {
    flex: 1 1 auto;
    min-width: 0;
    margin: 0;
    text-align: center;
    font-size: 1.05rem;
    font-weight: 600;
    color: var(--ts-color-ink);
    overflow-wrap: anywhere;
}

/* The two step buttons are a chevron each, and nothing else: no chip, no border,
   no square. They flank the module title and are meant to read as quieter than
   it — a light blue mixed from the two brand blues at rest, the full Blue only
   under the pointer or the keyboard. The 2.75 rem box stays for the pointer
   target; `.btn`'s 2 rem of side padding was written for a labelled action. */
.player-nav__step {
    flex: 0 0 auto;
    inline-size: 2.75rem;
    padding-inline: 0;
    --bs-btn-bg: transparent;
    --bs-btn-border-color: transparent;
    --bs-btn-hover-bg: transparent;
    --bs-btn-hover-border-color: transparent;
    --bs-btn-active-bg: transparent;
    --bs-btn-active-border-color: transparent;
    --bs-btn-disabled-bg: transparent;
    --bs-btn-disabled-border-color: transparent;
    --bs-btn-color: color-mix(in srgb, var(--ts-periwinkle) 65%, var(--ts-blue));
    --bs-btn-hover-color: var(--ts-blue);
    --bs-btn-active-color: var(--ts-blue);
    --bs-btn-disabled-color: var(--ts-periwinkle);
    box-shadow: none;
    font-size: 1.15rem;
}

/* --- the certificate questionnaire ---------------------------------------- */

/* Views/Evaluation/_EvaluationPanel and _EvaluationThanks, fetched by player.js into
   #player-evaluation in place of the slide pane once the training is complete. Their
   header is the module block above — the certificate glyph in the green tile where the
   number goes, the way back where the pager goes — and their card is the slide card, so
   nothing new is drawn for either. What is here: the step line under the card title,
   the header's title alignment, and the thank-you card. The answer blocks themselves
   are `.evaluation-form`, styled with the standalone page in PART 2.

   No transition on a step change: a block is shown and the one before it hidden, which
   is all `prefers-reduced-motion` could ask for. The scroll that brings the panel into
   view is player.js's, and it already drops to `auto` under that preference. */

/* One control where the module block has two chevrons, so a centred title would sit
   off-centre between the tile and the close. Start-aligned, with the bar's own gap. */
.player-evaluation__header .player-module__title {
    text-align: start;
}

/* "Stap 2 van 4", between the block's name and the rule. Quieter than the title —
   the Blue lightened, as the pager's chevrons are — and the same size as the meta line
   of the heading row. Mixed rather than a faint-ink token: it sits on the White card. */
.player-evaluation__step {
    margin: 0 0 var(--ts-space-xs);
    font-size: 0.95rem;
    color: color-mix(in srgb, var(--ts-blue) 75%, var(--ts-white));
}

/* The Continue button while the block on screen still has a question without an answer:
   player.js keeps `aria-disabled="true"` on it (see _EvaluationSteps for why not `disabled`).
   Light Blue, drawn rather than faded — `.player .btn[aria-disabled="true"]` above fades the
   whole button, label included, and the press is still meant here: it says what is missing.
   Hover and active keep the same light Blue, so that press does not flash the button dark.
   Same specificity as the rule above, declared later, so this wins. */
.evaluation-actions .btn[aria-disabled="true"] {
    opacity: 1;
    --bs-btn-bg: color-mix(in srgb, var(--ts-blue) 40%, var(--ts-white));
    --bs-btn-border-color: color-mix(in srgb, var(--ts-blue) 40%, var(--ts-white));
    --bs-btn-hover-bg: color-mix(in srgb, var(--ts-blue) 40%, var(--ts-white));
    --bs-btn-hover-border-color: color-mix(in srgb, var(--ts-blue) 40%, var(--ts-white));
    --bs-btn-active-bg: color-mix(in srgb, var(--ts-blue) 40%, var(--ts-white));
    --bs-btn-active-border-color: color-mix(in srgb, var(--ts-blue) 40%, var(--ts-white));
}

/* The thank-you card: glyph, sentence and the way out, stacked and centred — a status,
   not a form, so it does not take the form's left-aligned footer. The badge is the
   `.funnel-badge` the dialogs use, grown to be the one thing on the card. */
.player-evaluation__thanks {
    display: grid;
    justify-items: center;
    gap: var(--ts-space-md);
    text-align: center;
}

.player-evaluation__thanks-badge {
    inline-size: 3.5rem;
    block-size: 3.5rem;
    margin: 0;
    font-size: 1.6rem;
}

.player-evaluation__thanks-text {
    margin: 0;
    white-space: pre-line; /* the confirmation is two sentences on two lines */
    max-inline-size: 36rem;
    font-size: 1.05rem;
    font-weight: 600;
    color: var(--ts-color-ink);
}

.player-evaluation__thanks .funnel-actions {
    justify-content: center;
}

/* --- the table of contents ------------------------------------------------ */

/* The rail draws Views/Shared/_TrainingToc — .ts-programme, styled with the
   detail page further up this file. Everything the player used to spell out for
   itself here (.player-toc__number, .player-toc__slide, .player-toc__done and
   their open/current states) was a second vocabulary for that same control and
   is gone with it.

   What is left is the one thing the other three callers have no need of: here the
   list is not in a card, so each module is a White tile straight on the Blue page
   and needs the air between two of them that the card used to give the whole
   block. The partial's own `mt-2` is what carries it, doubled. */
.player-toc .ts-programme .ts-programme__number,
.player-toc .ts-programme [data-toc-module] {
    margin-top: var(--ts-space-md);
}

/* The not-started state is `.bg-darkblue` — the Blue itself — and on the three
   other callers' White pages that reads. Here the page is Blue, so a not-started
   module was a Blue tile on a Blue ground and vanished. Periwinkle on this page
   only, with Blue ink because the White number the utility pairs with would
   vanish in turn on the tint. The view and player.js keep saying `.bg-darkblue`:
   that stays the one name for the state, only its colour here differs. The
   module block above the slide takes the same, so the two keep reading as one. */
.player-toc .ts-programme__number.bg-darkblue,
.player-module__number.bg-darkblue {
    background: var(--ts-periwinkle);
    color: var(--ts-blue);
}

/* The gutter's number carries Bootstrap's `.text-white`, which is `!important`. */
.player-toc .ts-programme__number.bg-darkblue .text-white {
    color: var(--ts-blue) !important;
}

/* --- the slide ------------------------------------------------------------ */

/* PART 3 below pads each `.ts-code`; this is only the rhythm between two of them.
   `grid` rather than margins so the answer markers — which are visually hidden
   and out of flow — add no gap of their own. */
.player-slide {
    display: grid;
    gap: 0.35rem;
}

    /* A heading (type 1, the entry that turns blue) opens a group and the entries under it —
       body text, notes, images, links, the question blocks — belong to it. Stepping those in
       makes the grouping visible; the heading itself stays on the column's left edge.

       Direct children only: a question block renders its own `.ts-code--question` inside the
       wrapper, and matching that as well would step the question in twice. */
    .player-slide > .ts-code:not(.ts-code--heading) {
        margin-inline-start: var(--ts-space-md);
    }

    .player-slide img {
        max-inline-size: 100%;
        block-size: auto;
    }

/* From Bootstrap's xl up — the breakpoint the two columns split at (`col-xl-5` /
   `col-xl-7`) — the article is a scroller of its own, so that following the speaker
   moves the text and nothing else: the heading row, the module bar and the slide's
   title hold still above it, the wheel over the text moves the text and the wheel
   anywhere else moves the page. Stacked under the video, below the breakpoint, a
   scroller inside the page is only a trap for the thumb, so there the window scrolls
   as before.

   The height is player.js's, not this sheet's — see "auto-scroll" there: what sits
   above the article (the heading row, the module block, the card's title) wraps
   differently with every width and language, so the room left under it down to the
   bottom of the screen is measured and handed over as `--player-slide-max`. Without
   the script the variable is unset, `none` applies and the column is the plain grid
   it was. `overscroll-behavior: contain` keeps a wheel that reaches the end of the
   text from running on into the page; the end padding keeps the highlight off the
   scrollbar. */
@media (min-width: 1200px) {
    .player-slide {
        max-block-size: var(--player-slide-max, none);
        overflow-y: auto;
        overscroll-behavior: contain;
        scrollbar-width: thin;
        padding-inline-end: var(--ts-space-xs);
    }
}

/* --- the certificate dialog ----------------------------------------------- */

/* The theme has no dialog: its own modals are Bootstrap's, driven by jQuery on
   the old site. This is `.card`'s shape and the funnel's ink on a native
   <dialog>, which needs no script at all. */
.player-dialog {
    max-inline-size: min(92vw, 32rem);
    padding: 1.75rem;
    border: 1px solid var(--ts-color-border);
    border-radius: 0.55rem;
    background: var(--ts-white);
    color: var(--ts-color-ink);
    box-shadow: 0 1px 2px rgb(var(--ts-blue-rgb) / 6%), 0 16px 40px rgb(var(--ts-blue-rgb) / 18%);
}

.player-dialog::backdrop {
    background: rgb(16 22 60 / 45%);
}


/* --- printing ------------------------------------------------------------- */

/* #player-print calls window.print(), and this sheet is what turns "the page" into
   "the slide". The rule is turned around rather than written as a list of things
   to hide: the body keeps only #main, the player keeps only its heading and its
   slide column, and anything added to the screen later stays off the paper unless
   it is put back here on purpose.

   What is printed: the training's title with the speaker and the length under it
   (the heading row without the way back and its three actions), the module the
   slide belongs to (without the two chevrons) and the slide card — title, rule and
   time codes. Not printed: the site chrome, the video, the next-slide button, the
   reset, the documents, the table of contents, the retry notice, the certificate
   questionnaire and the dialog.

   The line the browser adds above and below every sheet — date, page title, URL,
   page count — belongs to the print dialog, not to the page; it is switched off
   there, under "Headers and footers", and no rule here can reach it. */
@media print {
    @page {
        margin: 1.5cm;
    }

    body.ts-player-page > :not(#main),
    .ts-player-page .player-topbar__back,
    .ts-player-page .player-topbar__actions,
    .ts-player-page .player > .col-xl-5,
    .ts-player-page .player-nav__step,
    .ts-player-page #player-next-slide,
    .ts-player-page #player-complete-failed,
    .ts-player-page #player-evaluation,
    .ts-player-page #certificate-dialog,
    .ts-player-page #quiz-reset-dialog {
        display: none !important;
    }

    /* Learn mode paints the page Blue with White ink; paper is the other way round.
       The hero padding of the funnel shell goes too — it is screen air. */
    body.ts-player-page,
    .ts-player-page #main,
    .ts-player-page #player-box {
        background: #fff !important;
        color: #000;
    }

    .ts-player-page #player-box,
    .ts-player-page #player-box .container-fluid {
        padding: 0 !important;
    }

    /* The remaining column takes the whole sheet whatever the breakpoint the print
       viewport lands on. */
    .ts-player-page .player > .col-xl-7 {
        flex: 0 0 100%;
        width: 100%;
        max-width: 100%;
    }

    .ts-player-page .player-topbar {
        margin-bottom: var(--ts-space-md);
    }

    /* Paper has no ellipsis to click through: the title is not clipped here. */
    .ts-player-page .player-topbar__title {
        font-size: 1.6rem;
        color: #000;
        white-space: normal;
        overflow: visible;
    }


    .ts-player-page .player-topbar__meta {
        color: #444;
    }

    /* The state colour of the module tile is a screen reading; on paper a printer
       set to skip backgrounds would leave a White number on a White sheet. Ink and
       a rule instead. `!important` because .bg-green & co. are the utility. */
    .ts-player-page .player-module {
        break-after: avoid;
    }

    .ts-player-page .player-module__number {
        background: none !important;
        border: 1px solid #000;
        border-inline-end: 0;
        color: #000;
    }

    .ts-player-page .player-module__bar {
        border: 1px solid #000;
        background: none;
    }

    .ts-player-page .player-module__title {
        color: #000;
    }

    .ts-player-page .player .card {
        border: 0;
        box-shadow: none;
    }

    .ts-player-page .player .card-body {
        padding: 0;
    }

    /* The highlight of the entry being spoken is a moment, not content; and a time
       code is one thought, so it is not split over two sheets. Whatever the screen
       gave the text — Blue ink on a White card — reads on paper as it is. */
    .ts-player-page .ts-code.is-current,
    .ts-player-page .ts-code.is-chapter-current {
        background: none;
    }

    .ts-player-page .ts-code {
        break-inside: avoid;
    }

    /* The screen's scroller (the xl rule above) would cut the text at a screen's height:
       on paper the article runs its full length. */
    .ts-player-page .player-slide {
        max-block-size: none;
        overflow: visible;
    }

    .ts-player-page .player-slide img {
        max-block-size: 60vh;
        object-fit: contain;
    }
}

/* ===========================================================================
   PART 3 — authenticated area (formerly account.css)
   =========================================================================== */

/* =============================================================================
   Teleseminar — authenticated area
   -----------------------------------------------------------------------------
   Folded into components.css from the former account.css, loaded by Login /
   MyAccount / Dashboard / Learn / Play / Order / Sales / Staff / Evaluation.
   It used to declare its own --ts-* namespace, colliding with the one above —
   different --ts-radius, different --ts-shadow, under the same names. Its
   identity tokens are now aliases onto the brand tokens in brand.css, kept
   under these names because this section's selectors reference them
   throughout. --ts-blue, --ts-radius, --ts-radius-lg, --ts-shadow, --ts-focus
   and --ts-space-1..5 share their names with brand.css exactly, so they are
   not redeclared here — they fall through from its :root untouched. Layout is
   grid / flex and mobile-first: no floats, no fixed pixel widths, no
   !important.
   ========================================================================== */

:root {
    --ts-navy: var(--ts-blue);
    --ts-navy-deep: var(--ts-blue-dark);
    --ts-accent: var(--ts-yellow);
    --ts-success: var(--ts-feedback-success);
    --ts-warning: var(--ts-feedback-warning);
    --ts-surface: var(--ts-white);
    --ts-surface-muted: var(--ts-lavender);
    --ts-text: var(--ts-blue);
    --ts-text-muted: rgb(var(--ts-blue-rgb) / 70%);
    --ts-border: var(--ts-periwinkle);
}

/* -------------------------------------------------------------- page shell */

.ts-page {
    padding-block: var(--ts-space-5);
    padding-inline: var(--ts-space-3);
    background-color: var(--ts-surface-muted);
}

@media (min-width: 62em) {
    .ts-page {
        padding-inline: clamp(1rem, 5vw, 3.5rem);
    }
}

.ts-page__inner {
    margin-inline: auto;
    max-width: 82rem;
}

.ts-page__header {
    margin-block-end: var(--ts-space-4);
}

.ts-page__eyebrow {
    display: block;
    color: var(--ts-accent);
    font-size: 0.85rem;
    font-weight: 700;
    letter-spacing: 0.18em;
    text-transform: uppercase;
}

.ts-page__title {
    margin: var(--ts-space-1) 0 0;
    color: var(--ts-navy);
    font-size: clamp(1.6rem, 1.1rem + 2vw, 2.4rem);
    line-height: 1.15;
}

.ts-page__lead {
    margin: var(--ts-space-2) 0 0;
    color: var(--ts-text-muted);
    max-width: 60ch;
}

/* ------------------------------------------------------------------- cards */

.ts-card {
    background: var(--ts-surface);
    border: 1px solid var(--ts-border);
    border-radius: var(--ts-radius-lg);
    box-shadow: var(--ts-shadow);
    padding: var(--ts-space-4);
}

.ts-card__title {
    margin: 0 0 var(--ts-space-3);
    color: var(--ts-navy);
    font-size: 1.15rem;
}

/* ------------------------------------------------------------------- grids */

.ts-grid {
    display: grid;
    gap: var(--ts-space-3);
    grid-template-columns: 1fr;
}

@media (min-width: 36em) {
    .ts-grid--2 { grid-template-columns: repeat(2, 1fr); }
}

@media (min-width: 62em) {
    .ts-grid--3 { grid-template-columns: repeat(3, 1fr); }
    .ts-grid--4 { grid-template-columns: repeat(4, 1fr); }
}

/* Tiles auto-fit: no media query needed, the cards decide their own width. */
.ts-tiles {
    display: grid;
    gap: var(--ts-space-3);
    grid-template-columns: repeat(auto-fill, minmax(min(16rem, 100%), 1fr));
    list-style: none;
    margin: 0;
    padding: 0;
}

/* ------------------------------------------------------------- stat tiles */

.ts-stat {
    display: flex;
    gap: var(--ts-space-3);
    align-items: flex-start;
    background: var(--ts-surface);
    border: 1px solid var(--ts-border);
    border-radius: var(--ts-radius-lg);
    padding: var(--ts-space-3);
}

.ts-stat__icon {
    display: grid;
    place-items: center;
    flex: 0 0 auto;
    inline-size: 2.75rem;
    block-size: 2.75rem;
    border-radius: 50%;
    background: rgb(var(--ts-yellow-rgb) / 12%);
    color: var(--ts-accent);
    font-size: 1.15rem;
}

.ts-stat__value {
    margin: 0;
    color: var(--ts-navy);
    font-size: 1.6rem;
    font-weight: 800;
    line-height: 1.1;
    overflow-wrap: anywhere;
}

.ts-stat__value--text {
    font-size: 1.05rem;
    font-weight: 700;
}

.ts-stat__label {
    margin: var(--ts-space-1) 0 0;
    color: var(--ts-text-muted);
    font-size: 0.9rem;
}

/* --------------------------------------------------------- product tiles */

.ts-tile {
    display: flex;
    flex-direction: column;
    background: var(--ts-surface);
    border: 1px solid var(--ts-border);
    border-radius: var(--ts-radius-lg);
    overflow: hidden;
    block-size: 100%;
}

.ts-tile__media {
    aspect-ratio: 16 / 9;
    inline-size: 100%;
    object-fit: cover;
    background: var(--ts-surface-muted);
}

.ts-tile__body {
    display: flex;
    flex-direction: column;
    gap: var(--ts-space-1);
    flex: 1 1 auto;
    padding: var(--ts-space-3);
}

.ts-tile__title {
    margin: 0;
    font-size: 1rem;
    line-height: 1.3;
}

.ts-tile__title a {
    color: var(--ts-navy);
    text-decoration: none;
}

.ts-tile__title a:hover,
.ts-tile__title a:focus-visible {
    text-decoration: underline;
}

.ts-tile__meta {
    margin: 0;
    color: var(--ts-text-muted);
    font-size: 0.875rem;
}

.ts-tile__footer {
    display: flex;
    flex-wrap: wrap;
    gap: var(--ts-space-2);
    align-items: center;
    justify-content: space-between;
    margin-block-start: auto;
    padding-block-start: var(--ts-space-2);
}

/* ------------------------------------------------------------------ forms */

.ts-form {
    display: grid;
    gap: var(--ts-space-3);
}

.ts-fieldset {
    border: 0;
    margin: 0;
    padding: 0;
}

.ts-fieldset > legend {
    float: none;
    inline-size: auto;
    margin-block-end: var(--ts-space-2);
    color: var(--ts-navy);
    font-size: 1.05rem;
    font-weight: 700;
}

.ts-field {
    display: grid;
    gap: var(--ts-space-1);
}

.ts-field > label {
    color: var(--ts-navy);
    font-size: 0.9rem;
    font-weight: 600;
}

.ts-field input,
.ts-field select,
.ts-field textarea {
    inline-size: 100%;
    padding: 0.6rem 0.75rem;
    border: 1px solid var(--ts-border);
    border-radius: var(--ts-radius);
    background: var(--ts-surface);
    color: var(--ts-text);
    font: inherit;
}

.ts-field input:focus-visible,
.ts-field select:focus-visible,
.ts-field textarea:focus-visible {
    outline: none;
    border-color: var(--ts-accent);
    box-shadow: var(--ts-focus);
}

/* Native constraint validation, shown only once the user has left the field. */
.ts-field input:user-invalid,
.ts-field select:user-invalid,
.ts-field textarea:user-invalid {
    border-color: var(--ts-color-danger);
}

.ts-field__hint {
    color: var(--ts-text-muted);
    font-size: 0.8rem;
}

.ts-field__error,
.field-validation-error {
    color: var(--ts-color-danger);
    font-size: 0.85rem;
}

.ts-form__actions {
    display: flex;
    flex-wrap: wrap;
    gap: var(--ts-space-2);
    align-items: center;
}

/* ---------------------------------------------------------------- buttons */

.ts-btn {
    display: inline-flex;
    gap: var(--ts-space-2);
    align-items: center;
    justify-content: center;
    padding: 0.65rem 1.25rem;
    border: 1px solid transparent;
    border-radius: 999px;
    font: inherit;
    font-weight: 700;
    line-height: 1.2;
    text-decoration: none;
    cursor: pointer;
    transition: background-color .15s ease, color .15s ease, border-color .15s ease;
}

.ts-btn:focus-visible {
    outline: none;
    box-shadow: var(--ts-focus);
}

.ts-btn--primary {
    background: var(--ts-navy-deep);
    color: var(--ts-white);
}

.ts-btn--primary:hover { background: var(--ts-navy); color: var(--ts-white); }

.ts-btn--accent {
    background: var(--ts-accent);
    color: var(--ts-color-ink);
}

.ts-btn--accent:hover { background: var(--ts-color-accent-dark); color: var(--ts-color-ink); }

.ts-btn--ghost {
    background: transparent;
    border-color: var(--ts-border);
    color: var(--ts-navy);
}

.ts-btn--ghost:hover { border-color: var(--ts-navy-deep); }

.ts-btn[disabled],
.ts-btn[aria-disabled="true"] {
    opacity: .55;
    cursor: not-allowed;
}

.ts-btn--icon {
    padding: 0.5rem;
    inline-size: 2.5rem;
    block-size: 2.5rem;
    border-radius: 50%;
}

/* ------------------------------------------------------------------ badge */

.ts-badge {
    display: inline-flex;
    align-items: center;
    gap: var(--ts-space-1);
    padding: 0.15rem 0.6rem;
    border-radius: 999px;
    background: var(--ts-surface-muted);
    color: var(--ts-navy);
    font-size: 0.8rem;
    font-weight: 700;
}

.ts-badge--success { background: var(--ts-feedback-success-bg); color: var(--ts-color-success); }
.ts-badge--warning { background: rgb(var(--ts-yellow-rgb) / 18%); color: var(--ts-color-accent-dark); }

/* --------------------------------------------------------------- progress */

.ts-progress {
    inline-size: 100%;
    block-size: 0.5rem;
    border-radius: 999px;
    background: var(--ts-surface-muted);
    overflow: hidden;
}

.ts-progress__bar {
    display: block;
    block-size: 100%;
    border-radius: inherit;
    background: var(--ts-success);
}

/* ----------------------------------------------------------------- tables */

.ts-table-wrap {
    overflow-x: auto;
}

.ts-table {
    inline-size: 100%;
    border-collapse: collapse;
    background: var(--ts-surface);
}

.ts-table th,
.ts-table td {
    padding: 0.6rem 0.75rem;
    border-block-end: 1px solid var(--ts-border);
    text-align: start;
    vertical-align: middle;
}

.ts-table thead th {
    color: var(--ts-navy);
    font-size: 0.85rem;
    letter-spacing: .04em;
    text-transform: uppercase;
}

.ts-table tfoot td {
    font-weight: 700;
}

/* --------------------------------------------------------------- messages */

.ts-alert {
    display: flex;
    gap: var(--ts-space-2);
    align-items: flex-start;
    padding: var(--ts-space-3);
    border: 1px solid var(--ts-border);
    border-inline-start: 4px solid var(--ts-accent);
    border-radius: var(--ts-radius);
    background: var(--ts-surface);
}

.ts-empty {
    padding: var(--ts-space-5) var(--ts-space-3);
    border: 1px dashed var(--ts-border);
    border-radius: var(--ts-radius-lg);
    color: var(--ts-text-muted);
    text-align: center;
}

/* --------------------------------------------------------------- stepper */

.ts-steps {
    display: flex;
    flex-wrap: wrap;
    gap: var(--ts-space-2);
    list-style: none;
    margin: 0 0 var(--ts-space-4);
    padding: 0;
    counter-reset: ts-step;
}

.ts-steps__item {
    display: flex;
    align-items: center;
    gap: var(--ts-space-2);
    padding: 0.4rem 0.9rem;
    border: 1px solid var(--ts-border);
    border-radius: 999px;
    background: var(--ts-surface);
    color: var(--ts-text-muted);
    font-size: 0.9rem;
}

.ts-steps__item::before {
    counter-increment: ts-step;
    content: counter(ts-step);
    display: grid;
    place-items: center;
    inline-size: 1.5rem;
    block-size: 1.5rem;
    border-radius: 50%;
    background: var(--ts-surface-muted);
    font-weight: 700;
}

.ts-steps__item[aria-current="step"] {
    border-color: var(--ts-navy-deep);
    color: var(--ts-navy);
    font-weight: 700;
}

.ts-steps__item[aria-current="step"]::before {
    background: var(--ts-navy-deep);
    color: var(--ts-white);
}

/* ============================================================ video player */

.ts-player {
    display: grid;
    gap: var(--ts-space-4);
    grid-template-columns: 1fr;
    align-items: start;
}

@media (min-width: 64em) {
    .ts-player {
        grid-template-columns: minmax(0, 5fr) minmax(0, 7fr);
    }
}

.ts-player__video {
    inline-size: 100%;
    aspect-ratio: 16 / 9;
    border-radius: var(--ts-radius);
    background: #000;
}

.ts-player__toolbar {
    display: flex;
    flex-wrap: wrap;
    gap: var(--ts-space-3);
    align-items: center;
    justify-content: space-between;
    margin-block-end: var(--ts-space-3);
}

.ts-player__nav {
    display: flex;
    gap: var(--ts-space-2);
    align-items: center;
    justify-content: space-between;
    margin-block-end: var(--ts-space-2);
}

.ts-player__nav h2 {
    margin: 0;
    font-size: 1.15rem;
    text-align: center;
}

/* Table of contents built on <details>: no JS, keyboard accessible by default. */


/* Slide content, highlighted in sync with the video. */
.ts-slide {
    border: 1px solid var(--ts-border);
    border-radius: var(--ts-radius);
    background: var(--ts-surface);
    overflow: hidden;
}

.ts-slide__header {
    padding: 0.6rem 0.85rem;
    background: var(--ts-navy-deep);
    color: var(--ts-white);
}

.ts-slide__header h2 { margin: 0; font-size: 1.05rem; }

.ts-slide__body { padding: var(--ts-space-2); }

.ts-code {
    padding: 0.6rem 0.75rem;
    border-radius: var(--ts-radius);
    /* The colour fades rather than snapping. The old site animated this from jQuery UI over a
       full second, which itself read as the text lagging behind the speaker; .4s keeps the
       softness without the lag. Nothing is animated from script.

       No prefers-reduced-motion guard here: the global rule further up this file already caps
       every transition-duration at .01ms under that setting. */
    transition: background-color .4s ease;
    white-space: pre-line;
}

.ts-code--heading { font-weight: 700; color: var(--ts-navy); }

/* --------------------------------------------------------- timeline highlight

   Two markers, and they never clear each other: the chapter heading stays lit for its whole
   chapter while the sentence being spoken lights up underneath it. That is what the old site
   drew with `bg-lightblue` on the title and `bg-timeline` on the line, and what a single
   `.is-current` had flattened into one undifferentiated mark.

   What stood here was `rgb(var(--ts-blue-rgb) / 14%)`, which painted nothing at all:
   --ts-blue-rgb is comma-separated ("24, 32, 111"), and feeding a legacy comma list into the
   modern slash syntax is invalid, so the browser dropped the whole declaration. The highlight
   has been invisible rather than merely bland — worth knowing when reading the bug report.

   Both colours are brand tokens: Periwinkle is what `bg-lightblue` resolves to, and
   `bg-timeline` resolves to Yellow. The Yellow is tinted because the V2 column is far wider
   than the old one, where full-strength #FAA916 behind a paragraph of navy body copy reads as
   a wall rather than as a highlight. The cards these sit on are white, so the mix is towards
   white in both modes. */
.ts-code.is-current {
    background: color-mix(in srgb, var(--ts-yellow) 30%, var(--ts-white));
}

/* Declared after `.is-current`, at equal specificity, so the blue wins on a heading that is
   also the entry being spoken — which is exactly when one element carries both classes. */
.ts-code.is-chapter-current {
    background: var(--ts-periwinkle);
}

.ts-code img {
    display: block;
    inline-size: 100%;
    block-size: auto;
    margin-inline: auto;
}

/* The answer window of a question is no longer drawn in the column — its wording is the option
   the question block lists, and the column showed every option twice. Nothing marks its moment
   any more: the option used to be tinted while its window was spoken, and that tint read as a
   hint at the right answer before the trainee had answered. What stood here,
   `.ts-answer.is-current`, matched nothing any more. */

@keyframes ts-blink { 50% { opacity: .25; } }

.ts-blink { animation: ts-blink .6s linear 3; }

/* --------------------------------------------------------- misc utilities */

.ts-stack { display: grid; gap: var(--ts-space-3); }
.ts-cluster { display: flex; flex-wrap: wrap; gap: var(--ts-space-2); align-items: center; }
.ts-muted { color: var(--ts-text-muted); }
.ts-nowrap { white-space: nowrap; }

.ts-visually-hidden {
    position: absolute;
    inline-size: 1px;
    block-size: 1px;
    margin: -1px;
    padding: 0;
    overflow: hidden;
    clip-path: inset(50%);
    white-space: nowrap;
}

@media (prefers-reduced-motion: reduce) {
    *,
    *::before,
    *::after {
        animation-duration: .01ms !important;
        animation-iteration-count: 1 !important;
        transition-duration: .01ms !important;
    }
}

/* ---------------------------------------------------------------- the player
   Plyr ships its own palette. Its variables are declared on :root and its
   stylesheet is linked before this file, so re-declaring them here is what makes
   the player carry the site's blue instead of the plugin's default. */

:root {
    --plyr-color-main: var(--ts-color-primary);
    --plyr-video-control-color: var(--ts-white);
    --plyr-badge-background: var(--ts-color-primary);
    --plyr-font-family: var(--ts-font);
    --plyr-range-track-height: 5px;
    --plyr-control-radius: 6px;
}

/* ----------------------------------------------------- the certificate dialog

   Not a responsive defect, but found while checking the screen: the certificate
   <dialog> was painted in the page flow at every width instead of waiting for
   showModal(). This file gives .ts-card `display: flex`, and an author
   rule beats the user agent's `dialog:not([open]) { display: none }`. Only the
   player uses dialog.ts-card, so the correction belongs here.                  */

dialog.ts-card:not([open]) {
    display: none;
}

/* ------------------------------------------------------ pointer target size

   Checked at 360 / 768 / 1024 / 1440 px. Three controls of this screen were
   smaller than the 44 x 44 px minimum at every width, which is a real problem
   on a phone where the table of contents is a single column of thin rows:

     - .ts-btn--icon (print, previous slide, next slide) measured 40 x 40
     - the breadcrumb links of the Learn page measured 20 tall

   Only the box grows; nothing here changes a colour, a font or a layout.
   Plyr's own control bar used to be grown here too, under a `.ts-player` root
   that no view carries any more; the bar is sized through Plyr's variables on
   `.player-media` instead, with the video rules further up this file.          */

.ts-btn {
    min-block-size: 2.75rem;
}

.ts-btn--icon {
    inline-size: 2.75rem;
    block-size: 2.75rem;
}


.ts-page__inner > nav > .breadcrumb .breadcrumb-item a {
    display: inline-flex;
    align-items: center;
    min-block-size: 2.75rem;
}

/* On a slide that is not finished the bar is there to be read, not dragged — it is what
   tells the trainee how much of the slide is left, which is why it is in the control set
   for both states. The clock beside it goes, as it did on the old site: an exact figure
   invites the seeking that the script then refuses.

   The control is hidden rather than dropped from the set, because Plyr lays the bar out
   through the same container. `pointer-events` is belt and braces over the script's
   `seeking` guard — the bar simply does not take a drag here. */
.player-media--no-clock .plyr__time {
    display: none;
}

.player-media--no-clock .plyr__progress__container {
    pointer-events: none;
}

/* The last slide of the training, once its video has run out: player.js puts this class on
   the wrapper from `ended`. The play button Plyr sets back on the poster means "continue to
   the next slide" everywhere else in the player; here there is no next slide and the
   certificate dialog is what follows, so the button goes. Taken off again on the first play
   or scrub, which is when a replay is actually asked for. Plyr shows the button through
   `.plyr--full-ui.plyr--video .plyr__control--overlaid { display: block }`, three classes
   deep, so the same three are named here to outweigh it. */
.player-media--last-ended .plyr--full-ui.plyr--video .plyr__control--overlaid {
    display: none;
}

/* The speed pane is the only pane of the settings menu the trainee ever sees — player.js
   presses the "Speed" entry of Plyr's home pane for them the moment the menu opens, speed
   being the one setting there is. So the back button at the head of that pane is a heading
   now and not a way back: no chevron, no hover, no pointer. The rule underneath it stays,
   which is what separates the title from the choices. */
.player-media .plyr__menu__container .plyr__control--back {
    pointer-events: none;
}

.player-media .plyr__menu__container .plyr__control--back::after {
    display: none;
}

/* ------------------------------------------------------- the end of a slide

   When the video runs out, the play button Plyr puts back on the poster stops being a
   replay: player.js turns that press into the next slide. A stopped picture says none of
   that on its own, so the button breathes — fading out and back in — until it is pressed.

   No script is involved: the notice below the picture is the state, and `:has()` reads it
   off the rail the two now share. `[data-goto-slide="0"]` is the last slide of the
   training, where the press leads nowhere and an invitation to move on would be a lie —
   the view leaves the notice out there altogether now, and `.player-media--last-ended`
   further up hides the play button itself; the test stays as a guard.
   Nothing is needed for playback either — Plyr's own `.plyr--playing` rule hides this
   button with `visibility`, which no opacity animation can bring back.

   Opacity only, and on the control's own layer: the button is a circle sitting over the
   frame, and anything that moved or scaled it would read as the picture itself moving.
   No prefers-reduced-motion guard here — the global rule at the top of this file already
   caps every animation on the page to a single 0.001ms pass. */
@keyframes player-attention {
    50% { opacity: 0.2; }
}

/* The test is kept off the button itself and left to say one thing: the rail is at the end
   of its slide. An `#id` inside `:has()` counts towards specificity, so a rule written that
   way straight onto the control would outweigh anything shorter — the `:hover` below could
   never take it back. A custom property carries the answer down instead, which leaves the
   two rules that do the animating at the same weight and in source order. */
.player-rail:has(#player-next-slide:not([hidden]) [data-goto-module]:not([data-goto-slide="0"])) {
    --player-attention: player-attention 1.2s ease-in-out infinite;
}

/* The same breathing once a question has been answered: the video no longer restarts by itself
   after the answer — the play is the trainee's, and it starts the second part of the slide — so
   the stopped picture has to say that play is what is asked next. player.js puts the class on
   the wrapper from the component's `AnswerConfirmed` call and takes it off on the next play. */
.player-media--answered {
    --player-attention: player-attention 1.2s ease-in-out infinite;
}

.player-rail .plyr__control--overlaid {
    animation: var(--player-attention, none);
}

/* Once it is under the pointer it has been found; a control that keeps flashing while it
   is being aimed at is only harder to press. Plyr's own `opacity: 1` on these two states
   cannot do it alone — a running animation outranks any ordinary declaration. */
.player-rail .plyr__control--overlaid:hover,
.player-rail .plyr__control--overlaid:focus {
    animation: none;
}

/* The way on, directly under the picture rather than under the slide pane, so it stands
   beside the play button that performs the same move. Flush with the picture's right edge,
   where a "next" action is looked for, and not `funnel-actions--end`: that class is the
   footer of a wide form. The button keeps its own width, so the label is what sits at
   the edge and not the middle of a bar as wide as the picture. */
.player-next-slide {
    display: flex;
    justify-content: flex-end;
}

/* "Please answer the question", under the picture: what a refused play means. Same place and
   same edge as the next-slide notice above, since it explains the same button; a bare line in
   white on the Blue ground like the score line, not a card. player.js blinks it (`ts-blink`)
   on every refused press, so a second press is seen to have landed too. */
.player-answer-first {
    display: flex;
    align-items: baseline;
    justify-content: flex-end;
    gap: 0.5rem;
    padding-inline: 0.25rem;
    font-weight: 600;
    color: var(--ts-white);
}

/* The quiz score under the picture: a bare recap line, white on the Blue ground the player runs
   on — no card, so the picture stays the only framed thing in the rail. Sentence and reset icon
   sit together at the end of the row, as the mockup draws them. The reset is a bare white icon
   and not the pale-blue chip of `.btn-ht-secondary`: a chip would be the one framed thing on the
   line. Bootstrap's button variables, as the ghost rule of the funnel does it. */
.player-score {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    justify-content: flex-end;
    gap: 0.25rem 0.5rem;
    padding-inline: 0.25rem;
    font-weight: 500;
    color: var(--ts-white);
}

.player-score__reset {
    --bs-btn-bg: transparent;
    --bs-btn-color: var(--ts-white);
    --bs-btn-border-color: transparent;
    --bs-btn-hover-bg: transparent;
    --bs-btn-hover-color: var(--ts-periwinkle);
    --bs-btn-hover-border-color: transparent;
    --bs-btn-active-bg: transparent;
    --bs-btn-active-color: var(--ts-periwinkle);
    --bs-btn-active-border-color: transparent;
    padding: 0.25rem 0.5rem;
    font-size: 1.25rem;
    line-height: 1;
}

/* --- the quiz ------------------------------------------------------------- */

/* The question block is the entry player.js marks current while the speaker poses the question,
   but it does not take the yellow of a spoken sentence: the question is on screen from the start,
   and a yellow wall behind the form read as a highlight of the whole block. Same specificity as
   `.ts-code.is-current` above, declared later, so this wins. */
.ts-code--quiz.is-current {
    background: none;
}

/* The options of a question, rendered by MultipleAnswersQuestion.razor: one strip each, the
   strip being the label, so the whole line is the target and not the box alone. Once the outcome
   is on screen the fieldset carries `--revealed` and the rows read as a result: the strips stop
   taking the pointer, and the component takes the inputs out of the tab order and ignores any
   change. Not the `disabled` attribute — a disabled box is drawn greyed out, and the box is what
   says which option the trainee chose. The top margin is the breath between the question and the
   first option. */
.player-quiz__options {
    display: grid;
    gap: 0.35rem;
    min-inline-size: 0;
    margin: 0.75rem 0 0;
    padding: 0;
    border: 0;
}

.player-quiz__option {
    display: flex;
    align-items: center;
    gap: 0.6rem;
    margin: 0;
    padding: 0.5rem 0.75rem;
    border: 1px solid var(--ts-color-border);
    border-radius: var(--ts-radius);
    background: var(--ts-white);
    cursor: pointer;
    transition: background-color .4s ease, border-color .4s ease;
}

    .player-quiz__options--revealed .player-quiz__option {
        pointer-events: none;
        cursor: default;
    }

    .player-quiz__option input {
        flex: 0 0 auto;
        margin: 0;
        accent-color: var(--ts-color-primary);
    }

/* No tint while the option's own answer window is being spoken: a highlighted option read as a
   hint at the right answer before the trainee had answered. Nothing marks that moment now. */
.player-quiz__option-text {
    flex: 1 1 auto;
    min-inline-size: 0;
    padding: 0.1rem 0.3rem;
}

/* The outcome: every right option green whatever the trainee did, a wrong one red only when it
   was ticked. Brand green and red mixed towards white, not the feedback tokens: the player runs
   in learn mode, whose tokens are lightened for the Blue ground, and these rows sit inside the
   white slide card — the same reasoning as the `.is-current` tints above. */
.player-quiz__option--right {
    border-color: color-mix(in srgb, var(--ts-green) 45%, var(--ts-white));
    background: color-mix(in srgb, var(--ts-green) 12%, var(--ts-white));
}

.player-quiz__option--wrong {
    border-color: color-mix(in srgb, var(--ts-red) 40%, var(--ts-white));
    background: color-mix(in srgb, var(--ts-red) 7%, var(--ts-white));
}

.player-quiz__verdict {
    display: inline-flex;
    align-items: center;
    gap: 0.35rem;
    margin-inline-start: auto;
    font-size: 0.85rem;
    font-weight: 600;
    text-align: end;
}

.player-quiz__option--right .player-quiz__verdict { color: var(--ts-green); }
.player-quiz__option--wrong .player-quiz__verdict { color: var(--ts-red); }

.player-quiz__confirm {
    margin-block-start: 0.6rem;
}

/* The score box the last answered question of the slide draws: the cap, the bare figure large,
   and the way to start over at the end of the row — the mockup's box. The line under the video
   keeps the sentence; this one is the figure only, so it is sized as a figure, not as text. */
.player-quiz__score {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 0.5rem 0.9rem;
    margin-block-start: 0.75rem;
    padding: 0.75rem 1rem;
    border-radius: var(--ts-radius);
    background: var(--ts-color-surface-muted);
}

.player-quiz__score-icon {
    font-size: 1.6rem;
    color: var(--ts-color-ink);
}

.player-quiz__score-value {
    font-size: 1.75rem;
    font-weight: 700;
    line-height: 1.1;
    color: var(--ts-color-ink);
}

/* An outline, not the pale-blue chip of `.btn-ht-secondary`: the box is already that pale
   blue, and a chip on it would vanish. Same ink as the figure, and the chip comes back as the
   hover fill. Bootstrap's button variables, as the ghost rule of the funnel does it. */
.player-quiz__reset {
    --bs-btn-bg: transparent;
    --bs-btn-color: var(--ts-color-ink);
    --bs-btn-border-color: var(--ts-color-ink);
    --bs-btn-hover-bg: var(--ts-periwinkle);
    --bs-btn-hover-color: var(--ts-color-ink);
    --bs-btn-hover-border-color: var(--ts-color-ink);
    --bs-btn-active-bg: var(--ts-periwinkle);
    --bs-btn-active-color: var(--ts-color-ink);
    --bs-btn-active-border-color: var(--ts-color-ink);
    margin-inline-start: auto;
    border-width: 1px;
    border-style: solid;
}

/* A lost completion is the one failure on this screen the trainee must not scroll past, so
   it takes the feedback palette rather than the quiet treatment above. The tokens are read
   off the mode block: the `--ts-color-*` aliases on :root point at names that only exist on
   the body class and resolve to nothing. */
.player-complete-failed {
    gap: var(--ts-space-md);
    padding: 0.75rem 1rem;
    border-radius: var(--ts-radius);
    background: var(--ts-feedback-error-bg);
    color: var(--ts-feedback-error);
}

/* -------------------------------------------------------- learn-mode hero
   The vendored theme washes every one of these page shells — Trainings
   (Domains/Skills/Catalogue/DomainDetails/CollectionDetails), MyAccount,
   Support and the training Details page — in --ht-background-light via one
   of two classes on the outer <section>: .bg-light (main.css:871, its own
   !important) or .bg-transparent-top (main.css:880). All of them are reachable
   both signed out (the public catalogue) and signed in, and only the signed-in
   visitor should see the dark-Blue/white-text version — hence scoped to
   .ts-mode-learn rather than repointing --ht-background-light itself, which
   would also flip the same two classes on purely public pages (Home/About/
   Contact/Speakers/Staff/Error). The !important on the .bg-light pairing
   matches the vendor rule it has to outrank; .bg-transparent-top carries none,
   so the extra specificity alone is enough there. */
.ts-mode-learn .bg-light,
.ts-mode-learn .bg-transparent-top {
    background-color: var(--ht-color-dark) !important;
}

.ts-mode-learn .bg-light .heading :is(h1, h2, h3, h4, h5, h6, .title),
.ts-mode-learn .bg-light .heading .title a,
.ts-mode-learn .bg-transparent-top .heading :is(h1, h2, h3, h4, h5, h6, .title),
.ts-mode-learn .bg-transparent-top .heading .title a,
.ts-mode-learn .bg-light .ts-status-filter__label,
.ts-mode-learn .bg-transparent-top .ts-status-filter__label {
    color: var(--ts-white);
}

/* Details' own <h1> additionally carries #training-overview-title
   (main.css:208), which sets its own Blue — an id beats any class selector
   above, .heading and .bg-light included, no matter how many are chained.
   Matched here with an id of equal specificity so no !important is needed. */
.ts-mode-learn #training-overview-title {
    color: var(--ts-white);
}

/* The programme accordion's *open* item already has its own explicit paint
   (below); a *collapsed* item (items 2+ on first load) falls through to
   Bootstrap's own --bs-accordion-bg (#fff, never redirected for learn mode)
   and --bs-accordion-btn-color (--bs-body-color, which brand.css does flip
   to White for .ts-mode-learn) — White text on a still-White button. Kept
   White/Blue rather than repainting to the dark panel tone, matching the
   White-card-Blue-text look every other list on this page already has. */
.ts-mode-learn .ts-programme {
    --bs-accordion-bg: var(--ts-surface);
    --bs-accordion-btn-color: var(--ts-on-surface);
}

.ts-mode-learn .bg-light .heading .sub-title,
.ts-mode-learn .bg-transparent-top .heading .sub-title {
    /* --ht-color-white-rgb is comma-separated ("255, 255, 255", main.css:277)
       — mixing that legacy form with the modern rgb(x / a%) slash syntax is
       invalid CSS, so the declaration was dropped and every rule below fell
       back to an inherited colour instead of the White they specify. The
       legacy rgba(var(--x), alpha) form both syntaxes actually agree on. */
    color: rgba(var(--ht-color-white-rgb), 0.8);
}

.ts-mode-learn .bg-light .breadcrumb-item a,
.ts-mode-learn .bg-transparent-top .breadcrumb-item a {
    color: rgba(var(--ht-color-white-rgb), 0.8);
}

.ts-mode-learn .bg-light .breadcrumb-item.active,
.ts-mode-learn .bg-transparent-top .breadcrumb-item.active {
    color: rgba(var(--ht-color-white-rgb), 0.45);
}

/* The privacy statement (Home/Privacy) and the general conditions
   (Home/GeneralConditions) are the .bg-light pages whose body copy sits
   straight on the ground with no card under it: About and FAQ set theirs in
   White panels, and the catalogue shells carry only headings and
   breadcrumbs outside theirs. main.css paints `body` with --ht-color-content
   (Blue) — not Bootstrap's --bs-body-color, the one brand.css flips — so once
   the shell goes dark for a signed-in visitor (who reaches the page from the
   footer like anyone else) every paragraph, the address and the mailto link
   keep their Blue and vanish into it; only the headings survive, by the rule
   above. Body copy takes the mode's text token; the link the same, with the
   periwinkle the mode gives hovered links. main.css's bare `a` rule (Blue) is
   what the link selector has to outrank.

   Painted on the children of the section's own body container — the one the
   view drops the CMS block into — rather than on `.info-box-desc`: that class
   is the block's section markup, and a block authored without it (the
   general conditions hold one bare <p> while their text is being written)
   kept its Blue and showed a heading over nothing. The empty-state card is
   the container's other child; it is White in both modes and keeps its own
   text, so it is left out. */
.ts-mode-learn #privacy-box > .container-fluid > :not(.ts-empty-state),
.ts-mode-learn #privacy-box > .container-fluid > :not(.ts-empty-state) a,
.ts-mode-learn #conditions-box > .container-fluid > :not(.ts-empty-state),
.ts-mode-learn #conditions-box > .container-fluid > :not(.ts-empty-state) a {
    color: var(--ts-text);
}

.ts-mode-learn #privacy-box > .container-fluid > :not(.ts-empty-state) a:hover,
.ts-mode-learn #privacy-box > .container-fluid > :not(.ts-empty-state) a:focus-visible,
.ts-mode-learn #conditions-box > .container-fluid > :not(.ts-empty-state) a:hover,
.ts-mode-learn #conditions-box > .container-fluid > :not(.ts-empty-state) a:focus-visible {
    color: var(--ts-text-muted);
}

/* The French block of the statement was pasted into the CMS with the page
   shell around it: a second <main>, a second #privacy-box section carrying
   .pd-left-right-50px, its own .container-fluid and its own .col-xl-8. Nested
   in the view's container, that shell pushed the copy 74 px to the right of
   the heading, squeezed it to two thirds of the width and, with the shell's
   own .pd-top-300 / .pd-bottom-500, opened 3rem more under the heading and
   5rem more above the footer. Undo the shell's padding so the copy sits on
   the heading's left edge, straight under it, and runs the full width of the
   container, in every language that carries it — and on the general
   conditions page, whose blocks are authored the same way. */
#privacy-box main > section,
#conditions-box main > section {
    padding: 0 !important;
}

#privacy-box main .container-fluid,
#conditions-box main .container-fluid {
    padding-left: 0;
    padding-right: 0;
}

#privacy-box main .col-12,
#conditions-box main .col-12 {
    width: 100%;
}

/* .ts-empty-state (_NoTrainingsAvailable, Details' own "no modules yet"
   state) was assumed to sit directly on the page background like the
   headings above, so its text/badge were repainted White here too — but
   .ts-empty-state carries its own White background unconditionally
   (components.css, "background: var(--ts-white)"), in every context it's
   used in. It was already White-card-on-White-page correct before this
   session's inversion work; repainting its text White made it White-on-
   White instead. No override needed — removed. */

/* Hard rule carried over from brand.css: text on a White surface never
   flips. Details' speaker-name heading is the one .heading the two rules
   above would otherwise catch under a .ts-panel/.card — everything else they
   touch sits straight on the dark background, not inside either.

   .tour-item (the catalogue's own White training card — .heading.heading-sub
   > .sub-title holds the speaker name) was missing from this list: its
   speaker name inherited the "White heading text" rule meant for the dark
   page background behind the card, not the White card itself, and went
   invisible. */
.ts-mode-learn .bg-light :is(.ts-panel, .card, .tour-item) .heading :is(h1, h2, h3, h4, h5, h6, .title),
.ts-mode-learn .bg-light :is(.ts-panel, .card, .tour-item) .heading .title a,
.ts-mode-learn .bg-light :is(.ts-panel, .card, .tour-item) .heading .sub-title,
.ts-mode-learn .bg-transparent-top :is(.ts-panel, .card, .tour-item) .heading :is(h1, h2, h3, h4, h5, h6, .title),
.ts-mode-learn .bg-transparent-top :is(.ts-panel, .card, .tour-item) .heading .title a,
.ts-mode-learn .bg-transparent-top :is(.ts-panel, .card, .tour-item) .heading .sub-title {
    color: var(--ts-blue);
}

/* ===========================================================================
   PART 4 — header, nav, cart (2026 restyle)
   ---------------------------------------------------------------------------
   _Layout.cshtml carries the structural detail (why the top bar is public-only,
   why the cart moved); this is only the paint. main.css/responsive.css still own
   the layout mechanics (.header-box flex row, .mobile-nav-toggle, the drawer) —
   nothing here touches those.
   =========================================================================== */

/* The top bar reads heavier now: 0.9rem/regular in the reference, 1rem/Bold
   here, with more room top and bottom. Public mode only — it never renders
   once signed in, so no learn-mode branch is needed. */
.header .top-bar {
    font-size: 1rem;
}

.header .top-bar .top-bar-text {
    padding-block: 0.85rem;
    font-weight: var(--ts-weight-heading);
}

/* Sign-in / new-account links only (left side — the right-hand pickers also
   use .top-bar-text): flex anchor so the icon and label centre on each other
   with a wider gap than the old glyph + text-space gave. */
.header .top-bar .top-bar-left .top-bar-text {
    display: inline-flex;
    align-items: center;
    gap: 0.6rem;
}

/* Yellow on rollover — the same answer the profession and language pickers on
   the right already give (they are <button class="top-bar-text">, styled
   further up this file). Without this the sign-in and new-account links fall
   through to main.css's `.header .top-bar a:hover`, which paints them
   --ht-color-primary: Blue on the Blue bar, so the label reads as going out
   rather than lighting up. Four classes plus the pseudo-class outrank that
   rule's three-classes-and-an-element, so no !important is needed. The lock
   and plus SVGs follow along on their own — both are stroke="currentColor".
   Focus is included so a keyboard sees what a pointer sees. */
.header .top-bar .top-bar-left .top-bar-text:hover,
.header .top-bar .top-bar-left .top-bar-text:focus {
    color: var(--ts-yellow);
}

/* The stroke-based lock/plus SVGs in those links, sized to the text. 1em
   tracks the top bar's font-size; flex: none stops the label from squeezing
   the icon when it wraps. */
.ts-top-bar-icon {
    inline-size: 1em;
    block-size: 1em;
    flex: none;
}

/* --------------------------------------------------------------- wordmark */

/* The signed-in replacement for the logo: two stacked lines of text instead of
   an <img>, because line 1 has to hold the visitor's name. Same position and
   sizes as the public logo it replaces. */
.ts-wordmark {
    gap: 0.85rem;
}

.ts-wordmark__link {
    display: flex;
    flex-direction: column;
    color: var(--ts-white);
    line-height: 1.2;
}

/* Yellow on rollover, the same answer the nav links give (responsive.css's
   `.navbar ul li a:hover`). Without this the wordmark falls through to
   main.css's bare `a:hover`, which paints it --ht-color-primary-dark — a dark
   Blue that all but disappears against the Blue header. A class plus the
   pseudo-class outranks that element-and-pseudo-class rule, so no !important.
   Only line 1 (the name) follows along: line 2 carries its own colour below
   and stays Periwinkle, keeping the two lines' hierarchy intact. Focus is
   included so a keyboard sees what a pointer sees. */
.ts-wordmark__link:hover,
.ts-wordmark__link:focus-visible {
    color: var(--ts-yellow);
}

.ts-wordmark__line-1 {
    font-size: clamp(1.15rem, 1rem + 0.6vw, 1.5rem);
    font-weight: var(--ts-weight-display);
    letter-spacing: var(--ts-tracking-tight);
}

.ts-wordmark__line-2 {
    font-size: 0.9rem;
    font-weight: var(--ts-weight-body);
    color: var(--ts-periwinkle);
}

.ts-wordmark__logout-btn {
    display: grid;
    place-items: center;
    inline-size: 2rem;
    block-size: 2rem;
    border-radius: 50%;
    border: 1px solid var(--ts-periwinkle);
    background: transparent;
    color: var(--ts-white);
    font-size: 0.85rem;
    transition: background-color var(--ts-transition);
}

.ts-wordmark__logout-btn:hover,
.ts-wordmark__logout-btn:focus-visible {
    background: rgb(var(--ts-periwinkle-rgb) / 18%);
}

/* ------------------------------------------------------------------- cart */

/* Moved out of the top bar into logo-nav-box, at the far right of the main
   nav, in both modes. Same anatomy as .ts-wordmark__logout-btn — an outlined
   circle, transparent until hovered — so the two round buttons in the header
   read as one family instead of the cart being a separately-styled fill. Body
   tokens (not a fixed colour) because this button renders on both the White
   public header and the Blue learn-mode one. */
.ts-header-cart {
    margin-inline-start: 1rem;
}

#cardli {
    position: relative;
}

#cardli > a {
    display: grid;
    place-items: center;
    inline-size: 2.75rem;
    block-size: 2.75rem;
    border-radius: 50%;
    border: 1px solid var(--bs-border-color);
    background: transparent;
    color: var(--bs-body-color);
    transition: background-color var(--ts-transition);
}

    #cardli > a:hover,
    #cardli > a:focus-visible {
        background: rgb(var(--ts-periwinkle-rgb) / 18%);
    }

    #cardli #CardIcon {
        font-size: 1.05rem;
    }

/* Overrides main.css's #cart-badge (a plain inline pill) to sit as a small
   circular counter on the button's corner, and moves it onto the Yellow
   accent — a counter is not an error, so it does not borrow the danger red. */
#cart-badge {
    position: absolute;
    inset-block-start: -0.2rem;
    inset-inline-end: -0.2rem;
    inline-size: 1.15rem;
    block-size: 1.15rem;
    display: grid;
    place-items: center;
    padding: 0;
    border-radius: 50%;
    background: var(--ts-yellow);
    color: var(--ts-blue);
    font-size: 0.65rem;
    font-weight: var(--ts-weight-heading);
    line-height: 1;
}

/* ---------------------------------------------------------- learn-mode chrome */

/* No separator between the top bar and the nav row in learn mode — there is no
   top bar. A subtle lighter-navy rule closes the header instead, since Blue on
   Blue would otherwise run straight into the page. */
.ts-mode-learn .header .header-box {
    border-block-end: 1px solid var(--ts-blue-rule);
}

/* main.js's tnHeaderSticked adds .sticked/.showed past 130/260px of scroll;
   main.css then fixes .logo-nav-box to the viewport with a White background —
   that part is correct in both modes. What it doesn't account for is that
   the wordmark, tagline, logout button and nav text are all styled White/
   Periwinkle for the normal Blue header, so on the White sticky bar they
   turn unreadable. Public mode already reads fine here (--bs-body-color is
   Blue there); this repaints the same elements Blue for learn mode only. */
.ts-mode-learn .header.sticked.showed .ts-wordmark__link {
    color: var(--ts-blue);
}

/* The rule above is four classes, so it also out-ranks .ts-wordmark__link:hover
   and would leave the wordmark with no rollover at all on the sticky bar. This
   restores one, in the colour responsive.css gives the nav links on that same
   White background (--ht-color-primary) rather than Yellow, which has too
   little contrast there. */
.ts-mode-learn .header.sticked.showed .ts-wordmark__link:hover,
.ts-mode-learn .header.sticked.showed .ts-wordmark__link:focus-visible {
    color: var(--ht-color-primary);
}

.ts-mode-learn .header.sticked.showed .ts-wordmark__line-2 {
    color: rgb(var(--ts-blue-rgb) / 70%);
}

.ts-mode-learn .header.sticked.showed .ts-wordmark__logout-btn {
    border-color: var(--ts-border);
    color: var(--ts-blue);
}

.ts-mode-learn .header.sticked.showed .ts-wordmark__logout-btn:hover,
.ts-mode-learn .header.sticked.showed .ts-wordmark__logout-btn:focus-visible {
    background: rgb(var(--ts-blue-rgb) / 10%);
}

/* .navbar ul li a is shared by two different layouts: the desktop horizontal
   bar (where .sticked.showed genuinely means a White background, needing
   Blue text) and, below 1200px, the same markup becomes the slide-out mobile
   drawer — whose own background is a constant dark rgba(navy, 0.95)
   regardless of .sticked/.showed. Unscoped, this rule followed the visitor
   into the drawer and painted its links Blue-on-Blue. Scoped to the desktop
   breakpoint the sticky header's White background actually applies at. */
@media (min-width: 1200px) {
    .ts-mode-learn .header.sticked.showed .navbar ul li a {
        color: var(--ts-blue);
    }
}

.ts-mode-learn .header.sticked.showed #cardli > a {
    border-color: var(--ts-border);
    color: var(--ts-blue);
}

.ts-mode-learn .header.sticked.showed .mobile-nav-toggle.mobile-nav-show {
    color: var(--ts-blue);
}

/* ===========================================================================
   PART 5 — public homepage hero
   ---------------------------------------------------------------------------
   The template's own two-swiper hero (#caption-slider text, #teleseminar-slider
   background, both driven by main.js — see _Hero.cshtml). main.css keeps the
   layout mechanics (the caption panel's absolute position/width, the slider's
   height and radius); this is only the paint, restyled onto the reference: a
   solid Lavender caption panel over the left of the full-bleed photo, an
   ExtraBold Blue headline, and Yellow/Periwinkle pagination dots in place of
   Swiper's own black ones.
   =========================================================================== */

/* main.css's .container-fluid carries no gutter (its --bs-gutter-x is
   disabled — see the PART 2 note above), so the hero ran edge-to-edge with
   the viewport. Padding here insets the whole carousel from the page edges;
   the panels' own negative margins (main.css) still pull them flush against
   each other in the middle, so the seam is untouched.

   The right side still reached past this padding on its own: .hero-slider
   and .hero-slider-container carry a hand-tuned negative margin-left (to
   pull the photo flush against the caption panel) that was calibrated for
   the old zero-padding layout, and Swiper's own vendored CSS gives .swiper
   an explicit width:100% rather than width:auto — so, unlike a plain
   auto-width block, that width doesn't shrink to compensate for the
   negative margin, and the photo's right edge lands 50px+ past the new
   padding instead of at it. Reworking that margin chain risks reopening the
   gap it closes at the seam; clipping here is the reliable fix regardless of
   which nested element is the one actually overflowing. */
#hero .container-fluid {
    padding-inline: 1.5rem;
    overflow: hidden;
}

/* The narrower box (padding above) makes background-size:cover crop more of
   each slide to fill it. These photos carry their own baked-in headline and
   app-store badges toward the right, so anchoring the crop there keeps that
   text on-screen — the crop eats into the left (decorative pattern/photo
   background) instead, which loses less. */
.hero .hero-slider .swiper .swiper-slide {
    background-position: right center;
}

/* .hero-profession is the profession question _Hero.cshtml puts over the
   carousel for a visitor who has not chosen yet. It is listed beside the
   caption wherever the panel's paint is set, so the two cannot drift apart. */
.hero .hero-caption,
.hero .hero-profession {
    /* Base = the stacked layout: below 992px responsive.css unsets the
       caption's absolute positioning (via the higher-specificity
       .hero .hero-container .hero-caption) and it becomes an ordinary card
       above the slider — there it stays a solid tile with all four corners
       rounded. The overlay treatment lives in the min-width block below. */
    background: var(--ts-lavender);
    border-radius: var(--ts-radius-lg);
}

.hero .hero-caption {
    /* A bit more breathing room before the headline than the template's
       flat 50px, now that the panel isn't touching the page edge either. */
    padding-inline-start: 5.5rem;
}

/* Below 992px the question is an ordinary card too, first in the stack: over a
   photo that is 480px — 40vh under 768px — tall it would cover most of it. */
.hero .hero-profession {
    margin-bottom: 1rem;
    padding: 25px 2rem;
}

/* From 992px up the caption is absolutely positioned over the photo — an
   overlay, and styled as one. */
@media (min-width: 992px) {
    /* Bottom right of the picture, on the caption's own 50px inset so the two
       panels share a bottom line: clear of the caption on the left (45% at
       its widest, which leaves 419px of the 944px container at 992px) and of
       the pagination dots, which main.css puts at the top right (top: 50px;
       right: 50px). It floats free of every edge, so it keeps all four
       rounded corners. z-index 2 is the caption's, from main.css — over the
       slides. */
    .hero .hero-profession {
        position: absolute;
        bottom: 50px;
        right: 50px;
        z-index: 2;
        /* 34rem is what the select and the button need to share a line (12rem
           + gap + ~16rem of button); narrower than that the button wraps
           under the select. */
        width: min(34rem, 55% - 100px);
        margin-bottom: 0;
    }

    .hero .hero-caption,
    .hero .hero-profession {
        /* Translucent so the photo reads through the panel. Literal rgba of
           --ts-lavender (#E8EEFF), the alpha form the rest of this file
           uses. At 0.55, over the photos' dark blue the composite (~#8EAED8)
           still keeps the Blue .hero-title at ~6.2:1 — past AA's 4.5:1. */
        background: rgba(232, 238, 255, 0.55);
        /* The photos carry baked-in headlines and banners. A 12px blur took
           their letterforms away entirely, and the picture with them — the
           panels read as opaque. 2px only softens what is behind, so the
           photo stays recognisable through the panel. */
        -webkit-backdrop-filter: blur(2px);
        backdrop-filter: blur(2px);
    }

    .hero .hero-caption {
        /* The panel stays flush against the hero's left edge (left:0 from
           main.css), so the left corners are square where it meets that
           edge; only the free right edge rounds, matching the slider's own
           radius below. */
        border-top-left-radius: 0;
        border-bottom-left-radius: 0;
        border-top-right-radius: var(--ts-radius-lg);
        border-bottom-right-radius: var(--ts-radius-lg);
        /* Float inside the picture rather than run its full height — the
           template's own 50px inset. main.css already sets these, but under
           1200px responsive.css moves bottom to 195px to clear the
           check-room widget this hero doesn't render; restated here (loaded
           last, same specificity) so the even inset holds at every
           overlaying width. */
        top: 50px;
        bottom: 50px;
    }
}

.hero .hero-slider .swiper {
    border-top-left-radius: var(--ts-radius-lg);
    border-bottom-left-radius: var(--ts-radius-lg);
    border-top-right-radius: var(--ts-radius-lg);
    border-bottom-right-radius: var(--ts-radius-lg);
}

.hero .hero-caption .hero-title {
    font-size: clamp(2rem, 1.3rem + 3vw, 3.75rem);
    font-weight: var(--ts-weight-display);
    line-height: 1.05;
    letter-spacing: var(--ts-tracking-tight);
    color: var(--ts-blue);
}

/* Homepage only (#hero) — the expert and domain heroes share these classes.
   1.05 set a two-line headline almost touching itself; 1.2 opens it up. */
#hero .hero-caption .hero-title {
    line-height: 1.2;
}

/* The panel centres its content vertically (main.css: display:flex;
   align-items:center). A bottom margin on the content lifts the headline a
   little above the middle without giving up the centring. */
#hero .hero-caption .hero-caption-container {
    margin-bottom: 2rem;
}

.hero .hero-caption .hero-desc {
    color: var(--ts-color-ink-soft);
}

/* The slide's sentence, under the headline. Sized to run over about three
   lines in the panel on a desktop screen; it scales down with the viewport so
   a narrow screen does not get a wall of large text. */
#hero .hero-caption .hero-desc {
    font-size: clamp(1.25rem, 0.9rem + 1vw, 1.75rem);
    line-height: 1.4;
}

#hero .hero-caption .hero-desc > :last-child {
    margin-bottom: 0;
}

.hero .hero-slider .swiper-pagination-bullet {
    background: var(--ts-periwinkle);
    opacity: 1;
}

.hero .hero-slider .swiper-pagination-bullet-active {
    background: var(--ts-yellow);
}

/* --- the CV in the expert hero -------------------------------------------- */

/* SpeakerDetails/Index.cshtml now prints the expert's CV as one .hero-desc per
   paragraph (it used to be a single one holding the whole text, which collapsed
   the paragraph breaks and made a long CV an unreadable block). The template
   sizes .hero-desc as a standalone one-liner — `margin-bottom: 2rem` in main.css
   — so repeating it put 32px between every paragraph and hung another 32px off
   the end, which the caption's `align-items: center` then counted as content and
   pushed the whole CV visibly off-centre. Spacing the paragraphs against each
   other instead: nothing before the first, nothing after the last, and a gap in
   em so it tracks the font size rather than fighting it.

   Scoped to #SpeakerSpan, unique to this view: .hero-desc is the shared caption
   paragraph of the landing-page and training-domain heroes too, where it really
   is a single one-liner and the template's own margin is correct. */
#SpeakerSpan .hero-caption .hero-desc {
    margin-bottom: 0;
}

#SpeakerSpan .hero-caption .hero-desc + .hero-desc {
    margin-top: 0.9em;
}

/* The panel a long CV no longer breaks out of.

   main.css pins .hero-caption to the picture — position:absolute, top:50px,
   bottom:50px — so the panel is exactly as tall as the hero (100vh - 180px)
   whatever it holds, and its own `align-items: center` then splits the excess of
   a taller CV evenly above and below. Measured on Olivier Sustronck (speaker
   746, the longest CV the API serves) at 1492x802: 1184px of text in a 522px
   panel, so 331px printed over the site header — his name landed across the
   Teleseminar logo and the first paragraph scrolled off the top of the page,
   out of reach — and 331px over the trainings section below, text across text.

   So on this page the hero is sized by the CV instead of by the viewport: the
   two children of .hero-container share one grid cell, which makes the cell as
   tall as the taller of them while the caption still overlays the picture on the
   left, in the same place and at the same width. The 50px inset the template
   gives the panel becomes a margin rather than an offset, and the panel can then
   no longer overflow anything — short CV, the picture is the taller child and
   the panel floats inside it exactly as before (measured identical to the pixel
   at 992, 1200 and 1600px); long CV, the panel sets the height and the slide
   stretches to it. min-height keeps the slide's own calc(100vh - 180px) as the
   floor: sized in % alone it would contribute nothing to the row, and the hero
   would collapse onto the caption whenever the CV is the shorter of the two.

   From 992px only — below it responsive.css already unpins the caption and
   stacks it above the picture, where a CV of any length just makes the page
   longer. Scoped to the two pages whose caption is as long as the text the back
   office wrote — #CollectionSpan is a collection's hero, which holds the series'
   full description over its price row — because the landing page and the domain
   heroes share these classes with a one-line caption whose fixed height is
   correct. :is() takes the specificity of its heaviest argument, so both read as
   the single id these rules were written with. */
@media (min-width: 992px) {
    :is(#SpeakerSpan, #CollectionSpan).hero {
        height: auto;
    }

    /* minmax(0, …) and min-width:0, not the `1fr` and the `auto` minimum that
       come with it: a grid track never shrinks below the min-content width of
       what it holds, and Swiper writes an explicit pixel width on every slide.
       The track was sized from that width instead of from the viewport, Swiper
       then re-measured the wider container on the next init and wrote a wider
       slide again — 1492px of hero became a 14881px track, the caption 6145px
       wide with its paragraphs on single lines running off the page. This pair
       is what keeps the column at the width of .hero-container. */
    :is(#SpeakerSpan, #CollectionSpan) .hero-container {
        display: grid;
        grid-template-columns: minmax(0, 1fr);
    }

    :is(#SpeakerSpan, #CollectionSpan) .hero-caption,
    :is(#SpeakerSpan, #CollectionSpan) .hero-slider {
        grid-row: 1;
        grid-column: 1;
        min-width: 0;
    }

    /* position:relative, not static: it is what keeps main.css's z-index:2 on
       the caption, and so the panel over the picture rather than under it. The
       three offsets have to go with it — as a relative box, main.css's top:50px
       would shift the panel a second 50px down the page. */
    :is(#SpeakerSpan, #CollectionSpan) .hero-caption {
        position: relative;
        top: auto;
        bottom: auto;
        left: auto;
        justify-self: start;
        margin-block: 50px;
    }

    :is(#SpeakerSpan, #CollectionSpan) .hero-slider-container,
    :is(#SpeakerSpan, #CollectionSpan) .hero-slider .swiper,
    :is(#SpeakerSpan, #CollectionSpan) .hero-slider .swiper .swiper-wrapper {
        height: 100%;
    }

    :is(#SpeakerSpan, #CollectionSpan) .hero-slider .swiper .swiper-slide {
        height: 100%;
        min-height: calc(100vh - 180px);
    }
}

/* ===========================================================================
   PART 6 — dashboard
   ---------------------------------------------------------------------------
   The learning-environment reference's greeting header and its three panels:
   "Mijn nieuwe opleidingen" and "Ga verder waar je gebleven bent" in the left
   column, "Mijn overzicht" and its six tiles in the right one.

   Extends the .ts-panel/.ts-tile components PART 3 already defines (white
   cards, Periwinkle border) with the modifiers a Blue page background needs
   — --tinted, --accent — plus the value/label tile anatomy neither
   component had a use for yet, and the panel list the two left panels draw
   their rows into. Nothing here is dashboard-specific by selector, only by the
   class names used: phase 2 can reuse --tinted panels and --accent tiles
   anywhere on the Blue background.
   =========================================================================== */

.ts-dash-header {
    display: flex;
    flex-wrap: wrap;
    align-items: flex-end;
    justify-content: space-between;
    gap: var(--ts-space-md);
    margin-block-end: var(--ts-space-xl);
}

.ts-dash-greeting {
    margin: 0;
    font-size: clamp(1.75rem, 1.3rem + 2vw, 2.75rem);
    font-weight: var(--ts-weight-display);
    letter-spacing: var(--ts-tracking-tight);
    color: var(--ts-white);
}

.ts-dash-subtitle {
    margin: var(--ts-space-xs) 0 0;
    color: var(--ts-periwinkle);
}

/* The Vormingsjaar, which used to be decorative text and is now the one control
   in the header. Bigger than the subtitle beside it because it is the only thing
   on this page a visitor can change, and White rather than Periwinkle because a
   control has to read as one. */
.ts-dash-meta {
    text-align: end;
    color: var(--ts-white);
    font-size: clamp(1.1rem, 0.9rem + 0.7vw, 1.6rem);
}

    .ts-dash-meta p {
        margin: 0;
    }

/* The select carries the whole phrase - "Vormingsjaar 2026 v" - and its <label>
   is hidden, so this is a one-child flex row that keeps the control on the
   header's trailing edge and wraps the <noscript> button under it. */
.ts-year-filter {
    display: flex;
    flex-wrap: wrap;
    align-items: baseline;
    justify-content: flex-end;
    gap: var(--ts-space-2xs);
}

.ts-year-filter__control {
    position: relative;
    display: inline-flex;
    align-items: center;
}

/* appearance: none strips the platform control so the year reads as part of the
   sentence, but it stays a real <select> - keyboard operable, with its own focus
   ring below. Padding on the inline-end side leaves room for the chevron. */
.ts-year-filter__select {
    appearance: none;
    border: 0;
    border-radius: var(--ts-radius);
    padding: 0 1.6em 0 var(--ts-space-2xs);
    background: transparent;
    color: inherit;
    font: inherit;
    line-height: inherit;
    cursor: pointer;
}

    /* The open list is drawn by the platform against its own background, so the
       options need an explicit pair rather than the White-on-Blue they inherit
       from the header. */
    .ts-year-filter__select option {
        background: var(--ts-surface);
        color: var(--ts-on-surface);
    }

    .ts-year-filter__select:focus-visible {
        outline: var(--ts-focus-ring);
        outline-offset: var(--ts-focus-offset);
    }

/* Decoration only: the select underneath owns the clicks, so the chevron must
   not swallow the one aimed at its own centre. */
.ts-year-filter__chevron {
    position: absolute;
    inset-inline-end: var(--ts-space-2xs);
    pointer-events: none;
    font-size: 0.6em;
}

.ts-dash-grid {
    display: grid;
    gap: var(--ts-space-lg);
    grid-template-columns: 1fr;
    align-items: start;
}

@media (min-width: 75rem) {
    .ts-dash-grid {
        grid-template-columns: minmax(0, 3fr) minmax(0, 2fr);
    }
}

/* A trainee with no licence at all gets no left column, so the grid holds one
   child. Without this it would still be laid out in the 3fr track and leave the
   2fr one as a dead gutter, two fifths of the page wide, beside a panel that
   had every reason to take the space. Specificity beats the rule above, so it
   applies at every width without repeating the media query. */
.ts-dash-grid:has(> :only-child) {
    grid-template-columns: minmax(0, 1fr);
}

/* The two left panels stack. align-content keeps the column from stretching
   its panels to the height of "Mijn overzicht" beside it. */
.ts-dash-column {
    display: grid;
    gap: var(--ts-space-lg);
    align-content: start;
    min-inline-size: 0;
}

/* --------------------------------------------------------- panel, tinted */

.ts-panel--tinted {
    background: var(--ts-bg-alt);
    color: var(--ts-white);
    box-shadow: none;
}

.ts-panel__header {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    justify-content: space-between;
    gap: var(--ts-space-sm);
    margin-block-end: var(--ts-space-md);
}

.ts-panel__title {
    margin: 0;
    font-size: 1.1rem;
    font-weight: var(--ts-weight-heading);
}

.ts-panel__link {
    display: inline-flex;
    align-items: center;
    gap: var(--ts-space-2xs);
    color: var(--ts-yellow);
    font-size: 0.9rem;
    font-weight: var(--ts-weight-heading);
    white-space: nowrap;
}

    .ts-panel__link:hover,
    .ts-panel__link:focus-visible {
        color: var(--ts-yellow-dark);
    }

/* -------------------------------------------------- progress bar and button

   What is left of the single "continue watching" card the panel used to hold:
   the bar and the button, now drawn once per row by every row of both panels.
   The card shell itself became .ts-panel-list__item below; these two kept their
   names because the row partial reuses them unchanged. */

.ts-continue-card__bar {
    inline-size: 100%;
    max-inline-size: 20rem;
    block-size: 0.4rem;
    border-radius: 999px;
    background: var(--ts-border);
    overflow: hidden;
}

.ts-continue-card__bar-fill {
    display: block;
    block-size: 100%;
    border-radius: inherit;
    background: var(--ts-yellow);
}

.ts-continue-card__cta {
    white-space: nowrap;
}

/* The arrow sits a word-space away from the label on its own, which reads as
   cramped at this button's size. */
.ts-continue-card__cta > span + i {
    margin-inline-start: 0.35rem;
}

/* ------------------------------------------------------------- panel list

   The rows of "Mijn nieuwe opleidingen" and "Ga verder waar je gebleven bent".
   One <li> per training, drawn by Views/Dashboard/_DashboardTrainingRow.cshtml;
   the card shape is the continue-card's, which is why the progress bar and the
   button keep their .ts-continue-card__* classes rather than growing a second
   set of identical rules. */

.ts-panel-list {
    display: grid;
    gap: var(--ts-space-sm);
    margin: 0;
    padding: 0;
    list-style: none;
}

.ts-panel-list__item {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: var(--ts-space-md);
    background: var(--ts-surface);
    color: var(--ts-on-surface);
    border-radius: var(--ts-radius-lg);
    padding: var(--ts-space-lg);
    box-shadow: var(--ts-shadow-md);
}

/* Rows four and up. The server renders every row and hides the surplus here,
   which is what lets the header control expand the panel without a second
   request - and what leaves the whole list in the markup for a visitor whose
   script never runs, one navigation away on the same control. */
.ts-panel-list__item--extra {
    display: none;
}

.ts-panel-list--expanded .ts-panel-list__item--extra {
    display: flex;
}

.ts-panel-list__body {
    display: flex;
    flex-direction: column;
    gap: var(--ts-space-2xs);
    flex: 1 1 16rem;
    min-inline-size: 0;
}

.ts-panel-list__title {
    margin: 0;
    font-weight: var(--ts-weight-heading);
    color: var(--ts-on-surface);
}

.ts-panel-list__meta {
    display: flex;
    flex-wrap: wrap;
    align-items: baseline;
    gap: var(--ts-space-2xs) var(--ts-space-sm);
    margin: 0;
    font-size: 0.85rem;
    color: var(--ts-color-ink-soft);
}

/* The separator between speaker and duration is drawn, not written, so no
   screen reader reads it out as a word - and it is absent by construction when
   the training credits no speaker, because then there is no sibling to follow. */
.ts-panel-list__speaker + .ts-panel-list__duration::before {
    content: "\00B7";
    margin-inline-end: var(--ts-space-xs);
}

/* --------------------------------------------------- expand/collapse control

   Rendered as an <a> to the matching catalogue tab and upgraded in place by
   teleseminar.js into a button. Both states are the same element, so the rules
   are shared; the global *:focus-visible ring at the top of this file is what
   makes the keyboard state visible either way. The expanded state is carried by
   the label text ("Toon minder"), never by colour alone - the chevron only
   reinforces it. The rotation is scoped to the toggle so the neighbouring
   "Bekijk details" link keeps its arrow still. */

.ts-panel__link[data-panel-toggle] {
    cursor: pointer;
}

/* `chevron-right` closed, `chevron-down` open — the same glyph turned a quarter
   that the account page's order panels use, so the two collapsibles in the app
   open and close with one gesture rather than two different ones. */
.ts-panel__link[data-panel-toggle] i {
    transition: transform var(--ts-transition);
}

.ts-panel__link--expanded i {
    transform: rotate(90deg);
}

@media (prefers-reduced-motion: reduce) {
    .ts-panel__link[data-panel-toggle] i {
        transition: none;
    }
}


/* ---------------------------------------------------------- archive panel

   "Mijn gearchiveerde opleidingen": the third left panel, folded on a native
   <details> rather than on the top-3 toggle above - that control reveals the
   tail of a list already partly on screen, this one opens a block that is
   closed altogether, and <details> gives keyboard operation, the expanded
   state and the no-script case without a line of script.

   Secondary content on the same Blue page, so a step quieter than the two
   tinted panels: no fill, a hairline in --ts-border instead. The title stays
   --ts-text (White on Blue) and the count and intro --ts-text-muted
   (Periwinkle), both well past AA on --ts-bg. The state is carried by the open
   block itself and by the chevron's quarter turn - the same gesture as the
   panel toggle and the account page's order panels - never by colour. */

.ts-panel--quiet {
    background: transparent;
    color: var(--ts-text);
    border: 1px solid var(--ts-border);
    box-shadow: none;
}

/* The summary is the panel header: title and count on the left, chevron on
   the right, the whole strip clickable. list-style: none and the WebKit
   marker rule together hide the platform triangle the chevron replaces. The
   keyboard ring comes from the summary:focus-visible rule at the top of this
   file, like every other control. */
.ts-archive__summary {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--ts-space-sm);
    cursor: pointer;
    list-style: none;
}

    .ts-archive__summary::-webkit-details-marker {
        display: none;
    }

/* Opened, the header needs the same room below it the other panels' headers
   keep; closed, the panel ends with it. */
.ts-archive[open] > .ts-archive__summary {
    margin-block-end: var(--ts-space-md);
}

.ts-archive__count {
    font-weight: var(--ts-weight-body);
    color: var(--ts-text-muted);
}

.ts-archive__chevron {
    flex: none;
    font-size: 0.8rem;
    transition: transform var(--ts-transition);
}

.ts-archive[open] .ts-archive__chevron {
    transform: rotate(90deg);
}

.ts-archive__intro {
    margin: 0 0 var(--ts-space-md);
    font-size: 0.9rem;
    color: var(--ts-text-muted);
}


@media (prefers-reduced-motion: reduce) {
    .ts-archive__chevron {
        transition: none;
    }
}

/* -------------------------------------------------------- progress ring */

/* Three knobs, each defaulting to the value the dashboard already had, so the
   dashboard's ring computes byte-for-byte what it computed before this page
   existed. A second caller wanted a smaller ring in a different colour; giving
   it its own copy of the rule would have been a second definition of the same
   control, and overwriting the fill would have repainted the dashboard.

   The fill's default sits in the var() fallback below rather than in a
   declaration here, which is how the track and the hole already work. A custom
   property declared on this element wins over the same property inherited from
   an ancestor, so a declaration here would silently shadow the one the catalogue
   card sets on its panel to close a finished ring in Blue. The two callers that
   re-colour or re-size the ring per instance — .ts-progress-ring--sm and
   .tour-indicator — declare on the ring itself and keep winning either way. */
.ts-progress-ring {
    --ts-progress-ring-size: 4rem;
    --ts-progress-ring-thickness: 0.4rem;
    position: relative;
    display: grid;
    place-items: center;
    inline-size: var(--ts-progress-ring-size);
    block-size: var(--ts-progress-ring-size);
    border-radius: 50%;
    /* Both halves of the gradient are instance decisions. The track's token flips with the mode and
       the catalogue card is a White panel in both; the fill turns Blue on a finished training, which
       is the one place the ring stops meaning "in progress". Unset, they are the Yellow-on-border
       ring the dashboard has always drawn. */
    background: conic-gradient(
        var(--ts-progress-ring-fill, var(--ts-yellow)) calc(var(--ts-progress-ring-value, 0) * 1%),
        var(--ts-progress-ring-track, var(--ts-border)) 0);
    flex-shrink: 0;
}

/* The hole. --ts-progress-ring-hole is what the ring is cut out of when an instance sits on
   something other than the page's own surface — the catalogue card's tinted panel is the one such
   instance. It is painted as a layer *over* --ts-surface rather than instead of it, because in
   learn mode that value is a translucent tint whose whole point is to composite onto White; unset,
   the layer is transparent and the hole is --ts-surface exactly as before. */
.ts-progress-ring::before {
    content: "";
    position: absolute;
    inset: var(--ts-progress-ring-thickness);
    border-radius: 50%;
    background-color: var(--ts-surface);
    background-image: linear-gradient(
        var(--ts-progress-ring-hole, transparent),
        var(--ts-progress-ring-hole, transparent));
}

.ts-progress-ring__label {
    position: relative;
    font-size: 0.95rem;
    font-weight: var(--ts-weight-heading);
    color: var(--ts-on-surface);
}

/* The employees page draws one ring per training in a list, not one hero figure
   in a panel: smaller, thinner, and Blue rather than Yellow, which is what the
   design asks for and what keeps a column of them from reading as warnings. The
   label shrinks with the ring so "100%" still fits inside the hole. */
.ts-progress-ring--sm {
    --ts-progress-ring-size: 2.75rem;
    --ts-progress-ring-fill: var(--ts-blue);
    --ts-progress-ring-thickness: 0.25rem;
}

.ts-progress-ring--sm .ts-progress-ring__label {
    font-size: 0.7rem;
    color: var(--ts-blue);
}

/* ====================================================== mijn medewerkers

   The employees page: the list on the left and the "gevolgde opleidingen"
   rows on the right. Both are lists of two-line entries with something on
   each side, which is one flex row and a gap; neither needed a grid.

   Semantic tokens only, no hex and no !important. The one colour that did
   not exist is --ts-ink-muted, added to brand.css: the second line of an
   entry has to be quieter than the first and still readable, which is not
   what --ts-border or --ts-periwinkle are for.                             */

/* --------------------------------------------------------- the company */

/* Rendered instead of the picker when there is exactly one company. It is
   a fact about the page, not a control, so it is styled as a heading and
   not as a disabled input. */
.ts-employee-company {
    color: var(--ts-color-ink);
    font-weight: var(--ts-weight-heading);
}

/* ------------------------------------------------------ employee list */

.ts-employee-list {
    list-style: none;
    padding: 0;
    margin: 0;
}

.ts-employee-list__link {
    display: flex;
    align-items: center;
    gap: var(--ts-space-sm);
    padding: var(--ts-space-sm);
    border-radius: var(--ts-radius);
    color: var(--ts-color-ink);
    text-decoration: none;

    /* 44px, which is the minimum a finger can reliably hit. These rows are a
       single column of thin entries on a phone, which is exactly where that
       matters most. */
    min-block-size: 2.75rem;
}

.ts-employee-list__link:hover,
.ts-employee-list__link:focus-visible {
    background: var(--ts-color-primary-soft);
    color: var(--ts-color-ink);
}

/* The theme's focus ring, because the row is the click target and a link with
   no underline and no box would otherwise be focusable with nothing to see. */
.ts-employee-list__link:focus-visible {
    outline: var(--ts-focus-ring);
    outline-offset: var(--ts-focus-offset);
}

/* The selected row. Keyed on aria-current and not on a class of its own, so the
   highlight and the fact a screen reader announces cannot come apart: there is
   one attribute, and it is the one that carries the meaning. */
.ts-employee-list__link[aria-current="true"] {
    background: var(--ts-color-primary-soft);
    font-weight: var(--ts-weight-heading);
}

.ts-employee-list__avatar {
    display: grid;
    place-items: center;
    flex: none;
    inline-size: 2.25rem;
    block-size: 2.25rem;
    border-radius: 50%;
    background: var(--ts-color-primary-soft);
    color: var(--ts-color-ink);
}

.ts-employee-list__link[aria-current="true"] .ts-employee-list__avatar {
    background: var(--ts-white);
}

/* min-inline-size: 0 so a long address can shrink and be clipped rather than
   pushing the chevron out of the card. A flex item defaults to its content's
   width, which is what makes overflow the default here and not the exception. */
.ts-employee-list__body {
    display: flex;
    flex-direction: column;
    min-inline-size: 0;
    flex: 1 1 auto;
}

.ts-employee-list__name {
    font-weight: var(--ts-weight-heading);
    line-height: 1.3;
}

.ts-employee-list__mail {
    color: var(--ts-ink-muted);
    font-size: 0.85rem;
    line-height: 1.3;

    /* An address is one long unbroken token; without this it decides the width
       of the whole column. */
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.ts-employee-list__chevron {
    flex: none;
    color: var(--ts-color-ink);
    font-size: 0.85rem;
}

/* ----------------------------------------------------- remove control */

/* Sits under the form and reads as the quiet end of it, not as a second
   primary action. The reveal itself is the first of the two steps the
   removal asks for; funnel-details carries its own summary styling. */
.ts-employee-remove {
    margin-block-start: var(--ts-space-4);
}

/* ------------------------------------------------- gevolgde opleidingen */

.ts-followed {
    list-style: none;
    padding: 0;
    margin: 0;
}

.ts-followed__item {
    border: 1px solid var(--ts-color-border);
    border-radius: var(--ts-radius);
    background: var(--ts-color-surface);
    overflow: hidden;
}

.ts-followed__item + .ts-followed__item {
    margin-block-start: var(--ts-space-sm);
}

.ts-followed__head {
    display: flex;
    align-items: center;
    gap: var(--ts-space-sm);
    padding: var(--ts-space-sm) var(--ts-space-md);
}

.ts-followed__body {
    min-inline-size: 0;
    flex: 1 1 auto;
}

.ts-followed__title {
    margin: 0;
    color: var(--ts-color-ink);
    font-weight: var(--ts-weight-heading);
    line-height: 1.3;
}

.ts-followed__meta {
    margin: 0;
    color: var(--ts-ink-muted);
    font-size: 0.85rem;
    line-height: 1.4;
}

.ts-followed__dot {
    margin-inline: var(--ts-space-2xs);
}

/* The expand control. Square and 44px like the list rows, for the same
   reason: on a phone it is a lone icon in a thin row. */
.ts-followed__toggle {
    display: grid;
    place-items: center;
    flex: none;
    inline-size: 2.75rem;
    block-size: 2.75rem;
    border-radius: var(--ts-radius);
    color: var(--ts-color-ink);
    text-decoration: none;
}

.ts-followed__toggle:hover {
    background: var(--ts-color-primary-soft);
    color: var(--ts-color-ink);
}

.ts-followed__toggle:focus-visible {
    outline: var(--ts-focus-ring);
    outline-offset: var(--ts-focus-offset);
}

/* The chevron turns over when the row opens, driven by the same attribute the
   control announces its state with rather than by a class the script would have
   to keep in step. */
.ts-followed__toggle i {
    transition: transform var(--ts-transition);
}

.ts-followed__toggle[aria-expanded="true"] i {
    transform: rotate(180deg);
}

@media (prefers-reduced-motion: reduce) {
    .ts-followed__toggle i {
        transition: none;
    }
}

/* Where the fetched table of contents lands. The tint separates it from the row
   that opened it without a second border between the two. */
.ts-followed__toc {
    padding: 0 var(--ts-space-md) var(--ts-space-md);
    border-block-start: 1px solid var(--ts-color-border);
    background: var(--ts-color-primary-soft);
}

/* The numbered gutter is one twelfth of its row, and one twelfth of a 390px
   screen is 32px — which a two-digit module number in a 1rem font does not fit
   into. Below the phone breakpoint the number takes a fixed column wide enough
   for two digits and the accordion takes the rest, so nothing overflows and
   nothing is clipped. */
@media (max-width: 36em) {
    .ts-followed__toc .ts-programme .col-1 {
        flex: 0 0 2.5rem;
        max-inline-size: 2.5rem;
    }

    .ts-followed__toc .ts-programme .col-11 {
        flex: 1 1 auto;
        max-inline-size: calc(100% - 2.5rem);
    }

    .ts-followed__toc .ts-programme .accordion-button {
        padding-inline: var(--ts-space-sm);
    }
}

/* ------------------------------------------------------------------- tiles

   The three figures that used to sit in a .ts-dash-stats row inside the left
   panel — hours available, started, completed — are tiles in this grid now, so
   that row and its .ts-stat--tinted variant are gone with the markup that used
   them. Nothing else on the site drew either. Two consequences worth knowing:
   the tiles are whole-tile links, so the .stretched-link overlay (and the
   positioned ancestor .ts-stat--tinted needed to contain it) is no longer in
   play at all; and .ts-stat itself, in PART 3, is now unused by any view. */

/* Six tiles in three rows of two: hours available and hours attested, then the
   four counts. Two columns at every width the panel is wide enough for, and one
   on a phone - at 390 a label as long as "Aantal uur beschikbaar in mijn pakket"
   needs the whole width to stay on two lines.

   Two columns hold even when the left column is absent and this panel has the
   page to itself. That used to become four, which was right for seven small
   tiles and is wrong for six whose numbers are now the largest thing on the
   page: four of these across 1920px puts a two-digit figure in a letterbox. */
.ts-dash-tiles {
    display: grid;
    gap: var(--ts-space-sm);
    grid-template-columns: 1fr;
}

@media (min-width: 30rem) {
    .ts-dash-tiles {
        grid-template-columns: repeat(2, 1fr);
    }
}

/* The number is the tile. It scales with the viewport rather than sitting at one
   size, and the ceiling is where four digits and a decimal - 1234, 12,5 - still
   fit on one line inside half a phone-width panel. Tabular figures so the six
   values line up in their columns instead of drifting with digit widths. */
.ts-tile__value {
    display: block;
    font-size: clamp(2.5rem, 1.7rem + 2.6vw, 4rem);
    font-weight: var(--ts-weight-display);
    color: var(--ts-on-surface);
    line-height: 1.05;
    letter-spacing: var(--ts-tracking-tight);
    font-variant-numeric: tabular-nums;
}

.ts-tile__label {
    display: block;
    margin-block-start: var(--ts-space-2xs);
    font-size: 0.85rem;
    color: var(--ts-color-ink-soft);
}

/* .ts-tile is shared with two other card patterns (the media-caption tile at
   the top of this file and the product-listing tile in PART 3), both of
   which set their own min-height/block-size for their own content — a media
   thumbnail, a catalogue card. Neither applies here: this is a compact
   icon/value/label stat, and inheriting either left the dashboard tiles
   stretched to a tall, mostly-empty card. Reset back to fitting the content. */
.ts-dash-tiles .ts-tile {
    display: flex;
    flex-direction: column;
    align-items: flex-start;
    min-block-size: 0;
    block-size: auto;
    padding: var(--ts-space-lg);
    /* Half of the six tiles are links and took this from the <a> ink; the two
       hour figures and the trainings bought are <div>s and would otherwise
       inherit the Blue page's White body text straight onto a White card.
       Stated once for all six, on the card, so a tile never depends on its own
       element name to be legible. */
    color: var(--ts-on-surface);
    text-decoration: none;
    transition: box-shadow var(--ts-transition), transform var(--ts-transition);
}

    /* Scoped to a.ts-tile through the rest of this block: the two hour figures
       and the trainings bought are <div>s, because a balance, a yearly total and
       a lifetime count are not lists you can navigate to, and lifting one under
       the pointer would promise a click that never lands. */
    .ts-dash-tiles a.ts-tile:hover,
    .ts-dash-tiles a.ts-tile:focus-visible {
        background: var(--ts-lavender);
        box-shadow: var(--ts-shadow-md);
        transform: translateY(-2px);
    }

    /* The lift and the tint are both decoration; the ring is what says "this is
       where you are" to someone arriving by keyboard, so it is stated on its
       own and never carried by colour alone. */
    .ts-dash-tiles a.ts-tile:focus-visible {
        outline: var(--ts-focus-ring);
        outline-offset: var(--ts-focus-offset);
    }

/* The hours-available tile is Yellow permanently - it is the figure the page is
   for. It is not a link, so it has no hover state, and nothing dims it while
   another tile is hovered: the rule that used to do that existed because the
   Yellow tile was also a link and two lit tiles read as two selections. */
.ts-tile--accent {
    background: var(--ts-yellow);
    border-color: transparent;
}

    /* Blue on Yellow, stated for both lines: the label would otherwise keep the
       muted ink it has on a White card, which does not hold its contrast here. */
    .ts-tile--accent .ts-tile__value,
    .ts-tile--accent .ts-tile__label {
        color: var(--ts-blue);
    }

/* ----------------------------------------------- "Mijn attesten {jaar}"

   The attest tile and the panel it opens, directly below "Mijn overzicht".

   The cards are .ts-panel-list__item, the very shell the training panels draw -
   same White surface, same radius, same shadow - so only what is new to this
   list lives here: the tile's affordance line, the panel's subtitle and close
   button, the download button and the "in behandeling" badge.

   Green is read off --ts-green and not --ts-feedback-success: these cards sit
   on White, and in learn mode that token resolves to --ts-green-on-blue, a
   light green chosen to stay legible *on Blue* and far too pale here. The pair
   below is the one the public mode builds its own success tokens from. */

/* The tile is a link again as soon as it has a list to open, so the Lavender
   hover and the focus ring of a.ts-tile apply to it unchanged. */
.ts-tile__affordance {
    display: inline-flex;
    align-items: center;
    gap: var(--ts-space-2xs);
    margin-block-start: var(--ts-space-xs);
    font-size: 0.85rem;
    font-weight: var(--ts-weight-heading);
    color: var(--ts-on-surface);
}

    .ts-tile__affordance i {
        font-size: 0.7em;
        transition: transform var(--ts-transition);
    }

/* Open: the Yellow edge says which tile this panel belongs to. The state is
   carried by aria-expanded and by the chevron as well, never by the colour
   alone - and the chevron turns only once the script has upgraded the link,
   which is also when the attribute starts telling the truth. */
.ts-dash-tiles a.ts-tile[aria-expanded="true"] {
    border-color: var(--ts-accent);
}

    .ts-dash-tiles a.ts-tile[aria-expanded="true"] .ts-tile__affordance i {
        transform: rotate(180deg);
    }

/* .ts-panel sets no display of its own, so the UA's [hidden] rule still folds
   the panel; stated here so a later display declaration cannot silently unfold
   it. */
.ts-certificates[hidden] {
    display: none;
}

/* The header now holds two lines beside the close button, so it aligns to the
   top rather than to the middle of them. */
.ts-certificates__header {
    align-items: flex-start;
    flex-wrap: nowrap;
}

.ts-certificates__title {
    font-size: 1.25rem;
}

.ts-panel__subtitle {
    margin: var(--ts-space-2xs) 0 0;
    font-size: 0.85rem;
    color: var(--ts-white);
}

/* Square, quiet, and White like the heading it sits beside: on the tinted panel
   the x is the only control, so it needs no colour to be found. */
.ts-panel__close {
    display: inline-flex;
    flex: none;
    align-items: center;
    justify-content: center;
    inline-size: 2rem;
    block-size: 2rem;
    border-radius: var(--ts-radius);
    color: var(--ts-white);
    text-decoration: none;
    line-height: 1;
    cursor: pointer;
    transition: background var(--ts-transition);
}

    .ts-panel__close:hover,
    .ts-panel__close:focus-visible {
        background: color-mix(in srgb, var(--ts-white) 14%, transparent);
        color: var(--ts-white);
    }

    .ts-panel__close:focus-visible {
        outline: var(--ts-focus-ring);
        outline-offset: var(--ts-focus-offset);
    }

/* nowrap and a top-aligned row: at 390 the second line wraps under the title
   while the download button stays where it is, in the top corner. */
.ts-certificate {
    flex-wrap: nowrap;
    align-items: flex-start;
    gap: var(--ts-space-sm);
}

    .ts-certificate .ts-panel-list__body {
        flex: 1 1 auto;
    }

    .ts-certificate .ts-panel-list__title {
        font-size: 1.05rem;
    }

/* The hours are the figure of the row, so they are the one thing on the second
   line set in Bold. */
.ts-certificate__hours {
    font-weight: var(--ts-weight-heading);
}

/* Drawn and not written, like the separator between speaker and duration above:
   no screen reader reads it out, and it is absent by construction on a row that
   has nothing before it. */
.ts-certificate__hours + .ts-certificate__date::before {
    content: "\00B7";
    margin-inline-end: var(--ts-space-xs);
}

.ts-certificate__download {
    display: inline-flex;
    flex: none;
    align-items: center;
    justify-content: center;
    inline-size: 2.75rem;
    block-size: 2.75rem;
    border-radius: var(--ts-radius);
    /* 10 % and 18 %: the icon reads 4.55:1 on the resting tint and 4.07:1 on the
       hover one, so it clears AA for text on the first and AA for a graphical
       object on both - an icon button carries its meaning in the glyph. */
    background: color-mix(in srgb, var(--ts-green) 10%, var(--ts-white));
    color: var(--ts-green);
    font-size: 1.1rem;
    text-decoration: none;
    transition: background var(--ts-transition);
}

    .ts-certificate__download:hover,
    .ts-certificate__download:focus-visible {
        background: color-mix(in srgb, var(--ts-green) 18%, var(--ts-white));
        color: var(--ts-green);
    }

    .ts-certificate__download:focus-visible {
        outline: var(--ts-focus-ring);
        outline-offset: var(--ts-focus-offset);
    }

/* Still being handled: a neutral badge, never a greyed-out copy of the button -
   there is nothing to press, so there is nothing to disable. The wording carries
   the state; the Lavender only separates it from the card. */
.ts-certificate__state {
    flex: 0 1 auto;
    align-self: center;
    border-radius: var(--ts-radius);
    padding: var(--ts-space-2xs) var(--ts-space-xs);
    background: var(--ts-lavender);
    color: var(--ts-blue);
    font-size: 0.8rem;
    font-weight: var(--ts-weight-heading);
    text-align: center;
}

/* ===========================================================================
   PART 7 — footer
   ---------------------------------------------------------------------------
   `.scroll-top` (main.css) is `position: fixed; right: 25px; bottom: 25px` at
   every breakpoint — pinned to the viewport corner regardless of scroll
   position. On phones the footer's three widgets stack to full width, so the
   quick-links column ends up right where that corner sits once the visitor
   scrolls to the bottom, and the solid Yellow circle covers its text. Padding
   at the bottom of the footer keeps that corner empty page background
   instead, at the width where the columns actually stack. */
@media (max-width: 991.98px) {
    #footer {
        padding-block-end: 6rem;
    }
}

/* The footer rebuilt on the KVABB model (2026-09): logo and contact block in
   the first column, three link columns, a two-sided bottom bar. main.css still
   owns the ground (--ht-color-dark, which is Blue), the muted text, the White
   links, the widget titles and the 15 %-White rules; what follows is only what
   the template has no rule for. Token-driven throughout, so both site modes
   draw the same footer: the ground is the footer's own and not the body's, which
   is what keeps it Blue on the Blue body of the learning environment too. */

/* A little less air under the logo than the theme's 25 px: the address is the
   logo's caption, not a new block. */
.footer .footer-contact .footer-logo {
    margin-bottom: var(--ts-space-lg);
}

/* <address> is italic by default; these are lines of a contact card, set like
   the rest of the footer. The last line keeps no margin: the space to the call
   to action below is the button's own, measured once. */
.footer-contact__address {
    margin-bottom: 0;
    font-style: normal;
}

    .footer-contact__address > p:last-child {
        margin-bottom: 0;
    }

/* The fixed-width label ("Telefoon:", "E-mail:", "Btw:") that lines the values
   up in one column, as on the mockup. rem rather than ch so the column is the
   same in both languages: "Téléphone :" is the longest label and sets it. */
.footer-contact__label {
    display: inline-block;
    min-width: 6rem;
}

/* The column's call to action: the learning environment's "← Terug" chip
   (.btn-ht-secondary — periwinkle fill, Blue label, no visible border) with the
   arrow after the label. Roughly 36 px above it, so it reads as the close of the
   column and not as a fourth contact line. `.footer a` paints every link White,
   which would put White on periwinkle here; the chip keeps its Blue ink in every
   state, which is also the button's existing hover: main.css changes nothing on
   hover but the k-hover cursor. The padding is the Back button's own, not the 2
   rem of the theme's .btn: this is a chip in a column, not a hero button. */
.footer .footer-contact__cta {
    display: inline-flex;
    align-items: center;
    gap: var(--ts-space-xs);
    margin-top: calc(var(--ts-space-lg) + var(--ts-space-sm));
    padding: var(--ts-space-sm) var(--ts-space-lg);
    line-height: 1.2;
}

    .footer .footer-contact__cta,
    .footer .footer-contact__cta:hover,
    .footer .footer-contact__cta:focus,
    .footer .footer-contact__cta:active {
        color: var(--ts-blue);
    }

    .footer .footer-contact__cta > i {
        color: inherit;
    }

/* Yellow on rollover, the same answer the nav links (responsive.css's
   `.navbar ul li a:hover`), the wordmark and the top bar's links give. Without
   this the column links and the phone/e-mail links of the address fall through
   to main.css's `.footer a:hover`, which paints them --ht-color-primary: Blue
   on the footer's Blue ground, so the link goes out the moment the pointer
   reaches it. The same selector as the theme's rule, and this file loads after
   main.css, so the cascade order decides and no !important is needed. The
   contact chip above is not caught: its own three-class rule keeps it Blue on
   periwinkle in every state. Focus is included so a keyboard sees what a
   pointer sees. */
.footer a:hover,
.footer a:focus {
    color: var(--ts-yellow);
}

/* Copyright left, credit right, on one line while it fits and as two lines on a
   phone; the credit is plain muted text, which is what the theme's .footer
   colour already is. */
.footer .footer-bottom {
    display: flex;
    justify-content: space-between;
    flex-wrap: wrap;
    gap: 0 var(--ts-space-lg);
}

/* The credit ends in the corner the fixed .scroll-top button occupies once the
   visitor is at the bottom (25 px in from the right, 50 px wide — the last 25 px
   of the credit sat under it). Above 992 px the footer has no bottom padding to
   lift the text clear, as it has below (part 7 above), so the credit stops short
   of the corner instead. */
@media (min-width: 992px) {
    .footer .footer-credit {
        margin-inline-end: var(--ts-space-xl);
    }
}

/* ===========================================================================
   PART 8 — password requirements checklist
   ---------------------------------------------------------------------------
   The live list under every "choose a password" field, rendered by
   Views/Shared/_PasswordRequirements.cshtml and driven by
   wwwroot/js/password-requirements.js.

   Colours come from the brand's feedback tokens rather than from literals, so
   the list reads correctly in the inverted (on-blue) mode too — where
   --ts-feedback-success/error resolve to the on-blue variants that keep their
   contrast against the dark background. `green` and `red` would not.
   =========================================================================== */

.password-requirements {
    list-style: none;
    margin: calc(var(--ts-space-xs) * -1) 0 var(--ts-space-sm);
    padding: 0;
    font-size: 0.85rem;
    line-height: 1.6;
}

    .password-requirements li {
        display: flex;
        align-items: baseline;
        gap: var(--ts-space-xs);
        color: var(--ts-color-ink-soft);
    }

    /* Fixed-width icon column. Without it the text shifts sideways the moment a
       cross becomes a tick, because the two glyphs are not the same width — the
       jump the reference implementation has and that made the list feel broken
       while it was in fact working. .fa-fw does this in Font Awesome; the width
       is repeated here so the layout does not depend on that class surviving a
       markup edit. */
    .password-requirements li > i {
        flex: 0 0 1.25em;
        width: 1.25em;
        text-align: center;
    }

    .password-requirements li.is-unmet {
        color: var(--ts-color-danger);
    }

    .password-requirements li.is-met {
        color: var(--ts-color-success);
    }

@media (prefers-reduced-motion: no-preference) {
    .password-requirements li {
        transition: color 120ms ease-out;
    }
}

/* ===========================================================================
   PART 9 — My account: the preferences panel
   ---------------------------------------------------------------------------
   Views/MyAccount/Index.cshtml, the block that carries the training language
   and the target group. Every rule here closes a gap the vendored theme or
   Bootstrap leaves — no new component and no new colour: the radios, the
   select and the labels are the same `.form-check` / `.form-select` /
   `.form-label` vocabulary the funnel block above already paints. The one new
   class, `.account-language-options`, is a wrapper that lays the two radios out
   on a line and is used by this view alone. `.account-section-title` sits here too:
   it is the page's own, not the panel's — every block's heading is out on the blue
   now, and the ones with a card above them take it.
   =========================================================================== */

/* Everything in the panel inherits `font-size: 1.25rem` from
   `.reservation-box .reservation-detail` in assets/css/main.css, which is what
   made the radio labels and the select read at 20 px next to a 0.85rem
   `.form-label` — a whole size out of step with the design, where the option
   text is only a little larger than the label naming it. The reset is hung on
   this panel's id and not on `.reservation-detail`, because the four cards
   above it — personal details, password, company, invoices — are meant to read
   at that 1.25rem: they are a summary of what is on record, not a form of small
   controls, and resetting the shared class would restyle all of them. The id
   also outweighs main.css's two classes on its own, so nothing here has to
   shout `!important`. */
#account-preferences {
    font-size: 1rem;
}

/* The gap over a section title that has a card above it — on My account that is
   every title but the two opening a column. Bootstrap's spacing stops at `mt-5` —
   3rem, which on top of a card's own `mb-4` left a title an even distance from the
   card before it and the block it names, so it read as belonging to neither. The rem
   this adds is the rem the view takes off underneath (`mb-2` where these titles used
   to carry `mb-4`): the blocks do not move, the titles come down to them. The class
   is applied by Views/MyAccount/Index.cshtml alone, and never on the first title of a
   column — that one has to line up with the other column's top edge. */
.account-section-title {
    margin-top: 4rem;
}

/* The two cards of a row end on the same line. Bootstrap's `.row` already
   stretches both cells to the taller one, but a card is only as tall as its own
   body, so the preferences panel — two controls beside a three-field password
   form — stopped halfway down its cell and left the blue showing under it. From
   `lg`, where the cells sit side by side, each is a flex column and the card
   takes whatever height is left under its title; the card's own `mb-4` keeps
   the bottom edges level. Below `lg` the cells stack and the cards keep their
   own height. The class is applied by Views/MyAccount/Index.cshtml alone. */
@media (min-width: 992px) {
    .account-grid > .col-lg-6 {
        display: flex;
        flex-direction: column;
    }

    .account-grid > .col-lg-6 > .card {
        flex: 1 1 auto;
    }

    /* The preferences card is the one whose content is far shorter than its cell:
       two controls beside three fields and a button. Stretched by the rule above,
       it kept its controls piled at the top and showed the rest of itself as a
       blank white block. The body is a flex column that spreads the two forms
       evenly down the card instead, so the card reads as full at any height the
       password form gives it. The forms keep their `mb-3`: it is what spaces
       them below `lg`, where the card is as tall as its content and the two
       simply stack. */
    #account-preferences > .card-body {
        display: flex;
        flex-direction: column;
        justify-content: space-evenly;
    }
}

/* Bootstrap floats every `<legend>` and gives it a heading-sized font of its
   own, so a legend carrying `.form-label` came out left-floated and half again
   the size of the labels beside it. The font-size is settled by
   `.reservation-box .form-label` (two classes beat the bare element), which
   leaves the float, the width and the padding — the same three the evaluation
   form's legend needs, see `.evaluation-form .funnel-question__text` above. */
.reservation-box legend.form-label {
    float: none;
    width: 100%;
    padding: 0;
}

/* Bootstrap's radio is a 1em square that floats out of a label the height of one
   line — a 30 px row at this page's type size, where a pointer target has to be
   44. The row becomes a flex line of its own height with the control centred in
   it, so the whole 44 px is clickable rather than just the glyph, and the label
   fills the row instead of sitting on its own baseline. The evaluation form
   solves the same problem at its own scale (`.evaluation-form .form-check`
   above); it is not reused here because that block is boxed and this one is a
   plain stack of two options. */
#account-language-choice .form-check {
    display: flex;
    align-items: center;
    gap: 0.6rem;
    min-block-size: 2.75rem;
    margin-bottom: 0;
    padding-left: 0;
}

    #account-language-choice .form-check .form-check-input {
        float: none;
        flex: 0 0 auto;
        margin: 0;
    }

    #account-language-choice .form-check .form-check-label {
        display: flex;
        align-items: center;
        min-block-size: 2.75rem;
        cursor: pointer;
    }

/* Two options, one line — the design puts Nederlands and Frans (FR: Français
   and Néerlandais) beside each other, and the fieldset alone could not do it:
   it also carries the legend, and a flex fieldset would have pulled that onto
   the same row. Hence the wrapper. `column-gap` rather than `gap`, so the
   spacing is only ever horizontal; `wrap`, so the pair drops under each other
   on a narrow viewport rather than widening the card and taking the page's
   horizontal scrollbar with it. The options do not shrink, so what wraps is a
   whole option and never the words inside one. */
.account-language-options {
    display: flex;
    flex-wrap: wrap;
    column-gap: 2.5rem;
}

    .account-language-options .form-check {
        flex: 0 0 auto;
    }

/* Bootstrap's `.form-select-lg` declares a `font-size: 1.25rem` of its own, so
   the panel reset above cannot reach it — inheritance only settles what nothing
   else declares. The size is therefore said again here. The height that the
   1.25rem used to buy comes back as a minimum rather than being left to fall
   out of the type: `-lg` is on this select for the touch target, not for the
   wording, and 1rem of text inside its 0.5rem paddings lands at about 42 px —
   under the 44 the radios beside it hold to. */
#account-preferences .form-select-lg {
    font-size: 1rem;
    min-block-size: 2.75rem;
}

/* ===========================================================================
   PART 10 — My account: the orders and invoices card
   ---------------------------------------------------------------------------
   Views/MyAccount/Index.cshtml, the full-width block that replaced the old
   standalone invoice table. No new component and no new colour: the table is
   the theme's `.table` inside the same `.card.reservation-detail` as the four
   blocks above it, the strip is painted with `--ts-color-primary-soft`
   (Lavender) and the PDF glyph with `--ts-red`, both already in brand.css.
   Nothing here touches the `ts-` namespace.
   =========================================================================== */

/* Same reset, and the same reason, as `#account-preferences` above:
   `.reservation-box .reservation-detail` in assets/css/main.css sets
   `font-size: 1.25rem`, which is right for a card that is a short summary of
   what is on record and wrong for this one. Six columns, one of them carrying
   a wrapped product name per sale line, at 20 px is a table that scrolls
   sideways on a laptop. The note on `#account-preferences` says the four cards
   above it are meant to read at 1.25rem and lists the invoice table among
   them — that was the invoice table, four short columns wide; this is the
   orders table that replaced it. Hung on the id for the same two reasons:
   it restyles nothing else, and it outweighs main.css's two classes without
   an `!important`. */
#account-orders {
    font-size: 1rem;
}

/* The cells' ink, said explicitly because the mode token underneath them is
   wrong here: Bootstrap's `.table` colours every cell with `--bs-table-color`,
   which it takes from `--bs-body-color` — and brand.css flips that to White in
   learn mode, for text that sits on the Blue page. These cells sit on the White
   card, where White is invisible. The headings, the amounts and the strip never
   showed it because each carries its own colour; the dates, the product names
   and the licence values had nothing of their own and vanished. One rule covers
   both tables — the nested licence table is a `.table` under this id too. */
#account-orders .table {
    --bs-table-color: var(--ts-color-ink);
}

/* A little more air between the column headings. The gap is bought with
   padding on the right of the heading cell and not on both sides: every
   column but the invoice is left-aligned, so a left pad would walk the
   heading off the values underneath it. Widening the header cells widens
   the columns with them — the table is still `auto` layout and every cell
   keeps the theme's own padding, so nothing moves but the spacing. The
   right-aligned invoice column is left out: there the pad would show as an
   indent from the table's edge. */
.account-orders > thead > tr > th:not(.text-end) {
    padding-right: 3rem;
}

/* Sentence case, as the mockup draws it. `.reservation-box .table > thead th`
   further up puts every table heading on the funnel pages in small
   letter-spaced capitals; that is right for the short reservation tables and
   wrong here. "Datum bestelling", "Bedrag (excl. btw)" are phrases, and six of
   them set in 0.78rem capitals read as a legend printed above the table rather
   than as the table's own header row. Blue and bold are kept — on those the
   mockup and the house style agree — and the size comes up to the text
   underneath. Hung on the id, like `#account-orders` above and for the same
   reason: it outweighs that two-class rule without an `!important`, and it
   reaches this table and its nested one and nothing else. */
#account-orders .account-orders > thead > tr > th,
#account-orders .account-orders__licences > thead > tr > th {
    font-size: 1rem;
    font-weight: var(--ts-weight-heading);
    letter-spacing: normal;
    text-transform: none;
    color: var(--ts-color-primary);
}

/* An order and the panel underneath it are one block, so the rule that would
   have run between them is dropped and the panel row carries the separator
   instead. Without this the strip reads as a row of its own that happens to
   sit below the order, rather than as that order's own drawer. */
.account-orders > tbody > tr.account-orders__order > td {
    border-bottom-width: 0;
}

/* One product per line. A `<div>` and not a `<br>`-separated run, so the line
   height is settled here and a wrapped product name indents under itself
   rather than under the line above. */
.account-orders__product + .account-orders__product {
    margin-top: 0.35rem;
}

/* The invoice download. Red is the brand's `--ts-red` constant and not
   `--ts-color-danger`: that one aliases the mode-scoped `--ts-feedback-error`,
   which lightens to #f2867e in learn mode for legibility on Blue — and this
   glyph sits on a White card, where the lightened value is the one that cannot
   be read. Same reasoning as the validation-summary border in PART 4. */
.account-orders__invoice {
    color: var(--ts-red);
}

    .account-orders__invoice:hover,
    .account-orders__invoice:focus-visible {
        color: var(--ts-red);
    }

    .account-orders__invoice > i {
        font-size: 1.35rem;
    }

/* The order rows are top-aligned (the outer table dropped `align-middle`, see
   Index.cshtml), so a multi-product order keeps its one date and its amounts on
   the first product's line. The invoice link would spoil that line on its own:
   `.btn-lg` is what gives it the 44 px target, and that padding stands the
   glyph a step below the text around it. The negative margin lifts the button
   so the glyph sits centred on the row's first line — half the difference
   between the button's height and a text line — and costs the target nothing:
   the padding is untouched, the whole button just starts at the cell's top
   edge like the text does. */
.account-orders__order > td > .account-orders__invoice {
    margin-top: -0.75rem;
}

/* The panel row spans every column, and the one padding its cell carries is the
   indent on the left: the strip is that order's own drawer, so it starts a step
   in from the row above instead of reaching the table's edge. The right edge
   stays where it was — stepping in on both sides would read as a box centred
   under the row rather than as something hanging off it. */
.account-orders__trainings-row > td {
    padding: 0 0 0.75rem 4rem;
}

/* A smaller step on narrow viewports. The outer table already scrolls sideways
   here, so the full 4 rem indent would be paid for out of the four nested
   columns rather than out of any spare width. */
@media (max-width: 767.98px) {
    .account-orders__trainings-row > td {
        padding-left: 1.5rem;
    }
}

/* The drawer. Lavender is painted on the <details> and not on its summary, so
   an open panel is one continuous block running from the strip down past the
   last licence row, which is how the mockup shows it. Closed, the element *is*
   the strip, so the same fill draws the same bar it drew before. Bootstrap's
   cells stay out of the way: `.table` sets `--bs-table-bg: transparent`, so the
   nested rows let the block through rather than painting white over it. */
.account-orders__trainings {
    border-radius: var(--ts-radius);
    background: var(--ts-color-primary-soft);
}

/* The foot of the block, paid only when it is open. Closed, the summary's own
   padding is the whole height of the bar and a bottom pad would stretch it. */
.account-orders__trainings[open] {
    padding-bottom: 0.9rem;
}

/* The toggle strip. A native <details>, as Sales/AssignTrainings already uses,
   so it is keyboard operable with no script: `Enter` and `Space` on the summary
   open and close it, and the state is exposed to assistive technology by the
   element itself. */
.account-orders__trainings > summary {
    display: flex;
    align-items: center;
    gap: 0.6rem;
    min-height: 2.75rem;
    padding: 0.5rem 0.9rem;
    color: var(--ts-color-primary);
    font-weight: var(--ts-weight-heading);
    cursor: pointer;

    /* The card draws its own chevron, so the browser's disclosure triangle is
       removed — `list-style` for the standard marker, the pseudo-element for
       WebKit, which shipped the older one and still honours it. */
    list-style: none;
}

        .account-orders__trainings > summary::-webkit-details-marker {
            display: none;
        }

/* `chevron-right` closed, `chevron-down` open — one glyph turned a quarter,
   rather than two class names swapped by a script that would then have to run
   before the first paint to get the initially-open panel right. */
.account-orders__chevron {
    transition: transform 150ms ease-out;
}

.account-orders__trainings[open] > summary .account-orders__chevron {
    transform: rotate(90deg);
}

@media (prefers-reduced-motion: reduce) {
    .account-orders__chevron {
        transition: none;
    }
}

/* The nested table gets no `.table-responsive` of its own, deliberately: a
   second horizontal scroll region inside the first is a box a pointer can get
   stuck in and a screen reader announces twice. Five columns of short values
   fit; where they do not, they wrap, and the outer scroller is still there. */
.account-orders__licences {
    margin-top: 0.75rem;

    /* Fixed, and this is the whole reason the column widths below exist. Every
       order draws a licence table of its own, and `auto` layout measures each
       one against its own rows: a panel whose titles are long pushed the
       speaker, the duration and the holder far to the right, while the panel
       underneath it — two short titles — pulled them back left. The four
       columns therefore landed somewhere different in every drawer on the
       page, and the duration in particular never lined up with the duration
       above it. Fixed layout settles the widths from these rules instead of
       from the content, so the five columns sit at the same place in every
       panel and a long title wraps rather than shoving the row sideways. */
    table-layout: fixed;
}

/* The widths. The title had been taking whatever its longest line asked for,
   which is what stood the speaker halfway across the drawer; a third of the
   width holds the common titles on one line, wraps the long ones onto two, and
   brings the speaker back to where the mockup has it. The duration gets the
   narrowest column of the five — "1,5u" is four characters — and the attest
   the second widest after the title, because its link is a phrase and a glyph
   and not a bare icon. `nth-child` and not a `<colgroup>`: these five widths
   belong with the other rules for this table rather than in the middle of the
   markup, and the table's cells are all the view writes. */
#account-orders .account-orders__licences > * > tr > *:nth-child(1) {
    width: 32%;
}

#account-orders .account-orders__licences > * > tr > *:nth-child(2) {
    width: 20%;
}

#account-orders .account-orders__licences > * > tr > *:nth-child(3) {
    width: 9%;

    /* "2u" over "1,5u": the proportional figures of the body face set a comma
       and a 1 narrower than a 5, so a column of durations read as ragged even
       once it stopped moving between panels. Tabular figures give every digit
       the same advance, and the numbers stack. */
    font-variant-numeric: tabular-nums;
}

#account-orders .account-orders__licences > * > tr > *:nth-child(4) {
    width: 21%;
}

#account-orders .account-orders__licences > * > tr > *:nth-child(5) {
    width: 18%;
}

/* The block's side padding, spent on the first and last cell rather than on the
   table itself: `.table` is `width: 100%`, so padding on the element would push
   its right edge past the drawer it sits in. The left inset sets the licence
   columns a step in from "Opgenomen opleidingen", where the mockup has them;
   the right one keeps the last column off the rounded corner. */
#account-orders .account-orders__licences > * > tr > *:first-child {
    padding-left: 2.25rem;
}

#account-orders .account-orders__licences > * > tr > *:last-child {
    padding-right: 0.9rem;
}

/* Same trade as the row indent above: on a phone the outer table is already
   scrolling sideways, so the step in is paid out of the four nested columns. */
@media (max-width: 767.98px) {
    #account-orders .account-orders__licences > * > tr > *:first-child {
        padding-left: 0.9rem;
    }
}

/* The attest link: "Attest 2u" in Dutch, "Attestation 2h" in French, then the
   glyph. Blue and not the invoice link's red — that red is the PDF glyph's own
   convention, and this is a line of text with an icon after it rather than an
   icon on its own. The inline-flex is what keeps the glyph on the phrase's
   line when the phrase wraps, and `white-space: nowrap` on the label is what
   stops "Attestation" and "2h" being split across two lines, which would read
   as two separate things. The block padding buys the row a target worth
   pointing at without the `.btn-lg` the invoice link needs: that one is a bare
   glyph, where the padding *is* the target. */
.account-orders__attest {
    display: inline-flex;
    align-items: center;
    gap: 0.4rem;
    padding: 0.35rem 0;
    color: var(--ts-color-primary);
    font-weight: var(--ts-weight-heading);
}

    .account-orders__attest > span:first-child {
        white-space: nowrap;
    }

    .account-orders__attest:hover,
    .account-orders__attest:focus-visible {
        color: var(--ts-color-primary);
    }

/* A hair smaller than the invoice's 1.35rem: that glyph carries its cell on its
   own, this one sits beside a phrase and would out-weigh it at the same size. */
.account-orders__attest > i {
    font-size: 1.1rem;
}

.account-orders__empty {
    margin: 0;
    color: var(--ts-color-ink-soft);
}

.account-orders__trainings .account-orders__empty {
    padding: 0.75rem 0.9rem 0;
}
