/* ---- Site chrome: theme toggle, social links, brand ---- */
.theme-toggle {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 2.25rem;
    height: 2.25rem;
    padding: 0;
    border: 1px solid var(--ap-border);
    background: transparent;
    color: var(--ap-accent-text);
    border-radius: var(--ap-radius-button);
    transition: border-color 0.15s ease, color 0.15s ease;
}

.theme-toggle:hover {
    border-color: var(--ap-accent);
    color: var(--ap-accent-text-hover);
}

/* The footer's social icons share the theme-toggle recipe on purpose: border,
   accent icon and radius all come from the theme, so the row stays theme-agnostic. */
.social-link {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 2.25rem;
    height: 2.25rem;
    border: 1px solid var(--ap-border);
    color: var(--ap-accent-text);
    border-radius: var(--ap-radius-button);
    font-size: 0.85rem;
    transition: border-color 0.15s ease, color 0.15s ease;
}

.social-link:hover {
    border-color: var(--ap-accent);
    color: var(--ap-accent-text-hover);
}

.nav-brand-text {
    font-family: var(--ap-font-display);
    font-weight: 800;
    font-size: 0.8rem;
    letter-spacing: 0.14em;
    text-transform: uppercase;
}

.nav-mono {
    font-family: var(--ap-font-mono);
    font-size: 0.7rem;
    letter-spacing: 0.12em;
    text-transform: uppercase;
}

.section {
    padding: var(--ap-space-section) 0;
}

/* ---- Scroll reveal ---- */

/* Armed by app/reveal.js and only when motion is allowed, so a script that never runs
   leaves the section readable rather than invisible. Shared rather than per module: the
   signature templates each had their own copy of these eight lines. */
.reveal {
    opacity: 0;
    transform: translateY(20px);
    transition: opacity 0.8s cubic-bezier(0.2, 0.7, 0.2, 1), transform 0.8s cubic-bezier(0.2, 0.7, 0.2, 1);
}

.reveal.is-in {
    opacity: 1;
    transform: none;
}

/* app/reveal.js already refuses to arm under reduced motion; this catches a section the
   class reached before the query flipped. */
@media (prefers-reduced-motion: reduce) {
    .reveal {
        opacity: 1;
        transform: none;
        transition: none;
    }
}

/* ---- Per-block settings (the .blk wrapper BlockRenderer emits) ---- */
/* The wrapper owns a block's vertical padding: no block template carries a
   padding utility, so these presets are the only thing that pads a block.
   Normal is one block space per edge, half a section space, so two adjacent
   blocks still add up to the theme's section rhythm. */
.blk--top-none .section {
    padding-top: 0;
}

.blk--top-compact .section {
    padding-top: calc(var(--ap-space-block) / 2);
}

.blk--top-normal .section {
    padding-top: var(--ap-space-block);
}

.blk--top-large .section {
    padding-top: calc(var(--ap-space-block) * 2);
}

.blk--bottom-none .section {
    padding-bottom: 0;
}

.blk--bottom-compact .section {
    padding-bottom: calc(var(--ap-space-block) / 2);
}

.blk--bottom-normal .section {
    padding-bottom: var(--ap-space-block);
}

.blk--bottom-large .section {
    padding-bottom: calc(var(--ap-space-block) * 2);
}

/* Device visibility. The 992px break matches the col-lg-* splits the block
   templates use, so "mobile" here means what the layout already treats as
   mobile. */
@media (max-width: 991.98px) {
    .blk--hide-mobile {
        display: none;
    }
}

@media (min-width: 992px) {
    .blk--hide-desktop {
        display: none;
    }
}

/* Split-variant column order. .split-row marks the row whose two columns the options swap; a
   row nested inside one of those columns never carries it, and neither does a block with no
   split variant. A reversal holds at every width, so "comme sur ordinateur" keeps its
   promise, and the mobile option flips it back. */
.blk--reversed .split-row > :first-child {
    order: 2;
}

@media (max-width: 991.98px) {
    .blk--m-inverted .split-row > :first-child {
        order: 2;
    }

    .blk--reversed.blk--m-inverted .split-row > :first-child {
        order: 0;
    }
}

@media (min-width: 992px) {
    .divider-start-lg {
        border-left: 1px solid var(--ap-border);
    }

    /* The rule belongs between the two columns, so it changes side with them.
       The gutter around it is the row's own, never a padding of this rule. */
    .blk--reversed .divider-start-lg {
        border-left: 0;
        border-right: 1px solid var(--ap-border);
    }
}

/* ---- Base typography and surfaces ---- */
.display-title {
    font-family: var(--ap-font-display);
    font-weight: 900;
    letter-spacing: -0.03em;
    text-transform: uppercase;
    line-height: 0.98;
    overflow-wrap: break-word;
    margin-top: 12px;
}

.lead-quote {
    max-width: 520px;
    font-family: var(--ap-font-mono);
    color: var(--ap-faint);
}

.section-head {
    display: flex;
    align-items: baseline;
    gap: 1.25rem;
    margin-bottom: 2.25rem;
}

/* Unscoped: badges/strip and stats/panel place the eyebrow outside a
   .section-head, and it must look the same everywhere. */
.label {
    font-family: var(--ap-font-mono);
    font-size: 0.8rem;
    letter-spacing: 0.2em;
    text-transform: uppercase;
    color: var(--ap-faint);
    white-space: nowrap;
}

.rule-line {
    flex: 1;
    height: 1px;
    background: var(--ap-border);
}

.divider-accent {
    border-top: 2px solid var(--ap-accent);
}

.panel {
    position: relative;
    overflow: hidden;
    background-color: var(--ap-panel);
    border: 1px solid var(--ap-border);
    border-radius: var(--ap-radius);
}

.panel > *,
.cta-band > .container-fluid {
    position: relative;
    z-index: 1;
}

/* ---- Steps ---- */
.stepper .step-node {
    position: relative;
    z-index: 1;
    flex: none;
    display: flex;
    align-items: center;
    justify-content: center;
    width: 3.5rem;
    height: 3.5rem;
    border: 1px solid var(--ap-accent);
    border-radius: 50%;
    background: var(--ap-bg);
    color: var(--ap-accent-text);
    font-size: 1.25rem;
}

.stepper-horizontal {
    display: flex;
}

.stepper-horizontal .step {
    position: relative;
    flex: 1;
    display: flex;
    flex-direction: column;
    align-items: center;
    text-align: center;
    padding: 0 1rem;
}

.stepper-horizontal .step:not(:last-child)::after {
    content: '';
    position: absolute;
    top: 1.75rem;
    left: 50%;
    width: 100%;
    height: 1px;
    background: var(--ap-border);
    z-index: 0;
}

.stepper-horizontal .step-content {
    margin-top: 1.25rem;
}

@media (max-width: 767.98px) {
    .stepper-horizontal {
        flex-direction: column;
        gap: 2.25rem;
    }

    .stepper-horizontal .step {
        align-items: flex-start;
        text-align: left;
        padding: 0;
    }

    .stepper-horizontal .step:not(:last-child)::after {
        display: none;
    }

    .stepper-horizontal .step-content {
        margin-top: 1rem;
    }
}

.meta-line {
    font-family: var(--ap-font-mono);
    font-size: 0.625rem;
    line-height: 1.9;
    letter-spacing: 0.12em;
    text-transform: uppercase;
    color: var(--ap-faint);
}

/* ---- Image frames ---- */
.image-frame {
    position: relative;
    width: 100%;
    border: 1px solid var(--ap-border);
    background-color: var(--ap-panel);
    background-image:
        url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%238a8781' stroke-width='1.4' stroke-linecap='round' stroke-linejoin='round'%3E%3Crect x='3' y='3' width='18' height='18' rx='2'/%3E%3Ccircle cx='8.5' cy='8.5' r='1.8'/%3E%3Cpath d='m21 15-4.5-4.5a2 2 0 0 0-2.8 0L5 19'/%3E%3C/svg%3E"),
        repeating-linear-gradient(45deg, var(--ap-border) 0, var(--ap-border) 1px, transparent 1px, transparent 14px);
    background-repeat: no-repeat, repeat;
    background-position: center center, 0 0;
    background-size: 52px 52px, auto;
}

/* Every frame states its crop once, and on the frame rather than the image, so an unfilled
   slot is the same box as a filled one and a row of them keeps one baseline whatever the
   tenant has uploaded. A block wanting the file's own proportions puts aspect-ratio back to
   auto on the image it owns (gallery/masonry, hero/split). */
.frame-hero {
    aspect-ratio: 3 / 2;
}

.frame-square {
    aspect-ratio: 1 / 1;
}

.frame-wide {
    aspect-ratio: 3 / 2;
}

.frame-cinema {
    aspect-ratio: 21 / 9;
}

.frame-portrait {
    aspect-ratio: 4 / 5;
}

.site-image {
    width: 100%;
    object-fit: cover;
    border: 1px solid var(--ap-border);
}

/* ---- Audience cards and bullet lists ---- */
.audience-card {
    height: 100%;
    border: 1px solid var(--ap-border);
    border-top: 2px solid var(--ap-accent);
    padding: 1.75rem 1.5rem;
    transition: border-color 0.15s ease;
}

.audience-card:hover {
    border-color: var(--ap-accent);
}

.audience-icon {
    font-size: 1.6rem;
    color: var(--ap-accent);
}

.feature-item {
    display: grid;
    grid-template-columns: 1.5rem 1fr;
    gap: 0.875rem;
    padding: 0.875rem 0;
    border-top: 1px solid var(--ap-border);
    align-items: baseline;
}

.feature-item .check {
    color: var(--ap-accent);
}

/* ---- Call-to-action band ---- */
.cta-band {
    position: relative;
    overflow: hidden;
    background-color: var(--ap-accent);
    color: var(--ap-accent-contrast);
}

/* The same field panel and split set in the body face, kept to a measure the
   full-width band would otherwise let run edge to edge. */
.cta-band__body {
    max-width: 56ch;
    color: inherit;
}

.cta-band a.btn {
    --bs-btn-bg: var(--ap-accent-contrast);
    --bs-btn-border-color: var(--ap-accent-contrast);
    --bs-btn-color: var(--ap-accent);
    --bs-btn-hover-bg: transparent;
    --bs-btn-hover-border-color: var(--ap-accent-contrast);
    --bs-btn-hover-color: var(--ap-accent-contrast);
    --bs-btn-active-bg: transparent;
    --bs-btn-active-border-color: var(--ap-accent-contrast);
    --bs-btn-active-color: var(--ap-accent-contrast);

    border-radius: var(--ap-radius-button);
}

.cta-band .eyebrow,
.cta-ink .eyebrow,
.cta-note {
    color: inherit;
}

.cta-note {
    font-family: var(--ap-font-mono);
    font-size: 0.62rem;
    letter-spacing: 0.12em;
    text-transform: uppercase;
    opacity: 0.8;
}

/* Every accent token is measured against --ap-bg, so the swapped body pair is
   the only combination an inverted surface promises (ADR-0005). */
/* The inverted surface, shared by the two blocks that close a page on an
   invitation. The accent tokens are measured against --ap-bg and carry no
   guarantee on an ink fill (ADR-0005), so everything on it inherits. */
.cta-ink,
.newsletter-band {
    border-radius: var(--ap-radius);
    background-color: var(--ap-text);
    color: var(--ap-bg);
}

.cta-ink__body {
    max-width: 52ch;
    opacity: 0.85;
}

.cta-ink .btn-accent,
.newsletter-band .btn-accent {
    --bs-btn-bg: var(--ap-bg);
    --bs-btn-border-color: var(--ap-bg);
    --bs-btn-color: var(--ap-text);
    --bs-btn-hover-bg: transparent;
    --bs-btn-hover-border-color: var(--ap-bg);
    --bs-btn-hover-color: var(--ap-bg);
    --bs-btn-active-bg: transparent;
    --bs-btn-active-border-color: var(--ap-bg);
    --bs-btn-active-color: var(--ap-bg);
}

/* ---- Blog ---- */
/* Layout goes through the Bootstrap utilities in the template; these classes
   carry nothing but the tokens and the hover. .border already resolves
   --bs-border-color onto --ap-border (theme.css). */
.article-card {
    color: var(--ap-text);
    transition: border-color 0.15s ease;
}

.article-card:hover {
    border-color: var(--ap-accent);
    color: var(--ap-text);
}

.article-card .image-frame,
.article-card .site-image {
    border: 0;
    border-bottom: 1px solid var(--ap-border);
}

/* The list variant is an index, so each row carries what a reader decides on:
   when it was written, what it is about, and the first sentence of it. */
.article-list__row {
    border-top: 1px solid var(--ap-border);
}

.article-list__row:last-child {
    border-bottom: 1px solid var(--ap-border);
}

.article-list__link {
    display: grid;
    gap: 0.4rem;
    padding: 1.5rem 0;
    color: var(--ap-text);
    text-decoration: none;
    transition: color 0.15s ease;
}

@media (min-width: 768px) {
    .article-list__link {
        grid-template-columns: minmax(0, 1fr) auto;
        align-items: baseline;
        column-gap: 2rem;
    }

    .article-list__meta,
    .article-list__title,
    .article-list__excerpt {
        grid-column: 1;
    }

    .article-list__more {
        grid-column: 2;
        grid-row: 2;
        text-align: right;
        white-space: nowrap;
    }
}

.article-list__link:hover {
    color: var(--ap-accent-text);
}

.article-list__excerpt {
    max-width: 68ch;
    color: var(--ap-muted);
}

.article-list__more {
    color: var(--ap-accent-text);
}

.article-card-meta {
    padding-top: 0.75rem;
    font-family: var(--ap-font-mono);
    font-size: 0.625rem;
    letter-spacing: 0.1em;
    text-transform: uppercase;
    color: var(--ap-faint);
}

.measure-article {
    max-width: 760px;
}

.article-body {
    font-size: 1.0625rem;
    line-height: 1.75;
    color: var(--ap-muted);
}

.article-body p {
    margin-bottom: 1.25rem;
}

/* ---- Public forms ---- */
.public-form .form-label {
    font-weight: 600;
    color: var(--ap-text);
}

/* The one mark every form shares: the asterisk hangs on the `required` class
   bootstrap_5_layout puts on the label, an optional field carries nothing. Its alt text
   is empty: a screen reader acts on the control's required attribute, and the legend
   (components/_required_legend.html.twig) carries the meaning for everyone else. */
.required-legend {
    color: var(--ap-muted);
    font-size: 0.9rem;
    margin-bottom: 1rem;
}

.required-mark,
.form-label.required::after {
    color: var(--ap-accent-text);
    font-weight: 700;
}

.form-label.required::after {
    content: ' *' / '';
}

.public-form .form-control,
.public-form .form-select {
    padding: 0.65rem 0.9rem;
    transition: border-color 0.15s ease, box-shadow 0.15s ease;
}

.public-form .form-control::placeholder {
    color: var(--ap-faint);
}

.public-form .form-control:focus,
.public-form .form-select:focus {
    border-color: var(--ap-accent);
    box-shadow: 0 0 0 0.2rem color-mix(in srgb, var(--ap-accent) 25%, transparent);
}

/* Radio choices as selectable cards: bootstrap_5's btn-check markup
   (visually hidden input + adjacent label), pure-CSS states. The
   public site ships no framework JS (ADR-0007). */
.public-form .choice-cards {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(9rem, 1fr));
    gap: var(--ap-space-tight);
}

.public-form .choice-cards label {
    display: flex;
    align-items: center;
    justify-content: center;
    text-align: center;
    min-height: 4.25rem;
    padding: 1rem;
    background-color: var(--ap-panel);
    border: 1px solid var(--ap-border);
    border-radius: var(--ap-radius);
    font-weight: 600;
    color: var(--ap-text);
    cursor: pointer;
    transition: background-color 0.15s ease, border-color 0.15s ease, color 0.15s ease;
}

.public-form .choice-cards label:hover {
    border-color: var(--ap-accent-strong);
}

.public-form .choice-cards .btn-check:checked + label {
    background-color: var(--ap-accent);
    border-color: var(--ap-accent);
    color: var(--ap-accent-contrast);
}

.public-form .choice-cards .btn-check:focus-visible + label {
    outline: 2px solid var(--ap-accent);
    outline-offset: 2px;
}

.public-form .choice-cards .btn-check.is-invalid + label {
    border-color: var(--bs-danger);
}

/* Multi-choice select: a <details> disclosure styled as a closed .form-select.
   The panel overlays, so opening it never reflows the form. */
.public-form .choice-dropdown {
    position: relative;
}

/* display:block, not flex: it drops the disclosure triangle and it is what
   makes text-overflow work. The extra right padding keeps a long selection's
   ellipsis clear of the chevron the theme places 12px from the edge. */
.public-form .choice-dropdown-summary {
    display: block;
    cursor: pointer;
    list-style: none;
    overflow: hidden;
    white-space: nowrap;
    text-overflow: ellipsis;
    padding-right: 2.25rem;
}

.public-form .choice-dropdown-summary::-webkit-details-marker {
    display: none;
}

.public-form .choice-dropdown-summary.is-empty {
    color: var(--ap-faint);
}

.public-form .choice-dropdown-summary:focus-visible {
    outline: 2px solid var(--ap-accent);
    outline-offset: 2px;
}

.public-form .choice-dropdown-panel {
    position: absolute;
    z-index: 3;
    inset-inline: 0;
    top: calc(100% + 0.25rem);
    max-height: 15rem;
    overflow-y: auto;
    padding: 0.75rem 1rem;
    background-color: var(--ap-panel);
    border: 1px solid var(--ap-border);
    border-radius: var(--ap-radius);
    box-shadow: 0 6px 24px rgba(0, 0, 0, 0.18);
}

.public-form .choice-dropdown-panel .form-check {
    margin-bottom: 0.25rem;
}

.public-form .choice-dropdown-panel .form-check-label {
    cursor: pointer;
}

/* Consent checkbox: accent fill on Bootstrap's own glyph. Its input
   is appearance:none, so accent-color would be a no-op. */
.public-form .form-check-input:checked {
    background-color: var(--ap-accent);
    border-color: var(--ap-accent);
}

.public-form .form-check-input:focus {
    border-color: var(--ap-accent);
    box-shadow: 0 0 0 0.2rem color-mix(in srgb, var(--ap-accent) 25%, transparent);
}

.public-form .form-check .form-check-label {
    color: var(--ap-muted);
}

.confirm-badge {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 2.75rem;
    height: 2.75rem;
    background: var(--ap-accent);
    color: var(--ap-accent-contrast);
}

/* Multi-step forms: one fieldset per step. The thread and the nav are hidden until
   app/form_steps.js reveals them, so without the script the fieldsets read as sections. */
.form-steps__panel {
    min-width: 0;
    padding: 0;
    margin: 0;
    border: 0;
}

/* The rule between two sections; a page the script hid draws none under itself. */
.form-steps__panel:not([hidden]) + .form-steps__panel {
    margin-top: 2rem;
    padding-top: 2rem;
    border-top: 1px solid var(--ap-border);
}

.form-steps__legend {
    float: none;
    width: 100%;
    display: flex;
    align-items: baseline;
    gap: 0.75rem;
    margin-bottom: 1rem;
    font-family: var(--ap-font-display);
    font-size: 1.25rem;
    font-weight: 600;
    color: var(--ap-text);
}

.form-steps__legend:focus {
    outline: none;
}

.form-steps__index {
    font-family: var(--ap-font-mono);
    font-size: 0.75rem;
    letter-spacing: 0.12em;
    color: var(--ap-accent-text);
}

.form-steps__intro {
    margin-bottom: 1.25rem;
}

.form-steps__thread {
    display: flex;
    flex-direction: column;
    gap: 0.6rem;
    margin-bottom: 1.5rem;
}

.form-steps__rank {
    font-family: var(--ap-font-mono);
    font-size: 0.7rem;
    letter-spacing: 0.14em;
    text-transform: uppercase;
    color: var(--ap-muted);
}

.form-steps__bars {
    display: flex;
    gap: 0.35rem;
}

.form-steps__bars > span {
    flex: 1;
    height: 3px;
    border-radius: 3px;
    background: var(--ap-border);
    transition: background-color 0.2s ease;
}

.form-steps__bars > .is-done {
    background: var(--ap-accent);
}

.form-steps__nav {
    display: flex;
    align-items: center;
    gap: 0.75rem;
    margin-top: 1.5rem;
}

/* Suivant and Envoyer keep the right edge whether or not Précédent is on screen. */
.form-steps__nav > [data-form-steps-next],
.form-steps__nav > [data-form-steps-submit] {
    margin-left: auto;
}

/* The page app/form_steps.js brings on screen slides in; the panel it hides is display:none. */
@media (prefers-reduced-motion: no-preference) {
    .form-steps__panel:not([hidden]) {
        animation: form-steps-in 0.25s ease-out;
    }

    @keyframes form-steps-in {
        from {
            opacity: 0;
            transform: translateY(0.5rem);
        }
    }
}

/* ---- FAQ ---- */
.faq details {
    border-top: 1px solid var(--ap-border);
}

.faq details:last-of-type {
    border-bottom: 1px solid var(--ap-border);
}

.faq summary {
    display: flex;
    align-items: center;
    gap: 1.25rem;
    padding: 1.5rem 0;
    cursor: pointer;
    list-style: none;
    font-family: var(--ap-font-display);
    font-weight: 700;
    font-size: 1.15rem;
}

.faq summary:hover {
    color: var(--ap-accent-text);
}

.faq summary::-webkit-details-marker {
    display: none;
}

.faq summary .sign {
    position: relative;
    flex: none;
    width: 1.75rem;
    height: 1.75rem;
    margin-left: auto;
    color: var(--ap-accent);
    transition: transform 0.15s ease, color 0.15s ease;
}

/* The mark is drawn, not typed: a "+" glyph rides its own font's metrics and
   the mono family is the tenant's choice, so it never lands on the center. */
.faq summary .sign::before,
.faq summary .sign::after {
    content: "";
    position: absolute;
    top: 50%;
    left: 50%;
    width: 0.875rem;
    height: 2px;
    background-color: currentColor;
    transform: translate(-50%, -50%);
}

.faq summary .sign::after {
    transform: translate(-50%, -50%) rotate(90deg);
}

.faq summary:hover .sign {
    color: var(--ap-accent-text);
}

.faq details[open] summary .sign {
    transform: rotate(45deg);
}

.faq .answer {
    padding: 0 0 1.5rem;
    color: var(--ap-muted);
}

/* ─────────────────────────────────────────────────────────────
   UI polish
   ───────────────────────────────────────────────────────────── */

:focus-visible {
    outline: 2px solid var(--ap-accent);
    outline-offset: 2px;
}

/* Header with a subtle shadow once the page is scrolled; sticky is the
   tenant's choice (.site-nav--sticky). */
.site-nav {
    z-index: 1030;
    background-color: var(--ap-bg);
    transition: box-shadow 0.2s ease;
}

.site-nav--sticky {
    position: sticky;
    top: 0;
}

.site-nav.is-scrolled {
    box-shadow: 0 6px 24px rgba(0, 0, 0, 0.18);
}

/* The upload keeps its own ratio: only the height is capped, so a wide mark and
   a square one both leave the navbar the same height. */
.navbar-logo {
    max-height: 60px;
}

/* Navbar variants: layout re-composition only, same markup tree, same
   tokens, no variant-specific colors. Below lg every variant keeps the stock
   Bootstrap collapse. */
@media (min-width: 992px) {
    /* Centered: brand row on top, links row below, toggle in the corner. */
    .site-nav--centered .container-fluid {
        position: relative;
        flex-direction: column;
        gap: 0.25rem;
    }

    .site-nav--centered .navbar-brand {
        justify-content: center;
        margin-right: 0;
    }

    .site-nav--centered .nav-brand-text {
        font-size: 1.35rem;
        letter-spacing: 0.02em;
        text-transform: none;
    }

    .site-nav--centered .navbar-collapse {
        justify-content: center;
    }

    .site-nav--centered .nav-item--toggle {
        position: absolute;
        top: 0.75rem;
        right: 3rem;
    }

    /* The two lists read as one row everywhere but in the split variant, which
       spends the gap on its own side balance instead. */
    .site-nav:not(.site-nav--split) .nav-group--lead:has(.nav-item) {
        margin-inline-end: 1.5rem;
    }

    /* Split: links on both sides of the centered brand. The collapse dissolves
       (display: contents) so both lists flex directly in the container, where equal
       side widths hold the brand centered whatever each side carries. */
    .site-nav--split .navbar-collapse {
        /* stylelint-disable-next-line declaration-no-important -- matches Bootstrap's own on .navbar-expand-lg */
        display: contents !important;
    }

    .site-nav--split .container-fluid {
        column-gap: 1.5rem;
    }

    .site-nav--split .nav-group {
        flex: 1 1 0;
    }

    .site-nav--split .nav-group--lead {
        order: -1;
    }

    .site-nav--split .nav-group--trail {
        order: 1;
        justify-content: flex-end;
    }

    .site-nav--split .navbar-brand {
        order: 0;
        margin-inline: 0;
    }

    .site-nav--split .nav-brand-text {
        font-size: 1.35rem;
        letter-spacing: 0.02em;
        text-transform: none;
    }
}

/* Transparent navbar over a media hero. Fixed white and translucent literals,
   never theme tokens: a scrim over a photograph is theme-invariant (the
   .hero-screen precedent). Desktop only: a transparent collapse is unreadable. */
@media (min-width: 992px) {
    .site-nav--overlay {
        position: absolute;
        inset-inline: 0;
        top: 0;
    }

    .site-nav--overlay.site-nav--sticky {
        position: fixed;
    }

    .site-nav--overlay:not(.is-past-hero) {
        --bs-border-color: transparent;

        background-color: transparent;
    }

    .site-nav--overlay:not(.is-past-hero).is-scrolled {
        box-shadow: none;
    }

    .site-nav--overlay:not(.is-past-hero)::before {
        content: "";
        position: absolute;
        inset: 0 0 auto;
        height: 34svh;
        background: linear-gradient(180deg, rgb(10 10 12 / 55%), transparent);
        pointer-events: none;
        z-index: -1;
    }

    .site-nav--overlay:not(.is-past-hero) .nav-link {
        color: rgb(255 255 255 / 72%);
    }

    .site-nav--overlay:not(.is-past-hero) .nav-link.is-active {
        color: var(--bs-white);
        border-bottom-color: rgb(255 255 255 / 70%);
    }

    .site-nav--overlay:not(.is-past-hero) .navbar-brand,
    .site-nav--overlay:not(.is-past-hero) .nav-brand-text {
        color: var(--bs-white);
    }

    .site-nav--overlay:not(.is-past-hero) .theme-toggle {
        color: var(--bs-white);
        border-color: rgb(255 255 255 / 35%);
    }

    .site-nav--overlay:not(.is-past-hero) .btn-accent {
        background: rgb(255 255 255 / 14%);
        border-color: rgb(255 255 255 / 55%);
        color: var(--bs-white);
        backdrop-filter: blur(6px);
    }
}

.footer-logo {
    max-height: 58px;
}

/* Footer variants. */
.site-footer--statement {
    padding-top: 4rem;
}

.footer-statement {
    font-size: clamp(1.8rem, 4vw, 2.6rem);
    max-width: 20ch;
}

.nav-link.is-active {
    color: var(--ap-accent-text);
    border-bottom: 1px solid var(--ap-accent);
}

.arrow {
    display: inline-block;
    transition: transform 0.15s ease;
}

.btn:hover .arrow,
a:hover > .arrow {
    transform: translateX(3px);
}

.article-card,
.audience-card {
    transition: border-color 0.15s ease, transform 0.15s ease, box-shadow 0.15s ease;
}

.article-card:hover,
.audience-card:hover {
    transform: translateY(-4px);
    box-shadow: 0 10px 30px rgba(0, 0, 0, 0.18);
}

.reading-progress {
    position: fixed;
    top: 0;
    left: 0;
    height: 3px;
    width: 0;
    background: var(--ap-accent);
    z-index: 1040;
    transition: width 0.1s linear;
}

@media (prefers-reduced-motion: reduce) {
    .article-card:hover,
    .audience-card:hover {
        transform: none;
    }

    .arrow,
    .btn:hover .arrow,
    a:hover > .arrow {
        transition: none;
        transform: none;
    }

    .reading-progress {
        transition: none;
    }
}

/* ---- Hero ---- */
.hero-statement-aside {
    border-left: 2px solid var(--ap-accent);
    padding-left: 1.25rem;
    text-wrap: pretty;
}

/* Type-led opener: with no image to carry it, the headline takes the full
   measure and the prose steps back into a reading column under it. */
.hero-typo {
    display: flex;
    flex-direction: column;
    align-items: center;
    text-align: center;
    gap: 1.75rem;
}

/* The three centered things this opener has: the column, the text inside it and
   the accent rule over the eyebrow. The buttons follow through .hero-ctas. */
.blk--align-start .hero-typo {
    align-items: flex-start;
    text-align: left;
}

.hero-typo .hero-ctas {
    justify-content: center;
}

.blk--align-start .hero-typo .hero-ctas {
    justify-content: flex-start;
}

.hero-typo__eyebrow::before {
    content: "";
    display: block;
    width: 3rem;
    height: 2px;
    margin: 0 auto 1.1rem;
    background: var(--ap-accent);
}

.blk--align-start .hero-typo__eyebrow::before {
    margin-inline: 0;
}

.hero-typo__headline {
    font-size: clamp(2.6rem, 7.5vw, 5.5rem);
    text-wrap: balance;
}

.hero-typo__reading {
    display: flex;
    flex-direction: column;
    gap: 0.75rem;
    max-width: 60ch;
}

/* No hairline: the opening photograph reads as a plate, not as a framed
   thumbnail. It takes its crop from .frame-hero like every other slot. */
.site-image.hero-split__image {
    border: 0;
}

/* Full-screen media hero: the media owns the viewport and the text sits on it
   directly, no panel between the two. Fixed white and translucent literals,
   never theme tokens: a scrim over a photograph is theme-invariant
   (ADR-0005). */
.hero-screen {
    position: relative;
    display: flex;
    align-items: center;
    min-height: 100svh;
    overflow: hidden;
}

.hero-screen--flat {
    background: var(--ap-panel);
    color: var(--ap-text);
}

.hero-screen__image {
    position: absolute;
    inset: 0;
    z-index: 0;
    width: 100%;
    height: 100%;
    object-fit: cover;
}

/* Darkest at the two edges, lightest across the middle, so the navbar and the
   scroll cue stay readable without a band cutting the photograph in two. It
   stops short of full transparency because the tenant's photo can be bright
   exactly where the headline lands. */
.hero-screen:not(.hero-screen--flat)::before {
    content: "";
    position: absolute;
    inset: 0;
    z-index: 1;
    background: linear-gradient(
        180deg,
        rgb(10 10 12 / 62%) 0%,
        rgb(10 10 12 / 24%) 32%,
        rgb(10 10 12 / 28%) 62%,
        rgb(10 10 12 / 58%) 100%
    );
    pointer-events: none;
}

/* Scroll cue. Decorative, so a pseudo-element rather than markup a screen
   reader announces, and scoped to the overlay case: that is the only one where
   the hero starts at the top of the viewport, so anywhere else the cue would
   sit below the fold it is pointing at. */
@media (min-width: 992px) {
    body.has-nav-overlay .site-nav + .blk .hero-screen:not(.hero-screen--flat)::after {
        content: "↓";
        position: absolute;
        bottom: 1.75rem;
        left: 50%;
        z-index: 2;
        transform: translateX(-50%);
        color: rgb(255 255 255 / 65%);
        font-size: 1.1rem;
        line-height: 1;
        pointer-events: none;
    }
}

.hero-screen__body {
    position: relative;
    z-index: 2;
    width: 100%;
    padding: 6rem 0 5rem;
}

.hero-screen:not(.hero-screen--flat) .hero-screen__body {
    color: var(--bs-white);
}

.hero-screen:not(.hero-screen--flat) .eyebrow,
.hero-screen:not(.hero-screen--flat) .lead-quote {
    color: inherit;
}

/* The quote rule reads as a stray mark beside a centered column. */
.hero-screen .lead-quote {
    padding-left: 0;
    border-left: none;
}

/* Glass buttons: the accent fill would be the one colored thing on the photo,
   and its contrast is guaranteed against the theme, not against the media. */
.hero-screen:not(.hero-screen--flat) .btn-accent,
.hero-screen:not(.hero-screen--flat) .btn-outline-accent {
    --bs-btn-bg: rgb(255 255 255 / 14%);
    --bs-btn-border-color: rgb(255 255 255 / 55%);
    --bs-btn-color: var(--bs-white);
    --bs-btn-hover-bg: rgb(255 255 255 / 26%);
    --bs-btn-hover-border-color: var(--bs-white);
    --bs-btn-hover-color: var(--bs-white);
    --bs-btn-active-bg: rgb(255 255 255 / 26%);
    --bs-btn-active-border-color: var(--bs-white);
    --bs-btn-active-color: var(--bs-white);

    backdrop-filter: blur(6px);
}

/* ---- Galleries ---- */
.gallery-masonry {
    columns: 2;
    column-gap: 1rem;
}

@media (min-width: 992px) {
    .gallery-masonry {
        columns: 3;
    }
}

/* The gap under a caption is what separates two images, so it has to beat the
   gap above it by enough that the caption reads as belonging to the one it
   sits under rather than to the one below. */
.gallery-masonry__item {
    break-inside: avoid;
    margin: 0 0 2.25rem;
}

.gallery-masonry__caption {
    display: block;
    margin-top: 0.4rem;
}

.gallery-masonry__item .site-image,
.gallery-masonry__item .image-frame {
    width: 100%;
    height: auto;
    min-height: 0;
    aspect-ratio: auto;
}

.gallery-masonry__item .image-frame {
    min-height: 220px;
}

/* Editor only, and after the rule above so these win: an unfilled slot has no
   image to take its proportions from, so every placeholder would render at one
   height and the mosaic would read as a grid. The template cycles the three
   shapes a mixed gallery produces. */
.gallery-masonry__item--tall .image-frame {
    min-height: 0;
    aspect-ratio: 3 / 4;
}

.gallery-masonry__item--square .image-frame {
    min-height: 0;
    aspect-ratio: 1 / 1;
}

.gallery-masonry__item--wide .image-frame {
    min-height: 0;
    aspect-ratio: 4 / 3;
}

/* An explicit width is load-bearing: the slides size with width: 100%, which
   contributes nothing to a shrink-to-fit box, so an auto-width carousel
   collapses to its indicator dots and never reaches the max-width. */
.gallery-carousel {
    width: 100%;
    max-width: 1080px;
    margin-inline: auto;
}

/* Shared by the gallery block and the boutique gallery: the arrows are
   positioned against the stage, so the thumbnails stay outside it. */
/* Bootstrap's carousel restyled to the theme tokens: its stock controls and
   indicators are white and vanish on the light theme. Never set a plain
   "margin" on the item: .carousel-item needs Bootstrap's margin-right: -100%. */
.gallery-carousel__item {
    margin-top: 0;
    margin-bottom: 0;
}

.carousel-control-prev:has(.carousel-chip),
.carousel-control-next:has(.carousel-chip) {
    width: 5rem;
    opacity: 1;
}

.carousel-chip {
    display: grid;
    place-items: center;
    width: 2.75rem;
    height: 2.75rem;
    border: 1px solid var(--ap-border);
    border-radius: var(--ap-radius);
    background: var(--ap-panel);
    color: var(--ap-text);
    transition: background-color 0.2s ease, border-color 0.2s ease, color 0.2s ease;
}

.carousel-control-prev:hover .carousel-chip,
.carousel-control-next:hover .carousel-chip,
.carousel-control-prev:focus-visible .carousel-chip,
.carousel-control-next:focus-visible .carousel-chip {
    border-color: var(--ap-accent);
    background: var(--ap-accent);
    color: var(--ap-accent-contrast);
}

@media (max-width: 575.98px) {
    .carousel-control-prev:has(.carousel-chip),
    .carousel-control-next:has(.carousel-chip) {
        width: 3.5rem;
    }

    .carousel-chip {
        width: 2.25rem;
        height: 2.25rem;
    }
}

.gallery-carousel .carousel-indicators,
.gallery-stage .carousel-indicators {
    position: static;
    margin: 1rem 0 0;
}

.gallery-carousel .carousel-indicators [data-bs-target],
.gallery-stage .carousel-indicators [data-bs-target] {
    background-color: var(--ap-text);
    opacity: 0.3;
}

.gallery-carousel .carousel-indicators .active,
.gallery-stage .carousel-indicators .active {
    background-color: var(--ap-accent);
    opacity: 1;
}

.gallery-carousel__item .image-frame,
.gallery-carousel__item .site-image {
    aspect-ratio: 16 / 9;
    width: 100%;
    height: auto;
    min-height: 0;
    object-fit: cover;
    border-radius: var(--ap-radius);
}

/* A 16/9 slide on a phone is a 190px letterbox; the taller crop keeps the
   subject readable at the widths a single-image variant is built for. */
@media (max-width: 575.98px) {
    .gallery-carousel__item .image-frame,
    .gallery-carousel__item .site-image {
        aspect-ratio: 4 / 3;
    }
}

/* The stage variant: the current image at the centre, its two neighbours in the
   wings. Bootstrap's carousel is the engine, so the wrap at both ends, the arrows,
   the keyboard and the swipe are its; only the placement is here. The band clips,
   because the wings are meant to run off the page. */
.gallery-stage-band {
    overflow: hidden;
}

.gallery-stage {
    --gs-peek: 58%;

    width: 100%;
    max-width: 920px;
    margin-inline: auto;
}

/* One grid cell holds every slide, so the stage keeps the height of the tallest
   and the wings can leave the box. Bootstrap stacks them with floats inside an
   overflow-hidden parent, which would clip exactly what this variant shows. */
.gallery-stage .carousel-inner {
    display: grid;
    overflow: visible;
}

.gallery-stage .carousel-inner > .carousel-item {
    grid-area: 1 / 1;
    display: block;
    float: none;
    margin-right: 0;
    visibility: hidden;
    opacity: 0;
    transition: transform 0.45s ease, opacity 0.45s ease;
}

.gallery-stage .carousel-inner > .carousel-item.active {
    z-index: 2;
    visibility: visible;
    opacity: 1;
    transform: none;
}

/* The wings: the item before .active, and the last one when the first is centred. */
.gallery-stage .carousel-inner > .carousel-item:has(+ .active),
.gallery-stage .carousel-inner:has(> .carousel-item:first-child.active) > .carousel-item:last-child {
    z-index: 1;
    visibility: visible;
    opacity: 0.35;
    transform: translateX(calc(-1 * var(--gs-peek))) scale(0.82);
}

.gallery-stage .carousel-inner > .active + .carousel-item,
.gallery-stage .carousel-inner:has(> .carousel-item:last-child.active) > .carousel-item:first-child {
    z-index: 1;
    visibility: visible;
    opacity: 0.35;
    transform: translateX(var(--gs-peek)) scale(0.82);
}

.gallery-stage__item .image-frame,
.gallery-stage__item .site-image {
    aspect-ratio: 3 / 2;
    width: 100%;
    height: auto;
    min-height: 0;
    object-fit: cover;
    border-radius: var(--ap-radius);
}

/* Only the centred slide names itself: a caption read off a dimmed wing would
   label the wrong photo. */
.gallery-stage__caption {
    margin-top: 1rem;
    text-align: center;
    opacity: 0;
    transition: opacity 0.45s ease;
}

.gallery-stage .carousel-inner > .carousel-item.active .gallery-stage__caption {
    opacity: 1;
}

/* The caption sits inside the carousel box, so centring the chips on the box
   would drop them below the middle of the photo. */
.gallery-stage .carousel-control-prev,
.gallery-stage .carousel-control-next {
    z-index: 3;
    padding-bottom: 2.6rem;
}

/* Wide enough for the chips to clear the centre photo and sit over the wings. */
@media (min-width: 992px) {
    .gallery-stage .carousel-control-prev {
        left: -3.25rem;
    }

    .gallery-stage .carousel-control-next {
        right: -3.25rem;
    }
}

/* A wing on a phone would leave the centre too narrow to read. */
@media (max-width: 767.98px) {
    .gallery-stage {
        --gs-peek: 76%;

        max-width: none;
    }

    .gallery-stage .carousel-inner > .carousel-item:not(.active) {
        opacity: 0.22;
    }
}

/* ---- Pricing ---- */
.pricing-tier {
    padding: 1.75rem;
    border: 1px solid var(--ap-border);
    border-radius: var(--ap-radius);
    background: var(--ap-panel);
}

.pricing-tier--on {
    border-color: var(--ap-accent);
    box-shadow: 0 0 0 1px var(--ap-accent);
}

.pricing-tier__price {
    font-family: var(--ap-font-display);
    font-size: 1.75rem;
    line-height: 1.1;
}

.pricing-tier__list {
    list-style: none;
    margin: 0 0 1.25rem;
    padding: 0;
}

.pricing-tier__list li {
    position: relative;
    padding-left: 1.35rem;
    margin-bottom: 0.4rem;
}

.pricing-tier__list li::before {
    content: "✓";
    position: absolute;
    left: 0;
    color: var(--ap-accent);
}

.pricing-table__row {
    display: grid;
    grid-template-columns: 1fr;
    gap: 1rem;
    padding: 1.5rem 0;
    border-top: 1px solid var(--ap-border);
}

@media (min-width: 768px) {
    .pricing-table__row {
        grid-template-columns: 1fr 1.4fr auto;
        align-items: start;
        gap: 2rem;
    }
}

.pricing-table__row--on {
    border-top-color: var(--ap-accent);
}

.pricing-table__price {
    text-align: left;
}

@media (min-width: 768px) {
    .pricing-table__price {
        text-align: right;
        min-width: 10rem;
    }
}

/* Sticky cells drop their borders under border-collapse, so the matrix keeps
   separate borders and paints them cell by cell. */
/* Positioned so the cells' .visually-hidden spans resolve against this box:
   without it they escape the scroller and widen the page on a phone. */
.price-matrix {
    position: relative;
    overflow-x: auto;
    margin-inline: -0.25rem;
    padding-inline: 0.25rem;
}

.price-matrix__table {
    width: 100%;
    min-width: 34rem;
    border-collapse: separate;
    border-spacing: 0;
}

.price-matrix__corner {
    background: var(--ap-bg);
}

.price-matrix__criterion,
.price-matrix__corner {
    position: sticky;
    left: 0;
    z-index: 1;
    min-width: 11rem;
}

.price-matrix__criterion {
    background: var(--ap-bg);
    font-weight: 500;
    text-align: left;
    padding: 0.85rem 1rem 0.85rem 0;
    border-top: 1px solid var(--ap-border);
}

.price-matrix__head {
    padding: 0 1rem 1rem;
    text-align: center;
    vertical-align: bottom;
}

.price-matrix__price {
    display: block;
    font-family: var(--ap-font-display);
    font-size: 1.5rem;
    line-height: 1.1;
}

.price-matrix__cell {
    padding: 0.85rem 1rem;
    text-align: center;
    vertical-align: middle;
    border-top: 1px solid var(--ap-border);
}

.price-matrix__head--on,
.price-matrix__cell--on {
    background: rgba(var(--ap-accent-rgb), 0.07);
}

.price-matrix__head--on {
    border-top: 2px solid var(--ap-accent);
    border-radius: var(--ap-radius) var(--ap-radius) 0 0;
    padding-top: 1rem;
}

/* Whichever of tbody or tfoot ends the table closes the highlighted column. */
.price-matrix__table > *:last-child > tr:last-child .price-matrix__cell--on {
    border-radius: 0 0 var(--ap-radius) var(--ap-radius);
}

.price-matrix__mark {
    font-size: 1.1rem;
    line-height: 1;
}

.price-matrix__mark--yes {
    color: var(--ap-accent-text);
}

.price-matrix__mark--no {
    color: var(--ap-muted);
}

.price-matrix-card {
    padding: 1.75rem;
    border-radius: var(--ap-radius);
}

.price-matrix-card--on {
    border-color: var(--ap-accent);
    box-shadow: 0 0 0 1px var(--ap-accent);
}

.price-matrix-card__list {
    margin: 0 0 1.25rem;
}

.price-matrix-card__row {
    display: flex;
    align-items: baseline;
    justify-content: space-between;
    gap: 1rem;
    padding: 0.5rem 0;
    border-top: 1px solid var(--ap-border);
}

.price-matrix-card__row:first-child {
    border-top: 0;
}

.price-matrix-card__criterion {
    color: var(--ap-muted);
    font-weight: 400;
}

.price-matrix-card__value {
    margin: 0;
    font-weight: 500;
    text-align: right;
}

/* ---- Key figures ---- */
.stat__value {
    font-family: var(--ap-font-display);
    font-size: clamp(1.9rem, 4vw, 2.75rem);
    line-height: 1;
    color: var(--ap-text);
}

/* Every accent token is measured against --ap-bg, never against --ap-text, so
   nothing accent-colored is guaranteed legible here (ADR-0005). Inverting the
   body pair is the only combination the palette promises on an ink fill. */
.stats-band {
    background-color: var(--ap-text);
    color: var(--ap-bg);
}

.stats-band .stat__value {
    color: inherit;
    font-size: clamp(2.1rem, 5vw, 3.25rem);
}

.stats-band .label {
    color: color-mix(in srgb, var(--ap-bg) 62%, var(--ap-text));
}

.stats-band__caption {
    max-width: 22ch;
    margin-inline: auto;
    font-size: 0.9rem;
    line-height: 1.5;
    color: color-mix(in srgb, var(--ap-bg) 78%, var(--ap-text));
}

/* ---- Logo band ---- */
.logo-strip {
    list-style: none;
    margin: 0;
    padding: 0;
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 2.5rem;
}

.logo-strip__item img {
    max-height: 88px;
    max-width: 300px;
    width: auto;
    filter: grayscale(1);
    opacity: 0.7;
    transition: filter 0.15s ease, opacity 0.15s ease;
}

.logo-strip__item img:hover {
    filter: none;
    opacity: 1;
}

/* An unfilled slot keeps its place in the strip: .image-frame has no
   intrinsic height and the flex item shrinks to fit, so the box is sized
   here to match the 88px logo cap. */
.logo-strip__item--empty {
    width: 160px;
}

.logo-strip__item--empty .image-frame {
    height: 88px;
    background-size: 36px 36px, auto;
}

/* ---- Testimonials ---- */
/* One surface per avis: the quote first because it is the content, the face a
   fixed round crop at the foot of it rather than a column that stretches to
   whatever the quote's height turns out to be. */
.testimonial-card {
    display: flex;
    flex-direction: column;
    gap: 1.25rem;
    margin: 0;
    padding: 1.75rem;
    background: var(--ap-panel);
    border: 1px solid var(--ap-border);
    border-radius: var(--ap-radius);
}

.testimonial-card__quote {
    flex: 1;
    margin: 0;
    font-family: var(--ap-font-display);
    font-size: 1.15rem;
    line-height: 1.45;
    text-wrap: pretty;
}

.testimonial-card__who {
    display: flex;
    align-items: center;
    gap: 0.9rem;
    padding-top: 1.25rem;
    border-top: 1px solid var(--ap-border);
}

.testimonial-card__portrait {
    flex: none;
    width: 3.25rem;
}

/* The alternating rows carry the testimony at display scale: the photograph is
   support, so the quote is the loudest thing in the row. */
.testimonial-lead {
    margin: 0;
    font-family: var(--ap-font-display);
    font-size: clamp(1.25rem, 2.2vw, 1.75rem);
    line-height: 1.35;
    text-wrap: pretty;
}

.testimonial-row + .testimonial-row {
    margin-top: 3rem;
}

/* ---- Rich text ---- */
.rich-text > :last-child {
    margin-bottom: 0;
}

.rich-text blockquote {
    margin: 1.5rem 0;
    padding-left: 1.25rem;
    border-left: 2px solid var(--ap-accent);
    font-family: var(--ap-font-display);
}

/* The title column is short and the text column is long, so the rule reads as
   the seam between them rather than as a box around either. */
@media (min-width: 992px) {
    .rich-text__title {
        position: sticky;
        top: 5rem;
    }
}

/* ---- Stats ---- */
/* A rule between the cells, so a short last label reads as the end of a column
   rather than as a hole on the right of the section. The padding is what keeps
   the figures off it: the first cell keeps its edge, aligned with the heading. */
@media (min-width: 768px) {
    .stat-row__cell + .stat-row__cell {
        border-left: 1px solid var(--ap-border);
        padding-left: 2rem;
    }

    .stat-row__cell:not(:last-child) {
        padding-right: 2rem;
    }
}

/* ---- Features: the numbered variants ---- */
/* The rank is the list's own index in both, which is why no variant of this
   block stores a number the order could contradict. */
.step-rank {
    font-family: var(--ap-font-display);
    font-size: 2.4rem;
    font-weight: 600;
    line-height: 0.9;
    color: var(--ap-muted);
}

.step-title {
    margin: 0;
    font-family: var(--ap-font-display);
    font-size: 1.1rem;
    font-weight: 600;
}

.step-body {
    margin: 0;
    color: var(--ap-muted);
}

/* One rule per row and one under the last, so the run reads as a sequence rather
   than as three boxes. */
.step-list {
    list-style: none;
    margin: 0;
    padding: 0;
    border-bottom: 1px solid var(--ap-border);
}

.step-list__row {
    display: grid;
    gap: 0.5rem;
    padding: 1.6rem 0;
    border-top: 1px solid var(--ap-border);
}

@media (min-width: 768px) {
    .step-list__row {
        grid-template-columns: 6rem 1fr 1fr;
        gap: 2rem;
        align-items: start;
        padding: 1.9rem 0;
    }
}

/* ---- Divider ---- */
/* A block that says nothing, on purpose. The rule takes the theme's border tone
   and never the accent, which is kept for the things that do mean something, and
   the size is the air the rule sits in rather than the block's own margins. */
.divider-rule {
    display: flex;
    align-items: center;
    gap: 1.5rem;
}

.divider-rule--small {
    padding: 2.5rem 0;
}

.divider-rule--medium {
    padding: 4rem 0;
}

.divider-rule--large {
    padding: 6rem 0;
}

/* The accent variant stacks its mark over its word, so the two centre on each
   other instead of sitting on one baseline. */
.divider-rule--centered {
    flex-direction: column;
    justify-content: center;
    gap: 0.9rem;
}

.divider-rule__mark {
    width: 3.5rem;
}

.divider-rule--editing {
    outline: 1px dashed var(--ap-border);
}

/* ---- Bandeau défilant ---- */
/* The track is the band's own width and each group is never narrower than it, so a
   list that does not fill the band spreads its words instead of running out mid-cycle
   and leaving it empty until the loop restarts. */
.marquee {
    overflow: hidden;
}

.marquee__track {
    display: flex;
}

.marquee__group {
    display: flex;
    flex: 0 0 auto;
    min-width: 100%;
    justify-content: space-around;
    animation: marquee-slide 34s linear infinite;
}

.marquee__word {
    display: inline-flex;
    align-items: center;
    white-space: nowrap;
    font-family: var(--ap-font-display);
}

.marquee__dot {
    flex: none;
    border-radius: 50%;
    background: var(--ap-accent);
}

/* Exactly one group, so the second one lands where the first started. */
@keyframes marquee-slide {
    from { transform: translate3d(0, 0, 0); }
    to { transform: translate3d(-100%, 0, 0); }
}

.marquee--loop {
    padding-block: clamp(1.125rem, 2.6vw, 1.75rem);
    border-block: 1px solid var(--ap-border);
    background: var(--ap-panel);
}

.marquee--loop .marquee__word {
    gap: clamp(1.5rem, 4vw, 3.25rem);
    padding-right: clamp(1.5rem, 4vw, 3.25rem);
    font-weight: 800;
    font-size: clamp(1.3rem, 3vw, 2.4rem);
    letter-spacing: -0.02em;
    text-transform: uppercase;
    color: var(--ap-muted);
}

.marquee--loop .marquee__dot {
    width: 0.5rem;
    height: 0.5rem;
}

/* Pulled up under the opener and tipped, so the band cuts the page rather than stacking
   under it. Inset by the bleed the tilt costs, half a band height times the sine either
   side: any wider and a phone scrolls sideways, and nothing here clips it, since the
   band's own overflow governs its contents and not its rotated box. The triangles of
   page its corners leave are the tilt rather than a gap to close. */
.marquee--defile {
    position: relative;
    z-index: 1;
    margin-block: -1.25rem 0;
    margin-inline: 2px;
    padding-block: 14px;
    transform: rotate(-1.5deg);
    background: var(--ap-accent);
    color: var(--ap-accent-contrast);
}

.marquee--defile .marquee__group {
    animation-duration: 40s;
}

.marquee--defile .marquee__word {
    gap: 1.5rem;
    padding-right: 1.5rem;
    font-weight: 600;
    font-size: clamp(1rem, 1.6vw, 1.25rem);
    letter-spacing: 0.04em;
    text-transform: uppercase;
}

.marquee--defile .marquee__dot {
    width: 0.35rem;
    height: 0.35rem;
    background: currentcolor;
    opacity: 0.7;
}

/* Every accent token is measured against --ap-bg and carries no guarantee on an ink
   fill (ADR-0005), so the words take the inverted body pair and only the dot, which
   is decoration inside an aria-hidden track, keeps the accent. */
.marquee--bandeau {
    padding-block: 0.7rem;
    background: var(--ap-text);
    color: var(--ap-bg);
}

.marquee--bandeau .marquee__group {
    animation-duration: 46s;
}

.marquee--bandeau .marquee__word {
    gap: 1.15rem;
    padding-right: 1.15rem;
    font-weight: 400;
    font-size: clamp(0.95rem, 1.4vw, 1.15rem);
}

.marquee--bandeau .marquee__dot {
    width: 0.32rem;
    height: 0.32rem;
}

@media (prefers-reduced-motion: reduce) {
    .marquee__group {
        animation: none;
    }
}

/* ---- Video ---- */
/* Click-to-load, the same contract as the map: the poster and the button are all
   a page view loads. The 16:9 lives on the frame rather than on a shared crop,
   because what fills it is an iframe as often as an image. */
.video-stacked__body {
    max-width: 60ch;
}

.video-canvas {
    position: relative;
    aspect-ratio: 16 / 9;
    overflow: hidden;
    border: 1px solid var(--ap-border);
    border-radius: var(--ap-radius);
    background: var(--ap-panel);
}

.video-canvas .site-image,
.video-canvas .image-frame,
.video-canvas iframe {
    position: absolute;
    inset: 0;
    width: 100%;
    height: 100%;
    border: 0;
    object-fit: cover;
}

.video-canvas__play {
    position: absolute;
    inset: 0;
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: 0.75rem;
    color: var(--ap-text);
    text-decoration: none;
}

/* White on a scrim rather than a token: an accent has no contrast guarantee over
   a photograph, and this one is over whichever photograph the tenant picked. */
.video-canvas--poster .video-canvas__play {
    color: #fff;
    background: linear-gradient(180deg, rgba(10, 10, 12, 0.2), rgba(10, 10, 12, 0.55));
}

.video-canvas__button {
    display: grid;
    place-items: center;
    width: 4.5rem;
    height: 4.5rem;
    border: 1px solid currentcolor;
    border-radius: 50%;
}

.video-canvas__play:hover .video-canvas__button {
    background: rgba(var(--ap-accent-rgb), 0.16);
}

.video-canvas__cue,
.map-canvas__cue {
    font-family: var(--ap-font-mono);
    font-size: 0.75rem;
    letter-spacing: 0.1em;
    text-transform: uppercase;
}

/* The bleed variant is one frame per page, so it takes a cinema crop and carries
   its title inside. The text takes no pointer events: the whole frame is the
   play control, and a title on top of it must not swallow the click. */
.video-canvas--bleed {
    aspect-ratio: 21 / 9;
}

.video-bleed__text {
    position: absolute;
    right: 1.5rem;
    bottom: 1.5rem;
    left: 1.5rem;
    color: #fff;
    pointer-events: none;
}

@media (max-width: 767.98px) {
    .video-canvas--bleed {
        aspect-ratio: 4 / 3;
    }
}

/* ---- Badge band ---- */
/* A band of guarantees, not a row of fields: no box around each item, one rule
   over and under the band, and a hairline between the cells, so the items read
   as one line rather than as a row of labelled inputs. */
.badge-strip {
    list-style: none;
    display: grid;
    gap: 1.25rem;
    margin: 0;
    padding: 1.75rem 0;
    border-top: 1px solid var(--ap-border);
    border-bottom: 1px solid var(--ap-border);
}

@media (min-width: 768px) {
    .badge-strip {
        grid-auto-flow: column;
        grid-auto-columns: 1fr;
        gap: 0;
    }

    .badge-strip__item {
        padding-inline: 2rem;
    }

    .badge-strip__item:first-child {
        padding-inline-start: 0;
    }

    .badge-strip__item:last-child {
        padding-inline-end: 0;
    }

    .badge-strip__item + .badge-strip__item {
        border-left: 1px solid var(--ap-border);
    }
}

.badge-strip__item {
    display: flex;
    align-items: flex-start;
    gap: 0.85rem;
}

.badge-strip__icon {
    flex: none;
    margin-top: 0.2rem;
    font-size: 1.05rem;
    color: var(--ap-accent);
}

.badge-strip__body {
    display: flex;
    flex-direction: column;
    gap: 0.2rem;
    min-width: 0;
}

.badge-strip__title {
    font-family: var(--ap-font-display);
    font-size: 1rem;
    font-weight: 700;
    color: var(--ap-text);
}

.badge-strip__detail {
    font-size: 0.85rem;
    line-height: 1.45;
    color: var(--ap-muted);
}

/* ---- Qualification cards ---- */
/* The numbers sit at the foot of the card behind their own rule, so a row of
   cards whose names and organisms wrap differently still lines its proof up. */
.qualification-card {
    padding: 1.5rem;
    border: 1px solid var(--ap-border);
    border-radius: var(--ap-radius);
    background: var(--ap-panel);
}

/* Wins over .site-image and .image-frame, which both set width: 100%. */
.qualification-card__logo {
    width: 3.5rem;
    aspect-ratio: 1;
    object-fit: contain;
}

.qualification-card__proof {
    display: flex;
    flex-direction: column;
    gap: 0.35rem;
    margin-top: 1.25rem;
    padding-top: 1.1rem;
    border-top: 1px solid var(--ap-border);
}

.qualification-card__number {
    font-family: var(--ap-font-mono);
    font-size: 0.8rem;
    color: var(--ap-text);
}

.qualification-card__link {
    color: var(--ap-accent-text);
    text-decoration: none;
}

.qualification-card__link:hover {
    color: var(--ap-accent-text-hover);
}

/* ---- Hours, map and contact ---- */
/* The hours status is a state, so it keeps the pill. The contact note is a
   sentence about the channels under it, and a bordered pill above a list of
   links reads as the first button in the section. */
.contact-note {
    display: block;
    margin-bottom: 1.5rem;
    font-family: var(--ap-font-mono);
    font-size: 0.7rem;
    letter-spacing: 0.12em;
    text-transform: uppercase;
    color: var(--ap-muted);
}

.hours-status {
    display: inline-block;
    margin-bottom: 1.5rem;
    padding: 0.45rem 0.9rem;
    border: 1px solid var(--ap-accent);
    border-radius: var(--ap-radius);
    font-family: var(--ap-font-mono);
    font-size: 0.7rem;
    letter-spacing: 0.12em;
    text-transform: uppercase;
    color: var(--ap-accent-text);
}

.hours-row {
    display: flex;
    align-items: baseline;
    gap: 0.75rem;
    padding: 0.8rem 0;
    border-bottom: 1px solid var(--ap-border);
}

.hours-row__day {
    font-family: var(--ap-font-mono);
    font-size: 0.75rem;
    letter-spacing: 0.12em;
    text-transform: uppercase;
}

.hours-row--closed .hours-row__day,
.hours-row--closed .hours-row__value {
    color: var(--ap-faint);
}

/* The dotted run that ties a label to its value. Baseline alignment keeps it on
   the first line, so a value carrying a unit under it does not drag it down.
   Margins stay at zero: the synthesized baseline is then the dotted edge. */
.dot-leader {
    flex: 1;
    align-self: baseline;
    border-bottom: 1px dotted var(--ap-border);
}

/* Shared click-to-load map surface: hours/panel and every map variant draw the
   same frame, so the RGPD-sensitive placeholder has one implementation. */
.map-canvas {
    display: block;
    height: 16rem;
    border: 1px solid var(--ap-border);
    border-radius: var(--ap-radius);
    background-image:
        repeating-linear-gradient(0deg, var(--ap-border) 0, var(--ap-border) 1px, transparent 1px, transparent 2.75rem),
        repeating-linear-gradient(90deg, var(--ap-border) 0, var(--ap-border) 1px, transparent 1px, transparent 2.75rem);
}

.map-canvas--flat {
    height: 7rem;
}

.map-canvas--tall {
    height: 20rem;
}

.map-canvas--bleed {
    height: 20rem;
}

.map-canvas--embed {
    position: relative;
    overflow: hidden;
}

.map-canvas--embed.is-loaded {
    background-image: none;
}

.map-canvas__load {
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: 0.5rem;
    width: 100%;
    height: 100%;
    padding: 1rem;
    border: 0;
    background: transparent;
    color: var(--ap-text);
    text-align: center;
}

.map-canvas__load:hover {
    background: rgba(var(--ap-accent-rgb), 0.08);
}

.map-canvas__notice {
    max-width: 24rem;
    color: var(--ap-muted);
    font-size: 0.75rem;
}

.map-canvas--embed iframe {
    display: block;
    width: 100%;
    height: 100%;
    border: 0;
}

.map-access {
    display: grid;
    gap: 1.25rem;
}

.map-access__item dt {
    font-family: var(--ap-font-mono);
    font-size: 0.7rem;
    letter-spacing: 0.12em;
    text-transform: uppercase;
    color: var(--ap-faint);
}

.map-access__item dd {
    margin: 0.35rem 0 0;
    color: var(--ap-muted);
}

.map-bleed__panel {
    margin-top: -2rem;
    margin-right: 1rem;
    margin-left: 1rem;
}

@media (min-width: 768px) {
    .map-access {
        grid-template-columns: repeat(auto-fit, minmax(12rem, 1fr));
    }
}

@media (min-width: 992px) {
    .map-canvas--tall {
        height: 100%;
        min-height: 24rem;
    }

    .map-canvas--bleed {
        height: 32rem;
    }

    .map-bleed {
        position: relative;
    }

    .map-bleed__panel {
        position: absolute;
        bottom: 2rem;
        left: 2rem;
        max-width: 26rem;
        margin: 0;
    }
}

.hours-contact {
    display: flex;
    flex-wrap: wrap;
    gap: 1.5rem;
    margin-top: 1rem;
    font-family: var(--ap-font-mono);
    font-size: 0.75rem;
    letter-spacing: 0.1em;
    text-transform: uppercase;
}

.contact-channel {
    display: flex;
    flex-direction: column;
    gap: 0.35rem;
    height: 100%;
    padding: 1.25rem;
    background: var(--ap-panel);
    border: 1px solid var(--ap-border);
    border-radius: var(--ap-radius);
    color: var(--ap-text);
    text-decoration: none;
    transition: border-color 0.2s ease, transform 0.2s ease;
}

.contact-channel:hover,
.contact-channel:focus-visible {
    border-color: var(--ap-accent);
    color: var(--ap-text);
    transform: translateY(-2px);
}

.contact-channel__kind {
    display: flex;
    align-items: center;
    gap: 0.5rem;
    font-family: var(--ap-font-mono);
    font-size: 0.7rem;
    letter-spacing: 0.12em;
    text-transform: uppercase;
    color: var(--ap-faint);
}

.contact-channel__label {
    font-family: var(--ap-font-display);
    font-size: 1.15rem;
    line-height: 1.3;
}

.contact-channel__detail {
    font-size: 0.9rem;
    color: var(--ap-muted);
}

/* The bar leaves the flow only on phones, where it is the block's whole point. */
.contact-bar {
    list-style: none;
    display: flex;
    gap: 0.75rem;
    margin: 0;
    padding: 0;
}

/* min-width:0 beats the flex-item default that would stop the label ellipsizing. */
.contact-bar__item {
    flex: 1 1 0;
    min-width: 0;
}

.contact-bar__link {
    display: flex;
    align-items: center;
    justify-content: center;
    gap: 0.5rem;
    padding: 0.85rem 1rem;
    background: var(--ap-accent);
    border: 1px solid var(--ap-accent);
    border-radius: var(--ap-radius);
    color: var(--ap-accent-contrast);
    font-family: var(--ap-font-mono);
    font-size: 0.75rem;
    letter-spacing: 0.1em;
    text-transform: uppercase;
    text-decoration: none;
    min-width: 0;
}

.contact-bar__kind {
    display: none;
}

.contact-bar__link:hover,
.contact-bar__link:focus-visible {
    background: var(--ap-accent-strong);
    border-color: var(--ap-accent-strong);
    color: var(--ap-accent-contrast);
}

.contact-bar__item:nth-child(n + 2) .contact-bar__link {
    background: var(--ap-panel);
    border-color: var(--ap-border);
    color: var(--ap-text);
}

.contact-bar__item:nth-child(n + 2) .contact-bar__link:hover,
.contact-bar__item:nth-child(n + 2) .contact-bar__link:focus-visible {
    border-color: var(--ap-accent);
    color: var(--ap-text);
}

@media (max-width: 767.98px) {
    .contact-bar {
        position: fixed;
        z-index: 1030;
        right: 0;
        bottom: 0;
        left: 0;
        gap: 0.5rem;
        padding: 0.6rem 0.75rem calc(0.6rem + env(safe-area-inset-bottom));
        background: var(--ap-bg);
        border-top: 1px solid var(--ap-border);
    }

    .contact-bar__link {
        gap: 0.4rem;
        padding: 0.85rem 0.5rem;
        font-size: 0.7rem;
        letter-spacing: 0.06em;
    }

    /* The number is unreadable at a third of a phone width, so the canal names it. */
    .contact-bar__label {
        display: none;
    }

    .contact-bar__kind {
        display: block;
        overflow: hidden;
        text-overflow: ellipsis;
        white-space: nowrap;
    }
}

/* ---- Opening status ---- */
.opening-now {
    display: flex;
    flex-direction: column;
    gap: 1rem;
}

.opening-now__eyebrow {
    font-family: var(--ap-font-mono);
    font-size: 0.65rem;
    letter-spacing: 0.14em;
    text-transform: uppercase;
    color: var(--ap-muted);
}

/* The visitor came for this line, so it outweighs the sentence naming the next
   change rather than sitting under it as a caption. */
.opening-state {
    display: inline-flex;
    align-items: center;
    align-self: flex-start;
    gap: 0.6rem;
    margin: 0;
    padding: 0.6rem 1.1rem;
    border: 1px solid var(--ap-border);
    border-radius: 100px;
    font-family: var(--ap-font-mono);
    font-size: 0.85rem;
    letter-spacing: 0.1em;
    text-transform: uppercase;
    color: var(--ap-text);
}

.opening-state__dot {
    width: 0.5rem;
    height: 0.5rem;
    border-radius: 50%;
    background: currentColor;
}

/* An accent fill is only guaranteed to read against --ap-accent-contrast (ADR-0005). */
.opening-state--open {
    border-color: var(--ap-accent);
    background: var(--ap-accent);
    color: var(--ap-accent-contrast);
}

.opening-state--closing-soon {
    border-color: var(--ap-accent);
    color: var(--ap-accent-text);
}

.opening-now__headline {
    margin: 0;
    font-family: var(--ap-font-display);
    font-size: clamp(1.35rem, 4vw, 1.75rem);
    line-height: 1.15;
}

.opening-closure {
    display: flex;
    flex-direction: column;
    gap: 0.35rem;
    margin: 0;
    padding: 0.9rem 1rem;
    border: 1px solid var(--ap-border);
    border-radius: var(--ap-radius);
    background: var(--ap-panel);
    font-size: 0.9rem;
}

.opening-closure__eyebrow {
    font-family: var(--ap-font-mono);
    font-size: 0.6rem;
    letter-spacing: 0.12em;
    text-transform: uppercase;
    color: var(--ap-muted);
}

.opening-week__row {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 1rem;
    padding: 0.75rem 0;
    border-bottom: 1px solid var(--ap-border);
}

.opening-week__row:last-child {
    border-bottom: 0;
}

.opening-week__day {
    display: flex;
    align-items: center;
    gap: 0.6rem;
}

.opening-week__hours {
    font-family: var(--ap-font-mono);
    font-size: 0.75rem;
    letter-spacing: 0.04em;
    text-align: right;
}

.opening-week__row--closed .opening-week__day,
.opening-week__row--closed .opening-week__hours {
    color: var(--ap-faint);
}

/* The swapped body pair is the only combination an inverted surface promises. */
.opening-week__row--today {
    margin: 0 -0.9rem;
    padding: 0.75rem 0.9rem;
    border-bottom-color: transparent;
    border-radius: var(--ap-radius);
    background: var(--ap-text);
    color: var(--ap-bg);
    font-weight: 600;
}

.opening-week__row--today.opening-week__row--closed .opening-week__day,
.opening-week__row--today.opening-week__row--closed .opening-week__hours {
    color: color-mix(in srgb, var(--ap-bg) 78%, var(--ap-text));
}

.opening-week__marker {
    width: 0.45rem;
    height: 0.45rem;
    border-radius: 50%;
    background: currentColor;
}

/* ---- Services and price cards ---- */
.price-list__head {
    display: flex;
    flex-wrap: wrap;
    align-items: baseline;
    justify-content: space-between;
    gap: 0.5rem 1rem;
    padding-bottom: 0.75rem;
    border-bottom: 1px solid var(--ap-border);
    margin-bottom: 0.5rem;
}

.price-list__item {
    padding: 1rem 0;
    border-bottom: 1px solid var(--ap-border);
}

.price-list__line {
    display: flex;
    align-items: baseline;
    gap: 0.75rem;
}

.price-list__name {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 0.5rem;
    font-weight: 600;
}

.price-list__badge {
    padding: 0.15rem 0.5rem;
    border: 1px solid var(--ap-border);
    border-radius: var(--ap-radius);
    font-family: var(--ap-font-mono);
    font-size: 0.6rem;
    letter-spacing: 0.1em;
    text-transform: uppercase;
    color: var(--ap-accent-text);
}

.price-list__price {
    flex: none;
    text-align: right;
    font-family: var(--ap-font-mono);
    color: var(--ap-accent-text);
}

.price-list__description {
    max-width: 48ch;
    margin-top: 0.25rem;
    font-size: 0.925rem;
}

.price-list__facts {
    margin: 1.5rem 0;
    padding-top: 1.25rem;
    border-top: 1px solid var(--ap-border);
}

.price-list__fact {
    display: flex;
    align-items: baseline;
    justify-content: space-between;
    gap: 1rem;
    padding: 0.4rem 0;
}

.price-card {
    background: var(--ap-panel);
    border: 1px solid var(--ap-border);
    border-radius: var(--ap-radius);
    overflow: hidden;
}

.price-card__media {
    position: relative;
}

.price-card__media .image-frame,
.price-card__media .site-image {
    border-width: 0 0 1px;
}

.price-card__flag {
    position: absolute;
    top: 0.6rem;
    left: 0.6rem;
    padding: 0.2rem 0.55rem;
    background: var(--ap-accent);
    color: var(--ap-accent-contrast);
    font-family: var(--ap-font-mono);
    font-size: 0.6rem;
    letter-spacing: 0.1em;
    text-transform: uppercase;
}

.price-card__body {
    padding: 1.1rem 1.25rem 1.25rem;
}

.price-card__price {
    padding-top: 0.75rem;
    border-top: 1px solid var(--ap-border);
    font-family: var(--ap-font-mono);
    color: var(--ap-accent-text);
}

/* ---- Team ---- */
.team-member__bio {
    max-width: 28ch;
    margin-inline: auto;
}

/* The name sits under a rectangular crop rather than beside a round one, so it
   reads as a caption for the picture above it. */
.team-card__name {
    font-family: var(--ap-font-display);
    font-size: 1.05rem;
    font-weight: 600;
}

/* One line per person: the portrait is a thumbnail beside the text rather than
   the head of a card, so a page « à propos » holds a whole team without a grid. */
.team-list__row {
    display: grid;
    grid-template-columns: 5rem 1fr;
    gap: 1.5rem;
    padding: 1.6rem 0;
    border-top: 1px solid var(--ap-border);
}

.team-list__row:last-child {
    border-bottom: 1px solid var(--ap-border);
}

@media (min-width: 768px) {
    .team-list__row {
        grid-template-columns: 7rem 1fr;
        gap: 2rem;
    }
}

.team-member__portrait {
    width: min(11rem, 60%);
    margin: 0 auto 1.25rem;
}

.frame-round,
.team-member__portrait .site-image {
    aspect-ratio: 1 / 1;
    min-height: 0;
    border-radius: 50%;
}

/* ---- Showcase ---- */
/* Stacked réalisations read as one list, so each carries its own bottom rule. */
.showcase-item {
    padding-bottom: 2.5rem;
    border-bottom: 1px solid var(--ap-border);
}

/* The crop follows the column the frame lands in, so the plank keeps its weight
   whatever the count: the span is what changes, in the template. */
.showcase-frames .image-frame,
.showcase-frames .site-image {
    aspect-ratio: 4 / 3;
    height: auto;
    min-height: 0;
    object-fit: cover;
}

/* A third of the plank is a narrow column, so the triptych stands its frames up:
   square on its own line, and at lg, where the story sits beside them, as tall as
   the story itself. The floor and the cap are in cqw so they follow the frame's
   own width: never flatter than square, never more upright than 4:5. */
@media (min-width: 768px) {
    .showcase-frames--alone {
        justify-content: flex-end;
    }

    .showcase-frames--triptych .image-frame,
    .showcase-frames--triptych .site-image {
        aspect-ratio: 1 / 1;
    }
}

@media (min-width: 992px) {
    .blk--reversed .showcase-frames--alone {
        justify-content: flex-start;
    }

    .showcase-frames--triptych {
        height: 100%;
    }

    .showcase-frames--triptych > * {
        container-type: inline-size;
    }

    .showcase-frames--triptych .image-frame,
    .showcase-frames--triptych .site-image {
        aspect-ratio: auto;
        height: 100%;
        min-height: 100cqw;
        max-height: 125cqw;
    }
}

/* The paragraph sits beside a portrait, so it keeps a reading measure of its
   own: half a wide band is far past the line length prose is read at. */
.content-split__body {
    max-width: 52ch;
}

/* The splits read left by default, so centering is the side they override. The
   body keeps its 52ch measure, which has to be centered as a box of its own. */
.blk--align-center .content-split__text {
    text-align: center;
}

.blk--align-center .content-split__body {
    margin-inline: auto;
}

/* ---- Content stack ---- */
/* Centered unless the block's alignment setting says otherwise. The default
   lives here rather than on a text-center utility in the template: a utility is
   !important and the wrapper class below could never beat it. */
.content-stack {
    text-align: center;
}

/* The auto end margin moves the column, not just its text: .row centers it with
   justify-content-center, so aligning the text alone would leave the block
   indented from both gutters. The reading measure is kept on purpose. */
.blk--align-start .content-stack {
    margin-inline-end: auto;
    text-align: left;
}

/* The image is contained, never cropped: capping it at the reading column is
   what lets this block promise three fields and no layout choice. */
.content-stack__media {
    margin-inline: auto;
    max-width: 560px;
}

/* The same cap pulled to the left edge: aligning the block changes where the
   picture sits, never its size. */
.blk--align-start .content-stack__media {
    margin-inline: 0;
}

.content-stack--medallion .content-stack__media {
    max-width: 11rem;
}

/* An empty slot has no image to give it a height; the medallion takes its own
   from .frame-round, so it must not inherit this one. */
.content-stack__media .image-frame {
    min-height: 220px;
}

.content-stack--medallion .content-stack__media .image-frame {
    min-height: 0;
}

/* The holding page shown for a tenant whose site is not public yet. */
.holding-page {
    max-width: 640px;
    margin: 0 auto;
    padding-top: 10vh;
}

/* ---- Announcement ---- */
/* The band bleeds full width and the note stays in the measure, so only the
   framing differs: the row inside is one shared partial. */
.announce__row {
    display: flex;
    flex-direction: column;
    align-items: flex-start;
    gap: 0.5rem 1.25rem;
}

.announce__tag {
    flex: none;
    font-family: var(--ap-font-mono);
    font-size: 0.7rem;
    letter-spacing: 0.1em;
    text-transform: uppercase;
}

.announce__message {
    flex: 1 1 auto;
    margin: 0;
    text-wrap: pretty;
}

.announce__cta {
    flex: none;
    font-family: var(--ap-font-mono);
    font-size: 0.8rem;
    letter-spacing: 0.08em;
    text-transform: uppercase;
    text-decoration: underline;
    text-underline-offset: 0.25em;
}

/* Editor-only: an expired annonce keeps its place in the builder so the tenant
   can see why it left the site. */
.announce__expired {
    flex: none;
    font-family: var(--ap-font-mono);
    font-size: 0.7rem;
    letter-spacing: 0.08em;
    opacity: 0.75;
}

.announce--band.announce--accent {
    background-color: var(--ap-accent);
    color: var(--ap-accent-contrast);
}

.announce--band.announce--neutral {
    background-color: var(--ap-text);
    color: var(--ap-bg);
}

.announce--band .announce__cta {
    color: inherit;
}

.announce--note {
    padding: 1.25rem;
    background-color: var(--ap-panel);
    border: 1px solid var(--ap-border);
    border-radius: var(--ap-radius);
}

.announce--note.announce--accent {
    border-left: 3px solid var(--ap-accent);
}

.announce--note .announce__tag {
    color: var(--ap-faint);
}

.announce--note .announce__cta {
    color: var(--ap-accent-text);
}

@media (min-width: 768px) {
    .announce__row {
        flex-direction: row;
        align-items: baseline;
    }
}

/* ---- Before / after ---- */
/* Both frames stay on one row at every width: a comparison read one image at a
   time is not a comparison. A fixed ratio on both sides is what makes the two
   photos comparable when their crops differ. */
/* minmax(0, …) rather than 1fr: a grid track sized to a replaced element takes
   its minimum from the file's intrinsic width, so a 2000px photo would push the
   row past the viewport. */
.before-after__pair {
    display: grid;
    grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
    gap: 0.5rem;
}

.before-after__side {
    position: relative;
    margin: 0;
}

.before-after__side .image-frame,
.before-after__side .site-image {
    aspect-ratio: 4 / 3;
    min-height: 0;
}

.before-after__tag {
    position: absolute;
    top: 0.6rem;
    left: 0.6rem;
    padding: 0.2rem 0.55rem;
    background: var(--ap-panel);
    color: var(--ap-text);
    border: 1px solid var(--ap-border);
    font-family: var(--ap-font-mono);
    font-size: 0.6rem;
    letter-spacing: 0.1em;
    text-transform: uppercase;
}

.before-after__tag--after {
    background: var(--ap-accent);
    border-color: var(--ap-accent);
    color: var(--ap-accent-contrast);
}

.before-after__case + .before-after__case {
    margin-top: 2.5rem;
    padding-top: 2.5rem;
    border-top: 1px solid var(--ap-border);
}

.before-after__facts {
    display: grid;
    gap: 1rem;
    margin-top: 2rem;
    padding-top: 1.25rem;
    border-top: 1px solid var(--ap-border);
}

.before-after__slider {
    --ba-split: 50%;
    position: relative;
    aspect-ratio: 16 / 9;
    overflow: hidden;
}

@media (max-width: 575.98px) {
    .before-after__slider {
        aspect-ratio: 4 / 3;
    }
}

.before-after__slider .image-frame,
.before-after__slider .site-image {
    position: absolute;
    inset: 0;
    width: 100%;
    height: 100%;
}

.before-after__reveal {
    position: absolute;
    inset: 0;
    clip-path: inset(0 calc(100% - var(--ba-split)) 0 0);
}

.before-after__slider .before-after__tag--after {
    left: auto;
    right: 0.6rem;
}

.before-after__handle,
.before-after__range {
    display: none;
}

.before-after__slider.is-interactive .before-after__handle {
    display: block;
    position: absolute;
    top: 0;
    bottom: 0;
    left: var(--ba-split);
    width: 2px;
    margin-left: -1px;
    background: var(--ap-bg);
}

.before-after__knob {
    position: absolute;
    top: 50%;
    left: 50%;
    display: grid;
    place-items: center;
    width: 2.75rem;
    height: 2.75rem;
    transform: translate(-50%, -50%);
    border: 2px solid var(--ap-bg);
    border-radius: 50%;
    background: var(--ap-accent);
    color: var(--ap-accent-contrast);
    font-family: var(--ap-font-mono);
}

.before-after__slider.is-interactive .before-after__range {
    display: block;
    position: absolute;
    inset: 0;
    width: 100%;
    height: 100%;
    margin: 0;
    padding: 0;
    appearance: none;
    background: transparent;
    opacity: 0;
    cursor: ew-resize;
}

/* The range itself is transparent, so the ring goes on the knob. Light border
   then dark outline, because neither token is guaranteed against a photograph. */
.before-after__slider:has(.before-after__range:focus-visible) .before-after__knob {
    outline: 2px solid var(--ap-text);
    outline-offset: 2px;
}

/* Arrows would need framework JS on a public page (ADR-0007), so the scroll is
   the control and a peeking next card is the affordance. */
.before-after__series {
    display: grid;
    grid-auto-flow: column;
    grid-auto-columns: 88%;
    gap: 1.5rem;
    overflow-x: auto;
    overscroll-behavior-x: contain;
    scroll-snap-type: x proximity;
    padding-bottom: 1rem;

    /* A drawn track, because the platform hides the overlay one until a scroll
       starts: the peeking card says there is more, this says how much more. */
    scrollbar-width: thin;
    scrollbar-color: var(--ap-border) transparent;
}

.before-after__card {
    min-width: 0;
    scroll-snap-align: start;
}

@media (min-width: 576px) {
    .before-after__facts {
        grid-template-columns: repeat(3, minmax(0, 1fr));
    }
}

@media (min-width: 768px) {
    .before-after__series {
        grid-auto-columns: 58%;
    }
}

@media (min-width: 992px) {
    .before-after__series {
        grid-auto-columns: 42%;
    }
}

/* The prestations are what the block is selling, so they read as a list on a
   surface rather than as loose rows under a centered title. */
.resa-services {
    list-style: none;
    margin: 0;
    padding: 0.5rem 1.25rem;
    background: var(--ap-panel);
    border: 1px solid var(--ap-border);
    border-radius: var(--ap-radius);
}

.resa-services__row {
    display: flex;
    justify-content: space-between;
    gap: 1.5rem;
    padding: 0.85rem 0;
}

.resa-services__row + .resa-services__row {
    border-top: 1px solid var(--ap-border);
}

.resa-services__meta {
    color: var(--ap-muted);
    white-space: nowrap;
}


/* ---- Module pages: the shared header ---- */
/* Back link, title, standfirst and meta, then a neutral rule: one skeleton for a form's
   page and for the booking ones, painted with the existing tokens. */
.module-head {
    display: flex;
    flex-direction: column;
    align-items: flex-start;
    gap: 0.75rem;
    padding-bottom: 1.75rem;
    margin-bottom: 2.5rem;
    border-bottom: 1px solid var(--ap-border);
}

.module-head__back {
    font-family: var(--ap-font-mono);
    font-size: 0.72rem;
    letter-spacing: 0.14em;
    text-transform: uppercase;
    color: var(--ap-muted);
}

.module-head__intro {
    max-width: 58ch;
    margin: 0;
    color: var(--ap-muted);
    text-wrap: pretty;
}

.module-head__meta {
    display: flex;
    flex-wrap: wrap;
    gap: 0.5rem;
    margin: 0.25rem 0 0;
}

.module-head__tag {
    padding: 0.4rem 0.75rem;
    border: 1px solid var(--ap-border);
    border-radius: var(--ap-radius-button);
    font-family: var(--ap-font-mono);
    font-size: 0.7rem;
    letter-spacing: 0.1em;
    text-transform: uppercase;
    color: var(--ap-faint);
}

.module-body {
    max-width: 45rem;
    margin-inline: auto;
}


/* ---- Public flash banner ---- */
/* Pinned to the bottom of the viewport: in the flow it would sit between the navbar
   and the first block and break the .site-nav + .blk adjacency the full-screen hero
   is promoted by. The container takes no pointer events, only the alert does. */
.site-flash {
    position: fixed;
    inset-inline: 0;
    bottom: 0;
    z-index: 1040;
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 0.5rem;
    padding: 0 1rem 1.25rem;
    pointer-events: none;
}

.site-flash .alert {
    width: min(100%, 34rem);
    pointer-events: auto;
    box-shadow: 0 6px 24px rgb(0 0 0 / 18%);
}

/* ---- Newsletter: the page section ---- */
/* No Bootstrap .input-group: its radii come from a selector too specific to
   override without !important, and the field welded to the button falls to a
   few characters on a phone. The flex-basis wraps the button to the next line
   before the address becomes unreadable. */
.newsletter-blk__group {
    display: flex;
    flex-wrap: wrap;
    gap: 0.75rem;
}

.newsletter-blk__group > .form-control {
    flex: 1 1 14rem;
}

.newsletter-blk__text {
    max-width: 46ch;
    color: var(--ap-muted);
}

/* Tied to the field above it rather than hung off the bottom of the section. */
.newsletter-blk__note {
    margin-top: 0.6rem;
    font-size: 0.8rem;
    color: var(--ap-faint);
}

/* Nothing on the ink surface keeps a token measured against --ap-bg. */
.newsletter-band .label,
.newsletter-band .newsletter-blk__text,
.newsletter-band .newsletter-blk__note {
    color: inherit;
    opacity: 0.78;
}

/* The quiet variant: an accent edge so the panel reads as a section rather
   than a grey box, and a measure so the field does not span it end to end. */
.newsletter-blk--stacked {
    border-top: 2px solid var(--ap-accent);
}

.newsletter-blk--stacked .newsletter-blk__text {
    margin-inline: auto;
}

.newsletter-blk__stack {
    width: min(30rem, 100%);
    margin-inline: auto;
}

.newsletter-blk--stacked .newsletter-blk__group {
    justify-content: center;
}

.newsletter-blk--stacked .newsletter-blk__note {
    text-align: center;
}

/* ---- Newsletter: the footer column ---- */
/* The field is welded to the button, so only the outer corners are shaped: Bootstrap
   already squares the joint, from a selector specific enough to hold against this one.
   They follow the button token, not --ap-radius, because the pair reads as one control. */
.newsletter .input-group > .form-control {
    border-start-start-radius: var(--ap-radius-button);
    border-end-start-radius: var(--ap-radius-button);
}

/* ---------- Products ---------- */

.products-head {
    display: flex;
    flex-direction: column;
    align-items: flex-start;
    gap: 1rem;
    margin-bottom: 2rem;
}

.products-head__title {
    min-width: 0;
}

.products-head__cta {
    flex: none;
}

/* Arrows would need framework JS on a public page (ADR-0007), so the scroll is
   the control and the card cut by the right edge is the affordance: the rail
   bleeds past the container padding it sits in. */
.products-rail,
.rayons-rail {
    display: grid;
    grid-auto-flow: column;
    gap: 1.5rem;
    overflow-x: auto;
    overscroll-behavior-x: contain;
    scroll-snap-type: x proximity;
    margin-right: -1.5rem;
    padding-right: 1.5rem;
    padding-bottom: 1rem;
}

.products-rail {
    grid-auto-columns: 62%;
}


/* A rayon card is the same object at every width, a square photo over one line
   of text, so its track is a constant: sized as a share of the viewport it
   shrank to a thumbnail on a phone. Only the peek stays proportional. */
.rayons-rail {
    grid-auto-columns: min(20rem, 85%);
}

.products-rail__card,
.rayons-rail__card {
    min-width: 0;
    scroll-snap-align: start;
}

/* Two tracks on a phone and four from lg, with the first rayon on a quarter of the
   mosaic. Explicit tracks rather than auto-fit: a spanning child and an auto-fit
   grid disagree about how many columns there are, and the tile lands on its own row. */
.rayons-bento {
    display: grid;
    grid-template-columns: repeat(2, minmax(0, 1fr));
    gap: clamp(0.875rem, 1.6vw, 1.25rem);
}

.rayons-bento__tile {
    position: relative;
    display: flex;
    align-items: flex-end;
    min-height: 14rem;
    overflow: hidden;
    border-radius: var(--ap-radius);
    background: var(--ap-panel);
    color: #fff;
}

.rayons-bento__photo,
.rayons-bento .image-frame {
    position: absolute;
    inset: 0;
    width: 100%;
    height: 100%;
    object-fit: cover;
    transition: transform 0.8s cubic-bezier(0.2, 0.7, 0.2, 1);
}

.rayons-bento__tile:hover .rayons-bento__photo,
.rayons-bento__tile:focus-visible .rayons-bento__photo {
    transform: scale(1.04);
}

/* Fixed rather than themed: a scrim over a photograph is theme-invariant, which is
   also why the tile's own text is white in both schemes. */
.rayons-bento__scrim {
    position: absolute;
    inset: 0;
    background: linear-gradient(180deg, transparent 45%, rgb(0 0 0 / 72%) 100%);
}

.rayons-bento__body {
    position: relative;
    display: flex;
    flex-direction: column;
    gap: 4px;
    padding: clamp(1rem, 2vw, 1.5rem);
}

.rayons-bento__name {
    font-family: var(--ap-font-display);
    font-size: clamp(1.2rem, 1.8vw, 1.6rem);
    line-height: 1.1;
}

.rayons-bento__detail {
    font-size: 0.84rem;
    color: rgb(255 255 255 / 85%);
}

@media (min-width: 992px) {
    .rayons-bento {
        grid-template-columns: repeat(4, minmax(0, 1fr));
    }

    .rayons-bento__tile:first-child {
        grid-column: span 2;
        grid-row: span 2;
    }
}

/* The one product card, rendered by the produits block and by the boutique listings
   alike, so it lives here rather than in shop.css: that sheet is a shop-only entrypoint
   and a builder page never loads it.

   A panel fill with a themed edge, and the photograph bleeding to the corners. Bootstrap's
   own .card gives the box the page's background and a translucent black border that belongs
   to no palette, which left the outline doing all the separating.

   Three rows: the photo, the text, then the price. The middle row takes the slack, so a
   rail of cards and a grid of them both line their prices up whatever the names are long. */
.product-card {
    display: grid;
    /* Explicit, so a style can span its photograph across every row (the band does) and a
       long word cannot widen the column past its share. */
    grid-template-columns: minmax(0, 1fr);
    grid-template-rows: auto 1fr auto;
    height: 100%;
    overflow: hidden;
    border: 1px solid var(--ap-border);
    border-radius: var(--ap-radius);
    background-color: var(--ap-panel);
    color: var(--ap-text);
    text-decoration: none;
    transition: transform 0.5s cubic-bezier(0.2, 0.7, 0.2, 1), box-shadow 0.5s ease;
}

/* focus-within, not focus: the card is the link, and a keyboard lands on it. */
.product-card:hover,
.product-card:focus-within {
    color: var(--ap-text);
    transform: translateY(-5px);
    box-shadow: 0 30px 50px -30px rgb(0 0 0 / 35%);
}

.product-card__media {
    position: relative;
    display: block;
    overflow: hidden;
    aspect-ratio: 1 / 1;
    background-color: rgba(var(--ap-accent-rgb), 0.08);
}

/* No edge and no corner of its own: the card draws the one and clips the other. Without the
   radius reset, shop.css rounds every .site-image on the shopfront, which inside a framed card
   cuts two notches of panel out from under the photograph while the card's own corners are
   somewhere else entirely. Three classes because that sheet loads after this one. */
.product-card .product-card__media .site-image,
.product-card .product-card__media .image-frame {
    display: block;
    width: 100%;
    height: 100%;
    border: 0;
    border-radius: 0;
    object-fit: cover;
}

.product-card--sold-out .product-card__media {
    filter: grayscale(1);
    opacity: 0.65;
}

/* The marks sit in a row rather than one over the other, because a card can now carry two. */
.product-card__badges {
    position: absolute;
    top: 0.75rem;
    left: 0.75rem;
    display: flex;
    gap: 0.35rem;
}

/* Accent ink on the page's own background, which is the one pair the accent tokens
   are measured against (ADR-0005); over a photograph it needs that plate anyway. */
.product-card__badge {
    padding: 0.375rem 0.7rem;
    border-radius: 999px;
    background-color: rgba(var(--ap-bg-rgb), 0.9);
    color: var(--ap-accent-text);
    font-family: var(--ap-font-mono);
    font-size: 0.66rem;
    font-weight: 700;
    letter-spacing: 0.08em;
    text-transform: uppercase;
}

.product-card__badge--quiet {
    color: var(--ap-muted);
}

.product-card__text {
    display: flex;
    flex-direction: column;
    gap: 0.1875rem;
    min-width: 0;
    padding: 1rem 1.125rem 0;
}

.product-card__name {
    margin: 0;
    font-family: var(--ap-font-display);
    font-size: 1.1rem;
    line-height: 1.18;
}

/* The one line a card can carry besides its name and its price: « épuisé », « plus que 3 »,
   « téléchargement ». One phrase of its own making, so it needs no clamp. */
.product-card__detail {
    font-size: 0.84rem;
    color: var(--ap-faint);
}

/* Its own row under a hairline rather than a column beside the name: at five cards across
   the figure took a third of the width, broke the name a word to the line, and left the
   space under itself empty. */
.product-card__foot {
    display: flex;
    align-items: baseline;
    justify-content: space-between;
    gap: 0.75rem;
    margin: 0.7rem 1.125rem 0;
    padding: 0.6rem 0 1.125rem;
    border-top: 1px solid var(--ap-border);
}

.product-card__price {
    font-weight: 800;
    font-size: 0.95rem;
}

.product-card__from {
    font-weight: 400;
    color: var(--ap-muted);
}

/* The frameless styles, which are every style but the two framed ones and the band. */
.product-card--nue,
.product-card--vignette,
.product-card--planche {
    border-color: transparent;
    border-radius: 0;
    background-color: transparent;
}

/* The card is invisible there, so the photograph is the object and carries the theme's
   radius on all four of its own corners. Left on the card, the clip rounded the top two
   and left the bottom two square. */
.product-card--nue .product-card__media,
.product-card--vignette .product-card__media,
.product-card--planche .product-card__media {
    border-radius: var(--ap-radius);
}

/* None of the frameless styles lifts: a block with no edge rising five pixels off a background
   it shares reads as a mis-set photograph rather than a hover. The two whose photograph is the
   whole object zoom it under its own clip instead, so nothing around it moves. */
.product-card--nue:hover,
.product-card--nue:focus-within,
.product-card--vignette:hover,
.product-card--vignette:focus-within,
.product-card--planche:hover,
.product-card--planche:focus-within {
    transform: none;
    box-shadow: none;
}

.product-card--nue .product-card__media .site-image,
.product-card--planche .product-card__media .site-image {
    transition: transform 0.5s cubic-bezier(0.2, 0.7, 0.2, 1);
}

.product-card--nue:hover .product-card__media .site-image,
.product-card--nue:focus-within .product-card__media .site-image,
.product-card--planche:hover .product-card__media .site-image,
.product-card--planche:focus-within .product-card__media .site-image {
    transform: scale(1.03);
}

.product-card--nue .product-card__text {
    padding-right: 0;
    padding-left: 0;
}

.product-card--nue .product-card__foot {
    justify-content: flex-start;
    margin-right: 0;
    margin-left: 0;
    padding-bottom: 0;
    border-top: 0;
}

/* The vignette: the price rides on the photograph and the name takes one line under it, so
   the card is short enough that a long shelf is scanned rather than read. Media and price share
   the first grid row, which is what puts one over the other without a second template. */
.product-card--vignette {
    grid-template-rows: auto auto;
}

.product-card--vignette .product-card__media {
    grid-area: 1 / 1;
    border: 1px solid var(--ap-border);
}

/* Positioned, because the media it shares the cell with is: a static sibling paints under a
   positioned one whatever the source order says. */
.product-card--vignette .product-card__foot {
    position: relative;
    grid-area: 1 / 1;
    align-self: end;
    justify-self: start;
    margin: 0 0 0.5rem 0.5rem;
    padding: 0.3rem 0.55rem;
    border: 1px solid var(--ap-border);
    border-radius: var(--ap-radius-button);
    background-color: rgba(var(--ap-bg-rgb), 0.94);
}

.product-card--vignette .product-card__price {
    font-family: var(--ap-font-mono);
    font-weight: 600;
    font-size: 0.72rem;
}

.product-card--vignette .product-card__text {
    grid-area: 2 / 1;
    padding: 0.55rem 0 0;
}

.product-card--vignette .product-card__name {
    overflow: hidden;
    font-size: 0.92rem;
    line-height: 1.25;
    text-overflow: ellipsis;
    white-space: nowrap;
}

/* The plate: the photograph standing tall and the words centered under it, no card at all.
   It is the produits block's editorial variant, which now draws this card rather than a second
   copy of it, and a style any listing can choose. */
.product-card--planche {
    text-align: center;
}

.product-card--planche .product-card__text {
    padding: 0.85rem 0 0;
    gap: 0.35rem;
}

.product-card--planche .product-card__name {
    font-size: 1.28rem;
}

.product-card--planche .product-card__foot {
    justify-content: center;
    margin: 0.35rem 0 0;
    padding: 0;
    border-top: 0;
}

.product-card--planche .product-card__price {
    font-family: var(--ap-font-mono);
    font-weight: 600;
    font-size: 0.78rem;
    letter-spacing: 0.06em;
}

/* The band: the words move into the photograph, on a veil at its foot. Nothing outside the
   picture, so no text height has to be equalized and the grid is regular whatever the names
   are long. The photograph spans every row and the words take the last two, which is what
   stacks them over it without a second template.

   White on a scrim rather than a token, as elsewhere in this sheet: an accent has no contrast
   guarantee over a photograph, and this one is over whichever photograph the tenant picked. */
.product-card--bandeau {
    grid-template-rows: 1fr auto auto;
    border: 0;
    color: #fff;
}

.product-card--bandeau:hover,
.product-card--bandeau:focus-within {
    color: #fff;
}

.product-card--bandeau .product-card__media {
    grid-row: 1 / -1;
    grid-column: 1;
}

.product-card--bandeau .product-card__media::after {
    content: "";
    position: absolute;
    inset: 0;
    background: linear-gradient(to top, rgb(14 12 9 / 88%) 0%, rgb(14 12 9 / 55%) 34%, transparent 62%);
}

/* Positioned, because the photograph they sit on is: a static sibling paints under a
   positioned one whatever the source order says. */
.product-card--bandeau .product-card__text {
    position: relative;
    grid-row: 2;
    grid-column: 1;
    padding: 0 1.1rem;
}

/* Two lines here rather than as many as the name is long: the veil has a top, and a name
   that runs past it is a name on the photograph itself. */
.product-card--bandeau .product-card__name {
    display: -webkit-box;
    -webkit-line-clamp: 2;
    -webkit-box-orient: vertical;
    overflow: hidden;
    font-size: 1.06rem;
}

.product-card--bandeau .product-card__foot {
    position: relative;
    grid-row: 3;
    grid-column: 1;
    align-items: baseline;
    margin: 0;
    padding: 0.35rem 1.1rem 1rem;
    border-top: 0;
}

.product-card--bandeau .product-card__from,
.product-card--bandeau .product-card__detail {
    color: rgb(255 255 255 / 78%);
}

/* The portrait crop, wherever a style stands its photograph up. */
.product-card--portrait .product-card__media,
.product-card--planche .product-card__media,
.product-card--bandeau .product-card__media {
    aspect-ratio: 4 / 5;
}

@media (prefers-reduced-motion: reduce) {
    .product-card,
    .product-card .site-image {
        transition: none;
    }

    .product-card:hover,
    .product-card:focus-within,
    .product-card:hover .site-image,
    .product-card:focus-within .site-image {
        transform: none;
    }
}


/* The phone list. Below the sm breakpoint a catalogue set to « liste » draws its cards as rows,
   a thumbnail then the name then the figure, eight to a screen where the square card gives one.
   It is a phone layout and nothing else: above sm the tenant's own card style draws, untouched.

   Every selector below names .product-card twice on purpose. The card styles reach their own
   parts with two classes, so a rule of two classes here would only tie with them and win on
   source order; three settles it, whatever order the sheet ends up in. */
@media (max-width: 575.98px) {
    /* A list has no gutter between its rows: the rule under each one is the separation. */
    .product-grid--liste {
        --bs-gutter-y: 0;
    }

    .product-grid--liste .product-card {
        grid-template-rows: auto;
        grid-template-columns: auto minmax(0, 1fr) auto;
        align-items: center;
        gap: 0.875rem;
        padding: 0.75rem 0;
        border: 0;
        border-bottom: 1px solid var(--ap-border);
        border-radius: 0;
        background-color: transparent;
        color: var(--ap-text);
    }

    .product-grid--liste .product-card:hover,
    .product-grid--liste .product-card:focus-within {
        transform: none;
        box-shadow: none;
        border-bottom-color: var(--ap-accent);
        color: var(--ap-text);
    }

    .product-grid--liste .product-card .product-card__media {
        grid-area: auto;
        width: 76px;
        aspect-ratio: 1 / 1;
        border: 1px solid var(--ap-border);
        border-radius: var(--ap-radius);
    }

    /* The band paints a veil over its photograph for text that is no longer on it. */
    .product-grid--liste .product-card .product-card__media::after {
        content: none;
    }

    /* A pill in the corner of a 76px thumbnail is unreadable, so the mark becomes a strip
       across its foot. It has to stay legible: a listing that hides it sends the visitor to a
       product they cannot buy. */
    .product-grid--liste .product-card .product-card__badges {
        top: auto;
        right: 0;
        bottom: 0;
        left: 0;
    }

    /* One mark only, the first, which the card orders épuisé before the discount before the
       novelty: two across 76 px are two unreadable marks rather than one readable one. */
    .product-grid--liste .product-card .product-card__badge:not(:first-child) {
        display: none;
    }

    .product-grid--liste .product-card .product-card__badge {
        flex: 1;
        padding: 0.1rem 0.2rem;
        border-radius: 0;
        font-size: 0.48rem;
        letter-spacing: 0.02em;
        text-align: center;
    }

    .product-grid--liste .product-card .product-card__text {
        grid-area: auto;
        padding: 0;
        text-align: left;
    }

    .product-grid--liste .product-card .product-card__name {
        display: block;
        overflow: hidden;
        font-size: 1rem;
        text-overflow: ellipsis;
        white-space: nowrap;
    }

    .product-grid--liste .product-card .product-card__foot {
        flex-direction: column;
        grid-area: auto;
        align-items: flex-end;
        gap: 0.15rem;
        margin: 0;
        padding: 0;
        border: 0;
        border-radius: 0;
        background-color: transparent;
    }

    .product-grid--liste .product-card .product-card__price {
        font-family: var(--ap-font-body);
        font-weight: 800;
        font-size: 0.95rem;
        letter-spacing: normal;
    }

    .product-grid--liste .product-card .product-card__from,
    .product-grid--liste .product-card .product-card__detail {
        color: var(--ap-faint);
    }
}

.shop-compare-at {
    color: var(--ap-faint);
    font-size: 0.75em;
    font-weight: 400;
}

.products-selection__cta {
    display: flex;
    justify-content: center;
    margin-top: 2.5rem;
    padding-top: 2rem;
    border-top: 1px solid var(--ap-border);
}

@media (min-width: 576px) {
    .products-rail {
        grid-auto-columns: 38%;
    }
}

@media (min-width: 768px) {
    .products-head {
        flex-direction: row;
        align-items: flex-end;
        justify-content: space-between;
        gap: 1.5rem;
    }

    .products-rail {
        grid-auto-columns: 28%;
    }
}

@media (min-width: 992px) {
    .products-rail,
    .rayons-rail {
        margin-right: -3rem;
        padding-right: 3rem;
    }

    .products-rail {
        grid-auto-columns: 22%;
    }
}

/* Legal page: anchors styled as tabs. :target picks the visible
   section, so the links deep-link, survive JavaScript being off and stay
   indexable. Without :has() support every section shows, which is the safe
   way to fail on a page whose purpose is being readable. */
.legal-tabs {
    display: flex;
    flex-wrap: wrap;
    gap: 1.5rem;
    border-bottom: 1px solid var(--ap-border);
    margin-bottom: var(--ap-space-block);
}

.legal-tabs a {
    padding: 0.75rem 0;
    margin-bottom: -1px;
    border-bottom: 2px solid transparent;
    color: var(--ap-muted);
    text-decoration: none;
}

.legal-tabs a:hover,
.legal-tabs a:focus-visible {
    color: var(--ap-text);
}

.legal-section + .legal-section {
    margin-top: var(--ap-space-block);
}

.legal-block {
    color: var(--ap-muted);
}

.legal-table th[scope="row"] {
    white-space: nowrap;
}

@supports selector(:has(*)) {
    /* The fragment swaps the section in place, so it must not also scroll: a
       margin taller than the viewport puts the scroll target above the document
       top, where it clamps to zero. Only inside @supports, because the fallback
       stacks every section and there jumping to one is the right behavior. */
    .legal-section {
        scroll-margin-top: 100vh;
    }

    body:has(.legal-section:target) .legal-section {
        display: none;
    }

    body:has(.legal-section:target) .legal-section:target {
        display: block;
        margin-top: 0;
    }

    body:has(#mentions:target) .legal-tabs a[href="#mentions"],
    body:has(#donnees:target) .legal-tabs a[href="#donnees"],
    body:has(#cgv:target) .legal-tabs a[href="#cgv"],
    body:not(:has(.legal-section:target)) .legal-tabs a:first-child {
        color: var(--ap-text);
        border-bottom-color: var(--ap-accent);
    }

    body:not(:has(.legal-section:target)) .legal-section:not(:first-of-type) {
        display: none;
    }
}

/* ---------- Medallion hero ---------- */

/* The rule crosses the column and the medallion sits on it, so the image is
   held by the section instead of floating above the text. */
.hero-medallion {
    position: relative;
    display: flex;
    justify-content: center;
    align-self: stretch;
}

.hero-medallion::before {
    content: "";
    position: absolute;
    top: 50%;
    right: 0;
    left: 0;
    border-top: 1px solid var(--ap-border);
}

.hero-medallion__media {
    position: relative;
    width: min(420px, 78vw);
}

.hero-medallion__image {
    box-shadow: 0 1.25rem 2rem -0.75rem color-mix(in srgb, var(--ap-text) 30%, transparent);
}

/* ---------- Price bands ---------- */

/* The 1px gap on a border-colored grid draws the cell rules at any column count. */
.price-bands {
    display: grid;
    grid-template-columns: 1fr;
    gap: 1px;
    overflow: hidden;
    border: 1px solid var(--ap-border);
    border-radius: var(--ap-radius);
    background: var(--ap-border);
}

.price-bands__cell {
    display: flex;
    flex-direction: column;
    gap: 0.5rem;
    padding: 1.75rem 1.5rem;
    background: var(--ap-panel);
    color: var(--ap-text);
    text-decoration: none;
    transition: background-color 0.15s ease;
}

a.price-bands__cell:hover,
a.price-bands__cell:focus-visible {
    background: var(--ap-bg);
    color: var(--ap-text);
}

.price-bands__range {
    font-family: var(--ap-font-display);
    font-size: 1.9rem;
    line-height: 1;
    color: var(--ap-accent-text);
}

.price-bands__title {
    margin-top: 0.25rem;
    font-weight: 700;
}

.price-bands__body {
    font-size: 0.9rem;
}

.price-bands__count {
    margin-top: auto;
    padding-top: 0.75rem;
}

@media (min-width: 768px) {
    .price-bands {
        grid-template-columns: repeat(2, 1fr);
    }

    /* An odd last cell spans the row, so the grid never shows an empty slot. */
    .price-bands__cell:last-child:nth-child(odd) {
        grid-column: 1 / -1;
    }
}

@media (min-width: 992px) {
    .price-bands--3 {
        grid-template-columns: repeat(3, 1fr);
    }

    .price-bands--4 {
        grid-template-columns: repeat(4, 1fr);
    }

    .price-bands--3 .price-bands__cell:last-child:nth-child(odd),
    .price-bands--4 .price-bands__cell:last-child:nth-child(odd) {
        grid-column: auto;
    }
}

/* ---------- Glossary ---------- */

.glossary-term {
    display: grid;
    grid-template-columns: 6rem 1fr;
    gap: 1.25rem;
    align-items: start;
    padding: 1.35rem 1.5rem;
    background: var(--ap-panel);
    border: 1px solid var(--ap-border);
    border-radius: var(--ap-radius);
}

/* Sized for the eight-character sigle the form allows; wrapping is the last resort. */
.glossary-term__code {
    font-family: var(--ap-font-display);
    font-size: 1.35rem;
    line-height: 1.1;
    color: var(--ap-accent-text);
    overflow-wrap: anywhere;
}

.glossary-point {
    padding-top: 1rem;
    border-top: 1px solid var(--ap-border);
}

/* ---------- Rayon chips ---------- */

/* Under 992px the chips have the whole width and no column to wrap into: eight
   rayons stack into four rows and push the framed rayon off the screen. One
   scrolling line instead, bleeding past the container padding like the rails. */
.rayon-chips {
    display: flex;
    gap: 0.6rem;
    overflow-x: auto;
    overscroll-behavior-x: contain;
    margin-right: -1.5rem;
    padding-right: 1.5rem;
    padding-bottom: 0.75rem;
}

.rayon-chips > li {
    flex: none;
}

.rayon-chip {
    display: inline-flex;
    align-items: center;
    gap: 0.5rem;
    padding: 0.6rem 1.1rem;
    border: 1px solid var(--ap-border);
    border-radius: var(--ap-radius-button);
    background: var(--ap-panel);
    color: var(--ap-muted);
    font-size: 0.85rem;
    text-decoration: none;
    transition: border-color 0.15s ease, color 0.15s ease;
}

.rayon-chip:hover,
.rayon-chip:focus-visible {
    border-color: var(--ap-accent);
    color: var(--ap-accent-text);
}

.rayon-chip.active {
    border-color: var(--ap-accent);
    background: var(--ap-accent);
    color: var(--ap-accent-contrast);
    font-weight: 600;
}

.rayon-chip__count {
    font-family: var(--ap-font-mono);
    font-size: 0.65rem;
    letter-spacing: 0.08em;
    opacity: 0.8;
}

.rayon-featured__facts {
    display: flex;
    flex-wrap: wrap;
    gap: 1rem 1.75rem;
    margin: 1.4rem 0 1.5rem;
    padding-top: 1.25rem;
    border-top: 1px solid var(--ap-border);
}

.rayon-featured__value {
    display: block;
    margin-bottom: 0.35rem;
    font-family: var(--ap-font-display);
    font-size: 1.5rem;
    line-height: 1;
}

/* The chips have their own column beside the framed rayon: they wrap into it
   rather than hide a tab behind a scroll gesture. */
@media (min-width: 992px) {
    .rayon-chips {
        flex-wrap: wrap;
        overflow-x: visible;
        margin-right: 0;
        padding-right: 0;
        padding-bottom: 0;
    }
}

/* Address suggestions. The list is built by address_autocomplete.js
   and never exists without it, so no rule here has a no-JavaScript counterpart. */
.address-autocomplete {
    position: relative;
}

.address-suggestions {
    position: absolute;
    z-index: 3;
    inset-inline: 0;
    top: calc(100% + 0.25rem);
    max-height: 15rem;
    overflow-y: auto;
    margin: 0;
    padding: 0.25rem;
    list-style: none;
    background-color: var(--ap-panel);
    border: 1px solid var(--ap-border);
    border-radius: var(--ap-radius);
    box-shadow: 0 6px 24px rgba(0, 0, 0, 0.18);
}

.address-suggestion {
    padding: 0.5rem 0.75rem;
    border-radius: var(--ap-radius);
    cursor: pointer;
    color: var(--ap-text);
}

.address-suggestion:hover,
.address-suggestion.is-active {
    background-color: color-mix(in srgb, var(--ap-accent) 14%, transparent);
}

/* The address register's advisory notice: it warns, it never refuses,
   so it reads as a remark rather than as a field error. */
.address-notice {
    margin-top: 0.25rem;
    color: var(--ap-muted);
}

.address-notice::before {
    content: '⚠ ';
}
