/* ==========================================================================
   bbb-theme — brand layer on top of the inherited o3-theme bundle.
   Loaded after out/o3-theme/src/css/main(.min).css, see tpl/layout/base.tpl.

   Why a plain CSS overlay instead of recompiling o3-theme's SCSS: the parent's
   green ($primary) is baked into exactly 24 compiled rules, all of which are
   Bootstrap component custom-property blocks. Redeclaring those is ~60 lines
   and stays correct across o3-theme updates, whereas forking build/scss/ would
   copy the parent's whole source tree into this repo.
   ponytail: CSS overlay, fork vendor/o3-shop/o3-theme/build/ into this theme
   and compile with gulp if the brand layer ever needs SCSS-level changes
   (grid/breakpoint/spacing scales, new components).

   Design tokens come from the mock-ups in html/ (Mehring GmbH).
   ========================================================================== */

/* Hanken Grotesk = display face of the brand. Self-hosted (SIL OFL 1.1) — never loaded
   from fonts.googleapis.com, the shop must not leak visitor IPs to Google.
   These are the designer's own files from html/fonts/, one file per weight; the theme
   previously carried a variable-axis subset pulled off gstatic. Latin subset, so German
   umlauts and ß are covered. Inter (body text) is not declared here: o3-theme already
   ships and self-hosts it. */
@font-face {
    font-family: "Hanken Grotesk";
    src: url("../font/hanken-grotesk-v12-latin-regular.woff2") format("woff2");
    font-weight: 400;
    font-style: normal;
    font-display: swap;
}

@font-face {
    font-family: "Hanken Grotesk";
    src: url("../font/hanken-grotesk-v12-latin-600.woff2") format("woff2");
    font-weight: 600;
    font-style: normal;
    font-display: swap;
}

@font-face {
    font-family: "Hanken Grotesk";
    src: url("../font/hanken-grotesk-v12-latin-700.woff2") format("woff2");
    font-weight: 700;
    font-style: normal;
    font-display: swap;
}

@font-face {
    font-family: "Hanken Grotesk";
    src: url("../font/hanken-grotesk-v12-latin-800.woff2") format("woff2");
    font-weight: 800;
    font-style: normal;
    font-display: swap;
}

:root {
    --bbb-navy: #00284d;
    --bbb-navy-dark: #001b35;
    --bbb-orange: #ff6b00;
    --bbb-orange-dark: #d95f00;
    --bbb-ice: #e1ffff;
    --bbb-surface: #f7f9fb;
    --bbb-surface-low: #f2f4f6;
    --bbb-border: #e2e8f0;
    /* The mock-ups' three text tones, which were literals in a dozen rules until now:
       `body { color: #191c1e }` (html/index.html), the `.text-muted` hung on every
       secondary line — rgba(33, 37, 41, .75) in Bootstrap 5.3 — and #43474e, which
       Wk2_2.html names --text-on-surface-variant and uses for form labels and long copy. */
    --bbb-text: #191c1e;
    --bbb-text-muted: rgba(33, 37, 41, 0.75);
    --bbb-text-variant: #43474e;
    --bbb-font-display: "Hanken Grotesk", sans-serif;

    /* Material Symbols "info" (outlined, weight 300 — QM 2026-09-04: unfilled icons; the mock-up
       renders the same glyph, as a font). Inlined as a mask so it takes
       currentColor and needs no font from fonts.googleapis.com — the shop must not leak
       visitor IPs to Google, the same rule the self-hosted webfonts follow. */
    --bbb-icon-info: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 -960 960 960'%3E%3Cpath d='M450-290h60v-230h-60v230Zm52.92-307.75q9.39-9.29 9.39-23.02t-9.29-23.02q-9.29-9.28-23.02-9.28t-23.02 9.28q-9.29 9.29-9.29 23.02t9.39 23.02q9.38 9.29 22.92 9.29 13.54 0 22.92-9.29ZM480.07-100q-78.84 0-148.21-29.92t-120.68-81.21q-51.31-51.29-81.25-120.63Q100-401.1 100-479.93q0-78.84 29.92-148.21t81.21-120.68q51.29-51.31 120.63-81.25Q401.1-860 479.93-860q78.84 0 148.21 29.92t120.68 81.21q51.31 51.29 81.25 120.63Q860-558.9 860-480.07q0 78.84-29.92 148.21t-81.21 120.68q-51.29 51.31-120.63 81.25Q558.9-100 480.07-100Zm-.07-60q134 0 227-93t93-227q0-134-93-227t-227-93q-134 0-227 93t-93 227q0 134 93 227t227 93Zm0-320Z'/%3E%3C/svg%3E");

    /* Height of the revealed header search field, and of the strip it sits in — one is the
       other plus the panel's 1rem top and bottom padding. Declared here rather than at the
       search rules because the room made for the strip is set on `<main>`, which is the
       header's *sibling*: a custom property inherits down from :root, never sideways. */
    --bbb-search-field-h: 46px;
    --bbb-search-strip: calc(var(--bbb-search-field-h) + 2rem);

    /* Bootstrap only writes --bs-primary from the theme setting (see base.tpl);
       the -rgb triplet and the focus ring stay green unless set here too. */
    --bs-primary: var(--bbb-navy);
    --bs-primary-rgb: 0, 40, 77;
    --bs-focus-ring-color: rgba(0, 40, 77, 0.25);
    --bs-link-color: var(--bbb-navy);
    --bs-link-color-rgb: 0, 40, 77;
    --bs-link-hover-color: var(--bbb-orange);
    --bs-link-hover-color-rgb: 255, 107, 0;
    --bs-btn-focus-shadow-rgb: 0, 40, 77;
    --bs-border-color: var(--bbb-border);

    /* o3-theme's body text is #121212; the mock-up's is #191c1e. It was the one colour of
       the design we never took, so everything that inherits — body copy, the top-level nav
       labels — has been two shades off all along. */
    --bs-body-color: var(--bbb-text);
    --bs-body-color-rgb: 25, 28, 30;
}

/* --------------------------------------------------------------------------
   Full-bleed bands
   --------------------------------------------------------------------------
   o3-theme stretches three backgrounds past the container with a `width: 100vw`
   pseudo-element and a hand-computed negative `left`:

       left: calc((-100vw + <container>rem - <gutter>) / 2)

   That subtracts the grid gutter once too often, so the band lands 15px too far
   left: it hangs 15px past the left edge and stops 15px short of the right one.
   On the parent's near-white palette nobody notices; with a navy top bar it is a
   white sliver in the top-right corner, and the left overhang is what made the
   page ~15px wider than the viewport.

   `left/right: calc(50% - 50vw)` with `width: auto` is the exact form: 50% resolves
   against the positioned ancestor (the container-width band itself), so the offset
   to each viewport edge is measured, not guessed, at any container width. */
.start__manufacturer::before {
    left: calc(50% - 50vw);
    right: calc(50% - 50vw);
    width: auto;
}

/* The top info bar cannot use that pseudo-element at all in this theme. o3-theme's works
   only because nothing in the header has a background: a `z-index: -1` child paints behind
   EVERY ancestor background up to the nearest stacking context, so the moment this theme
   painted the header white (the mock-up's header is white on an off-white page) the navy
   bleed vanished and the bar ended at the container edge.

   Making <header> a stacking context would fix the painting order but breaks something
   worse: oxwMiniBasket renders #offcanvas__basket inside <header> while Bootstrap appends
   its backdrop to <body>, so the backdrop (z-index 1040) would paint over the trapped panel.

   So the bar bleeds itself — negative inline margins out to the viewport, matching inline
   padding to put its content back on the container edge. Percentages here resolve against
   the container's content box, so both halves are measured, not guessed. */
.header__topinfo {
    /* Sized explicitly rather than left to flex-grow: the parent's `flex: 1 1 100%` plus
       negative margins made the grown width come out 165px short. With the width pinned and
       growth switched off, outer width is exactly the container's, so it still takes its own
       line above the logo row. */
    flex: 0 0 auto;
    width: 100vw;
    margin-inline: calc(50% - 50vw);
    padding-inline: calc(50vw - 50%);
}

/* `content: none`, not `display: none`: the bar bleeds itself now (see above), so this
   pseudo-element should not exist at all rather than exist and be told not to paint. */
.header__topinfo::before {
    content: none;
}

/* Companion to every vw-based bleed above: 100vw counts the classic scrollbar that
   the layout viewport excludes, so on a scrollbar-drawing browser the bands would
   overhang half a scrollbar each side and re-introduce a horizontal scroll. `clip`
   rather than `hidden` because it does not create a scroll container, so
   position: sticky keeps working inside it. */
body {
    overflow-x: clip;
}

/* --------------------------------------------------------------------------
   Measure
   --------------------------------------------------------------------------
   The mock-ups force `.container { max-width: 1140px !important }`; o3-theme lays
   everything out in `.container-xxl`, which is 1320px. That 180px is the single
   biggest reason the page reads differently even when every element matches: at
   1320px the columns are ~22% wider, so cards get bigger while the type stays the
   same size and the whole page feels looser than the design.

   One rule, and it applies shop-wide — which is right, the mock-ups use the same
   measure on every page. The one-row header still fits: 1140px of container leaves
   ~1110px of content against the ~975px that logo + nav + search + actions need. */
.container-xxl,
/* o3-theme's `sEnable3xl` option widens the container to 1650px from 1720px viewports via
   `body.xxxl-enabled .container-xxl` (specificity 0,2,0 — it beat the rule above on wide
   monitors). The option is off in theme.php; this keeps the measure even if it is toggled. */
body.xxxl-enabled .container-xxl {
    max-width: 1140px;
}

/* o3-theme throws away the container's own side padding on the header and main containers:

       body > header > .container-xxl,
       body > main   > .container-xxl { padding-inline: var(--content-inner-margin, 0) }

   and `--content-inner-margin` is only ever set for the boxed `body.has-bg-image` layout, so
   on this shop it resolves to 0. Above the container's max-width nobody notices, because
   there is whitespace outside it anyway — but on a phone the container edge *is* the viewport
   edge, so every heading and paragraph sat flush against the screen. The mock-up insets
   everything (12px, Bootstrap's default gutter half).

   Restored via the container's own gutter rather than a literal, so it tracks
   `--bs-gutter-x` and cancels the `.row` negative margins exactly. That is also what the
   footer's container has had all along — it is not a child of <main>, which is why the
   footer looked right on mobile while the page above it did not.

   Same specificity as the parent's rule, and this file loads after main.min.css, so the tie
   resolves here. The full-bleed bands are unaffected: they measure with `calc(50% - 50vw)`,
   where the 50% resolves against whatever the container's content box is. */
body > header > .container-xxl,
body > main > .container-xxl {
    padding-inline: calc(var(--bs-gutter-x) * 0.5);
}

/* sBackgroundColor is now the mock-up's off-white, but o3-theme also paints the header
   from --page-bg, and the mock-up's header is white. Keep the two apart.

   `.header__search-button` used to be in here too; it is now the mock-up's navy pill button
   (see "Search bar" below) and paints itself. */
.header__mainnav {
    background-color: #fff !important;
}

header {
    background-color: #fff;
}

/* --------------------------------------------------------------------------
   Sticky header — the mock-up's `<header class="mh-header bg-white sticky-top shadow-sm
   border-bottom">`, i.e. `position: sticky; top: 0; z-index: 1020` plus Bootstrap's
   shadow-sm and a hairline.

   One structural difference to work around: in the mock-up the navy utility bar is a
   **sibling before** <header>, so it scrolls away and only the white row pins. o3-theme puts
   it *inside* the header. Rather than restructure the template, the header sticks at
   `top: -36px` from md up — it scrolls up by exactly the bar's height and then pins, so the
   pinned strip is the white row alone, as in the mock-up. Below md the bar is not rendered
   at all, so there the offset has to be 0 or 36px of the logo row would be cut off.

   Sticky always creates a stacking context, so any offcanvas panel rendered inside <header>
   paints at the header's z-index — see the note on .header__topinfo above. That is why the
   category nav is a plain in-place collapse here and not a slide-in panel.

   `body { overflow-x: clip }` above is what keeps this working — `clip` does not create a
   scroll container, where `hidden` would have made the sticky never stick. */
header {
    position: sticky;
    top: 0;
    z-index: 1020;
    /* Shadow only, no hairline. The mock-up's header carries `shadow-sm border-bottom`, but its
       hero is a photo with a light top edge — on this shop the header sits directly above the
       **navy** hero band, where any hairline reads as a stray seam between white and navy
       instead of an edge. The shadow gives the separation the design is after. */
    box-shadow: 0 2px 4px rgba(0, 0, 0, 0.075);
}

/* `:not(.is-minimal)` matters: header.tpl renders the utility bar only when it renders the
   search, which is exactly when the header is not minimal. On the reduced checkout header
   there is no bar to scroll away, so -36px would slice 36px off the logo row instead. */
@media (min-width: 768px) {
    header:not(.is-minimal) {
        top: -36px;
    }
}

/* Mock-up leaves 39px under the nav labels and 37px under the logo; o3-theme leaves 7px and
   5px, which is what made the row look cramped against the content below it. Added on the
   container so it clears whichever of logo/nav/actions is tallest, rather than on one of them.

   QM: "Mittig, oben und unten gleich viel Platz." The space *above* the logo row is not padding
   at all — the utility bar is a flex item in this same container, so it is the wrap gap, and
   o3-theme's `gap: 15px` was answering a 32px padding below: 20px over the logo against 37px
   under it. Both sides are now 1.5rem, which puts the wordmark at 29/29 and keeps the white
   band within a few px of the mock-up's 113. `row-gap`, not `gap` — the 15px column gap is what
   separates logo, nav and actions across the row and has nothing to do with this.

   Only the full header: `header.is-minimal > .container-xxl` carries o3-theme's own 17px block
   padding at a higher specificity, and the checkout header is symmetric already (22/22).

   Kept in a custom property because the mega menu has to subtract it again — see
   `.megamenu { top: ... }` below. */
header > .container-xxl {
    --bbb-header-pad-b: 1.5rem;
    padding-bottom: var(--bbb-header-pad-b);
    row-gap: var(--bbb-header-pad-b);
}

/* Below lg the utility bar is not rendered (`d-none d-lg-flex`), so there is no wrap gap to
   carry the space above the logo and it sat flush against the top of the screen — the same
   imbalance, one breakpoint down. Padding, since here it is the container's own top edge; above
   lg it would push the navy bar down instead, that bar being a flex item of this container. */
@media (max-width: 991.98px) {
    header > .container-xxl {
        padding-top: var(--bbb-header-pad-b);
    }
}

/* --------------------------------------------------------------------------
   Typography — body stays Inter (parent default), headings go display face.
   -------------------------------------------------------------------------- */
h1, h2, h3, h4, h5, h6,
.h1, .h2, .h3, .h4, .h5, .h6,
.footer__box-title,
.component__productbox-title {
    font-family: var(--bbb-font-display);
    font-weight: 600;
}

/* o3-theme's section headings are 700; the mock-up's are 600. Worth matching — at
   32px the extra weight is what makes our headings look heavier than the design.
   (The mock-up's h1 computes to 300, but it requests Hanken Grotesk with only
   wght@600;700;800 from Google, so 300 falls back to 600 — the *rendered* weight is
   600 and this theme's self-hosted variable font must not take the 300 literally.) */
.title-big,
.start__sectionhead h2 {
    font-weight: 600;
}

/* --------------------------------------------------------------------------
   Bootstrap components that carry the parent's green as a literal value.
   -------------------------------------------------------------------------- */
/* QM: "Das Hover ist nicht orange. Jeder Button hovert orange." — and the mock-ups agree:
   `.btn-primary:hover` and `.btn-outline-primary:hover` both go to `--industrial-orange`
   (#FF6B00, our --bbb-orange) in Produktuerbersicht.html. The navy-on-navy hover this had was
   the odd one out, which is why the buttons the checkout and basket recolour by hand — Weiter,
   Warenkorb, the account navi — all already hover orange. One value here, not eight overrides.

   The accent-orange buttons keep their darker hover (--bbb-orange-dark): they are orange
   already, so orange-on-orange would be no hover at all. */
.btn-primary {
    --bs-btn-color: #fff;
    --bs-btn-bg: var(--bbb-navy);
    --bs-btn-border-color: var(--bbb-navy);
    --bs-btn-hover-color: #fff;
    --bs-btn-hover-bg: var(--bbb-orange);
    --bs-btn-hover-border-color: var(--bbb-orange);
    --bs-btn-focus-shadow-rgb: 0, 40, 77;
    --bs-btn-active-color: #fff;
    --bs-btn-active-bg: var(--bbb-orange-dark);
    --bs-btn-active-border-color: var(--bbb-orange-dark);
    --bs-btn-disabled-color: #fff;
    --bs-btn-disabled-bg: var(--bbb-navy);
    --bs-btn-disabled-border-color: var(--bbb-navy);
}

.btn-outline-primary {
    --bs-btn-color: var(--bbb-navy);
    --bs-btn-border-color: var(--bbb-navy);
    --bs-btn-hover-color: #fff;
    --bs-btn-hover-bg: var(--bbb-orange);
    --bs-btn-hover-border-color: var(--bbb-orange);
    --bs-btn-focus-shadow-rgb: 0, 40, 77;
    --bs-btn-active-color: #fff;
    --bs-btn-active-bg: var(--bbb-orange-dark);
    --bs-btn-active-border-color: var(--bbb-orange-dark);
    --bs-btn-disabled-color: var(--bbb-navy);
    --bs-btn-disabled-border-color: var(--bbb-navy);
}

/* The orange is the accent for promotional CTAs and badges, never a surface. */
.btn-accent {
    --bs-btn-color: #fff;
    --bs-btn-bg: var(--bbb-orange);
    --bs-btn-border-color: var(--bbb-orange);
    --bs-btn-hover-color: #fff;
    --bs-btn-hover-bg: var(--bbb-orange-dark);
    --bs-btn-hover-border-color: var(--bbb-orange-dark);
    --bs-btn-focus-shadow-rgb: 255, 107, 0;
}

.dropdown-menu,
.dropdown-menu-dark {
    --bs-dropdown-link-active-bg: var(--bbb-navy);
}

.nav-pills {
    --bs-nav-pills-link-active-bg: var(--bbb-navy);
}

.pagination {
    --bs-pagination-active-bg: var(--bbb-navy);
    --bs-pagination-active-border-color: var(--bbb-navy);
    --bs-pagination-focus-box-shadow: 0 0 0 0.25rem rgba(0, 40, 77, 0.25);
}

.list-group {
    --bs-list-group-active-bg: var(--bbb-navy);
    --bs-list-group-active-border-color: var(--bbb-navy);
}

.progress,
.progress-stacked {
    --bs-progress-bar-bg: var(--bbb-navy);
}

.accordion {
    --bs-accordion-btn-focus-box-shadow: 0 0 0 0.25rem rgba(0, 40, 77, 0.25);
}

.btn-close {
    --bs-btn-close-focus-shadow: 0 0 0 0.25rem rgba(0, 40, 77, 0.25);
}

.form-control:focus,
.form-select:focus,
.form-check-input:focus,
.nav-link:focus-visible {
    box-shadow: 0 0 0 0.25rem rgba(0, 40, 77, 0.25);
    border-color: var(--bbb-navy);
}

/* o3-theme squares every checkbox (`.form-check-input[type=checkbox] { border-radius: 0 }`);
   the mock-up keeps Bootstrap's rounded 0.25em. Nick, 2026-09-03, on step 4's AGB box:
   "a little bit round edges". Theme-wide on purpose: it is one control, not one page. */
.form-check-input[type=checkbox] {
    border-radius: 0.25em;
}

.form-check-input:checked,
.form-check-input[type=checkbox]:indeterminate,
.form-range::-webkit-slider-thumb,
.form-range::-moz-range-thumb {
    background-color: var(--bbb-navy);
    border-color: var(--bbb-navy);
}

.component__tobasket-minus,
.component__tobasket-plus {
    color: var(--bbb-navy);
}

.alist__orga-subcats .btn.active,
.alist__orga-subcats .btn[aria-current=page] {
    background: rgba(0, 40, 77, 0.08);
}

/* QM: "Hover ist auf dieser Seite immer der Orangeton, hier ist das Hover blau." o3-theme
   fills the category chip with --bs-primary on hover, which this theme sets to the brand navy —
   so the one place the chips could signal interaction came out the same colour as the headings.
   Orange is this theme's interaction colour everywhere else (nav labels, mega menu links,
   footer links), and the chips are the outlier. */
.alist__orga-subcats .btn:hover,
.alist__orga-subcats .btn:focus-visible {
    --chip-border: var(--bbb-orange);
    --chip-fg: #fff;
    background: var(--bbb-orange);
}

/* o3-theme paints the green into a few inline SVGs (manufacturers.tpl,
   product/tobasket.tpl) as presentation attributes. Matching on the literal
   colour recolours those without copying the templates. */
[fill="#82BA00"] {
    fill: var(--bbb-navy);
}

[stroke="#82BA00"] {
    stroke: var(--bbb-navy);
}

/* --------------------------------------------------------------------------
   Header — the top info strip is the mock-up's navy utility bar.
   sAccentColor stays the light "ice blue" because o3-theme reuses it for
   alerts, the manufacturer band and slider pagination, so the bar is recoloured
   here instead of through the theme setting.
   -------------------------------------------------------------------------- */
.header__topinfo {
    background-color: var(--bbb-navy) !important;
    color: #fff;

    /* Measured off the mock-up's `.mh-utility-bar`, which the parent's defaults overshot:
       13px text in a 36px bar with 16px icons; o3-theme inherits the 16px body size into a
       fixed 40px bar and sizes the icons at 20px, which made the strip read as a second
       navigation rather than a fine print line. */
    font-size: 13px;
    /* Mock-up's `.utility-bar` is weight 500; at 400 the thin white text reads dim on navy. */
    font-weight: 500;
    height: 36px;
}

/* The phone icon is inline SVG in header.tpl; the e-mail one is an <img> carrying
   width/height attributes from the o3headerTopInfo CMS content, so it has to be resized
   here rather than in the markup — otherwise editing that content resets the size. */
.header__topinfo svg,
.header__topinfo img {
    width: 16px;
    height: 16px;
}

.header__topinfo a,
.header__topinfo .btn {
    color: #fff;
}

.header__topinfo a:hover,
.header__topinfo .btn:hover {
    color: var(--bbb-orange);
}

.header__topinfo svg [stroke]:not([stroke="none"]) {
    stroke: currentColor;
}

/* Wishlist/basket counters are the mock-up's orange pills; o3-theme ties them
   to --bs-primary, which is now the navy and would vanish against the icons. */
.badge-counter {
    background-color: var(--bbb-orange);
}

/* --------------------------------------------------------------------------
   Search bar — the mock-up's `.mh-search-input-group`
   (html/assets/css/shared-components.css).

   Mock-up shape: one full pill field with a round navy button floating inside its right
   edge, not the two attached segments o3-theme builds out of `.input-group`. Its values are
   taken over verbatim — 46px field, 2rem radius, 1.25rem/3rem padding, #E2E8F0 border,
   36px button inset 5px, navy → orange on hover, and the 3px navy focus ring. The colours
   and the height are already tokens (--bbb-border / --bbb-navy / --bbb-orange /
   --bbb-search-field-h) holding exactly those values, so they are written as tokens.

   The panel is a wrapper this theme adds around the field + submit button
   (tpl/widget/header/search.tpl) so the pair can be revealed as one unit; here it doubles as
   the mock-up's positioning context. It is the `.input-group` child, so Bootstrap's
   `.input-group > *` radius and margin resets never reach the field or the button inside it —
   which is what lets the pill survive without fighting them.
   -------------------------------------------------------------------------- */
.header__search-panel {
    position: relative;
    /* Hidden until the magnifier is pressed (`.is-open` is set by the widget's own script). Was
       `flex` here and hidden again inside the xl block; now that the toggle is the control at
       every width, the closed state is the default one. */
    display: none;
    flex: 1 1 auto;
    min-width: 0;
}

.header__search.is-open .header__search-panel {
    display: flex;
}

.header__search-panel > .form-control {
    flex: 1 1 auto;
    min-width: 0;
    height: var(--bbb-search-field-h);
    padding-left: 1.25rem;
    padding-right: 3rem;
    border: 1px solid var(--bbb-border);
    border-radius: 2rem;
    /* The mock-up declares no fill, i.e. Bootstrap's white. o3-theme tints the header field
       #f4f8f9, which on the white strip reads as a grey bar. */
    background-color: #fff;
}

.header__search-panel > .form-control:focus {
    border-color: var(--bbb-navy);
    box-shadow: 0 0 0 3px rgba(0, 40, 77, 0.1);
    outline: none;
}

/* Selector carries the panel only to out-specify Bootstrap's `.input-group .btn
   {position:relative}`, which would otherwise keep the button in flow (0,2,0 beats 0,1,0)
   and leave it sitting beside the field instead of inside it. */
.header__search-panel > .header__search-button {
    position: absolute;
    right: 5px;
    top: 50%;
    transform: translateY(-50%);
    display: flex;
    align-items: center;
    justify-content: center;
    width: 36px;
    height: 36px;
    padding: 0;
    border: none;
    border-radius: 50%;
    /* `!important` only because o3-theme's own bundle carries
       `.header__search-button{background-color:var(--page-bg)!important}` and loads first —
       same specificity, so nothing but importance can beat it. */
    background: var(--bbb-navy) !important;
    color: #fff;
    transition: background 0.2s ease;
}

.header__search-button:hover {
    background: var(--bbb-orange) !important;
}

/* The glyph is an inline SVG with a hard-coded near-black stroke (o3-theme's, kept in
   tpl/widget/header/search.tpl); a CSS `stroke` beats the presentation attribute, so the
   icon follows the button's own colour on both states without touching the template. */
.header__search-button svg {
    height: 18px;
    width: 18px;
}

.header__search-button svg path {
    stroke: currentColor;
}

/* The magnifier is the search control at EVERY width now — Nick, on the phone: "on mobile the
   nav needs to be like this in icons (mainly search, the other 2 are icons)". Below xl the
   collapsed panel used to carry a full-width open field on a row of its own above the account and
   basket glyphs, so the menu had two icons and one field where the design has three icons.

   It used to be xl-only, which is why this rule and the panel's `display: none` below moved up
   out of the xl block: one behaviour, two placements for the *opened* field (an absolute strip at
   xl, in flow below it — see both media blocks).

   Not done with d-none/d-xl-flex: o3-theme purges unused utilities out of main.min.css — the
   bundle the productive storefront loads — and no o3 template uses an xl display utility, so all
   d-xl-* classes are missing from it. */
.header__search-toggle {
    display: flex;
    align-items: center;
    justify-content: center;
    color: var(--bbb-navy);
    background: transparent;
    border: 0;
}

.header__search-toggle:hover {
    color: var(--bbb-orange);
}

/* Main nav: uppercase, tracked out, orange on hover (mock-up navbar).
   Tokens read off the mock-up's `.mh-nav-link`: 14px / 600 / uppercase / 0.05em tracking /
   padding 0.5rem 1rem. o3-theme's `.navbar-nav` sets `--bs-nav-link-padding-x: 0` and the
   weight comes out 400, which made the labels read as body text and packed them ~16px
   tighter per side than the design. */
.header__mainnav .nav-link,
.header__mainnav > .container-xxl > ul > li > a {
    font-family: var(--bbb-font-display);
    font-size: 14px;
    font-weight: 600;
    letter-spacing: 0.05em;
    text-transform: uppercase;
    /* Not inherited: o3-theme pins `--bs-nav-link-color: #121212` (main.css:4440), so the new
       body colour does not reach these. The mock-up's `.mh-nav-link` is #191c1e. Hover keeps
       o3-theme's orange — its `:hover` rule out-specifies this one. */
    color: var(--bbb-text);
}

/* QM: "Logo zu klein". o3-theme caps `.header__logo` at 150px, which renders the 500x179
   mark 54px tall — the mock-up (html/index.html) sets the header logo to height="70",
   i.e. 196px wide at this aspect. */
.header__logo {
    max-width: 196px;
}

/* The one-row header's width budget, since three QM points all spend from it.

   The container is capped at 1140px, so from 1170px up the row always has the SAME 1110px of
   content — widening the window never buys space. Measured against the shop's five top-level
   categories: logo 196 + nav + magnifier 23 + actions 93 + gaps.

   The mock-up fits its nav in 690px; ours needs more for the same five labels because o3-theme
   puts a caret on *every* item with children, where the mock-up draws one on two of five. That
   is correct here — all five shop categories do have children — so the caret is made cheap
   instead (see .header__mainnav svg below) rather than the nav spacing being cut.

   Horizontal padding only: the vertical 0.875rem is what o3-theme measures the nav row and the
   megamenu's `top: 100%` from, and the row's height is set by the logo beside it anyway. */
.header__mainnav .navbar-nav > .nav-item > .nav-link {
    /* QM: "Abstände sind anders" — the mock-up's `.mh-nav-link` is `padding: 0.5rem 1rem`. This
       sat at 0.5rem purely to buy wrap safety, which the nav's `flex: 1 1 0` now provides
       structurally (see the media query below). */
    padding-inline: 1rem;
    /* The nav box is the row's leftover, so it can end up a few px under the labels' natural
       width; without this "Über Uns" breaks onto a second line and doubles the header's height. */
    white-space: nowrap;
    /* Caret cost, paid five times: o3-theme's is an 11px SVG with a 5px flex gap in front of it.
       Bootstrap's own `.dropdown-toggle::after` — which is what the mock-up's carets are — is
       0.6em wide with a 0.255em margin, i.e. ~12px all in. Matching that frees ~20px of the
       budget above. */
    gap: 3px;
}

/* Colour follows the label so caret and text turn orange together on hover. */
.header__mainnav .navbar-nav > .nav-item > .nav-link > svg {
    width: 9px;
    height: 9px;
    color: inherit;
}

.header__mainnav a:hover {
    color: var(--bbb-orange);
}

/* --------------------------------------------------------------------------
   Mega menu panel — the mock-up's `.mh-mega-menu`.

   o3-theme paints the panel with `.megamenu::before`, and a late rule in its own bundle
   overrides that pseudo to `background: var(--page-bg); border-radius: 0`. Since pass 3h set
   the page background to the mock-up's off-white, the panel became a square off-white slab
   floating on the white header — no card, no radius. Restored to the mock-up's surface:
   near-opaque white, 8px radius, one soft deep shadow.
   -------------------------------------------------------------------------- */
.megamenu::before {
    background: rgba(255, 255, 255, 0.98);
    border-radius: 0.5rem;
    border: 0;
    box-shadow: 0 1rem 3rem rgba(0, 0, 0, 0.175);
}

/* The mock-up's panel has no pointer triangle. o3-theme's is drawn from --page-bg too, so it
   would now read as an off-white notch cut out of a white card. `content: none` so it is never
   generated, rather than generated and hidden. */
.megamenu::after {
    content: none;
}

/* Panel spans the container, as in the mock-up (`width: 100%; max-width: 1140px`, centred).

   Two things forced this rather than the parent's content-width panel, and both are the same
   root cause — the parent's grid is `repeat(auto-fit, minmax(180px, max-content))` inside a
   panel with `width: max-content`, and auto-fit needs a *definite* width to decide a column
   count:
     1. With no definite width it always resolved to one column, so Verbrauchsmaterial's 11
        subcategories were a single column **583px tall**.
     2. Widening the panel to fit columns then made it grow rightwards from its own item —
        Ersatzteile ran to x=1410 on a 1440 viewport, 135px past the container.
   A container-width panel fixes both at once: the grid gets a definite width and spreads, and
   the panel can never stick out however wide its content is. It also matches the mock-up
   literally, which is the tie-breaker.

   Positioning context moves to the header's own container, so **three** ancestors between the
   panel and that container have to give up theirs — miss any one and the panel silently spans
   the nav (762px) instead of the container:
     - `.header__mainnav`   — o3-theme sets `position: relative`
     - `nav.navbar`         — *Bootstrap's* own `position: relative`, the easy one to overlook
     - the `<li>`           — o3-theme's `.mega-dropdown`
   NOT as safe as this comment first claimed: `.header__mainnav::after` (the nav hairline, already
   switched off here) was not the only thing anchored to those ancestors. o3-theme also puts the
   top-level active/hover underline on the `<li>` — see the rule right below, which had to be
   dealt with. Below xl the whole nav is `display: none` behind the burger. */
header > .container-xxl {
    position: relative;
}

.header__mainnav,
.header__mainnav nav.navbar,
.header__mainnav .navbar-nav > .nav-item {
    position: static;
}

/* --- Fallout from that `position: static`, and my own regression ------------
   o3-theme marks the current and hovered top-level item with an underline drawn on the
   **`<li>`**:

     [data-level=is-level-1].nav-item::after { content:""; position:absolute; bottom:8px;
                                               left:0; right:0; height:2px;
                                               background-color:transparent }
     …nav-item.active::after, …nav-item:hover::after { background-color: var(--bs-primary) }

   With the `<li>` made static for the container-wide panel, that `left: 0; right: 0` no longer
   resolves against the item — it resolves against the header's container. So on any page where a
   nav item is `.active` (i.e. inside a category, which includes every product page) it painted a
   **2px navy bar 1140px wide** across the whole nav row, and hovering any label did the same.
   The start page never shows it because no item is active there, which is why it survived the
   earlier hairline sweep.

   Removed with `content: none` so it is never generated, not merely painted transparent.

   The indicator itself is not lost: the mock-up does not underline nav items at all, it marks
   them with the accent colour, so the active item takes that instead. Hover already did. */
.header__mainnav .navbar-nav > .nav-item::after {
    content: none;
}

.header__mainnav .navbar-nav > .nav-item.active > a {
    color: var(--bbb-orange);
}

/* Second selector repeats the parent's own, which right-anchors the last two items' panels —
   irrelevant now that every panel spans the container, but it would otherwise win on
   specificity and leave those two unstretched. */
.megamenu,
.header__mainnav .navbar-nav > .nav-item:nth-last-child(-n + 2) > .megamenu {
    left: 0;
    right: 0;
    width: auto;
    max-width: none;
    /* THE DROPDOWN CLOSED ITSELF, and this is why: o3-theme positions the panel with
       `top: 100%`, which used to mean "just under the nav item" — but once the containing block
       became the header's container, 100% became the container's *padding box*, i.e. 32px of
       header padding lower. That left a 39px dead strip between the item and the panel: moving
       the pointer down crossed non-hoverable space, `.mega-dropdown:hover` ended and the panel
       vanished before the sub-categories could be reached. o3-theme's 12px `--bridge` covers a
       12px gap, not 39.

       Subtracting the container's own bottom padding puts the panel back on the nav row's bottom
       edge, where the bridge does its job. Same custom property as the padding itself, so the two
       cannot drift apart. */
    top: calc(100% - var(--bbb-header-pad-b, 0px));
}

/* ...and the last 8px. `.header__mainnav` is shorter than the header row it sits in (the logo is
   the tallest item), so even with the panel on the row's bottom edge a strip remained between the
   *item* and the panel with nothing hoverable — enough for a diagonal mouse move to lose
   `.mega-dropdown:hover` and dismiss the panel.

   The nav item now reaches the row's bottom, so the pointer stays inside the hover target all the
   way down. `height: 100%` and not `align-self: stretch`: `.header__mainnav` is `display: block`,
   so its `nav.navbar` child is a block box and align-self does nothing to it — the chain stopped
   stretching there. Only at xl, which is the only width where this nav is rendered. */
@media (min-width: 1200px) {
    header:not(.is-minimal) .header__mainnav {
        align-self: stretch;
    }

    header:not(.is-minimal) .header__mainnav > nav,
    header:not(.is-minimal) .header__mainnav .navbar-collapse,
    header:not(.is-minimal) .header__mainnav .navbar-nav,
    header:not(.is-minimal) .header__mainnav .navbar-nav > .nav-item {
        height: 100%;
    }

    /* "Bündig": the nav labels sit on the wordmark's baseline, and the icons come with them.

       The whole right-hand side of the row drops by one number — the distance between the row's
       centre, where `align-items: center` puts everything, and the bottom of the wordmark's
       letters. The logo cannot meet the text half way: it stays centred for qms14's "oben und
       unten gleich viel Platz", and its ink ends 14.5px above the image box's bottom edge,
       because the `g` descender and the swash occupy that band (alpha profile of logo_bbb.png:
       the letters end at source row 90 of 127, rows 91..126 carry 76px of ink and nothing else).

       Hence 6px, measured rather than guessed: at the rendered 49.78px the letters end 35.3px
       into the image, i.e. 100.3 against a text baseline of 94.4.

       Labels move by padding, so the hover and underline target moves with them and the two
       values still sum to o3-theme's 28px — the row height and the mega menu's `top` are
       measured from that. Search and actions move by margin: doubled, because they are centred
       flex items and half of a top margin is what shifts a centred box.

       Both went the wrong way once before. Dropping the labels alone (a first pass at this same
       note) left them 6.6px below the icons standing next to them, which reads worse than the
       original misalignment — the mock-up keeps labels and icons on one line and lifts the LOGO
       instead (`transform: translateY(-15px)` on `.navbar-brand img`), which is this rule with
       the sign flipped and the logo's equal spacing spent.

       `:has(.header__mainnav)`, plus a `0px` fallback everywhere the property is read: the basket
       page keeps the full header class but renders no category nav at all, so there are no
       labels for the icons to line up with and they stay on the row's centre there. */
    header:not(.is-minimal):has(.header__mainnav) > .container-xxl {
        --bbb-navrow-drop: 6px;
    }

    header:not(.is-minimal) .header__mainnav .navbar-nav > .nav-item > .nav-link {
        display: flex;
        align-items: center;
        height: 100%;
        padding-block: calc(14px + var(--bbb-navrow-drop, 0px)) calc(14px - var(--bbb-navrow-drop, 0px));
    }

    /* Search and actions take the same drop, further down this file — both already have a
       `margin` there and a shorthand would reset whatever were set here. */
}

/* --------------------------------------------------------------------------
   Mega menu — opens on click, and shows the whole branch (QM, both points).

   o3-theme opens the panel from CSS alone: `.mega-dropdown:hover > .megamenu`, plus
   `:focus-within` so the keyboard can reach it. Both are switched off here and the panel is
   keyed to `.is-open`, which the script at the bottom of tpl/widget/header/categorylist.tpl
   toggles on click. `:focus-within` has to go too or tabbing onto the nav item would still
   open the panel, which is the hover behaviour by another name.
   -------------------------------------------------------------------------- */
.mega-dropdown:hover > .megamenu,
.mega-dropdown:focus-within > .megamenu {
    display: none;
}

.mega-dropdown.is-open > .megamenu {
    display: block;
    animation: megamenu-reveal 0.18s ease-out;
}

/* Same for the caret flip — it belongs to the open state now, not to the pointer. */
.mega-dropdown:hover > .nav-link svg,
.mega-dropdown:focus-within > .nav-link svg {
    transform: none;
}

.mega-dropdown.is-open > .nav-link svg {
    transform: rotate(180deg);
}

/* o3-theme dims the page from backdrop.js, which adds `.backdrop` to <main> on mouseenter and
   drops it on mouseleave. With the panel no longer following the pointer that would dim on a
   hover that opens nothing, and un-dim while an opened panel is still up. Painted from the
   open panel itself instead, so the dim and the panel cannot disagree. */
main.backdrop::after {
    content: none;
}

/* xl only: below that the panel is an in-flow accordion inside the collapsed nav (see the
   max-width block further down), and dimming the page behind an element that pushes the page
   down instead of covering it just blacks out the whole phone screen. */
@media (min-width: 1200px) {
    body:has(.header__mainnav .mega-dropdown.is-open) main::after {
        content: "";
        position: absolute;
        top: 0;
        left: 0;
        width: 100%;
        height: calc(100% + 40px);
        background-color: rgba(0, 0, 0, 0.5);
        z-index: 10;
    }
}

/* Mock-up: 2.5rem panel padding, 3rem between columns.

   CSS multi-column, not the parent's Bootstrap row: the branches are wildly uneven — Maschinen
   is one group of 17, Verbrauchsmaterial is eleven groups of 2-9 — and a wrapping flex row makes
   every row as tall as its tallest cell, which leaves ragged holes. Columns balance instead.
   Long groups may split across tracks (`break-inside: auto` is the default) so a single 17-item
   branch fills the panel width rather than running 500px down one side; the heading is pinned to
   its first item with `break-after: avoid`. */
.megamenu__inner {
    display: flex;
    gap: 3rem;
    /* Designer: "Das Logo soll bündig zu den Kategorien liegen ... es soll auf einer Linie
       sein." The panel card spans the container's border box, so its 2.5rem padding put the
       first category 40px in, while the logo and every other content edge on the page sit on
       the container's own 15px gutter — three different left edges. Vertically it keeps the
       mock-up's 2.5rem; horizontally it now takes the container's gutter, so the columns start
       on the logo's line and the Bestseller card ends on the page's right edge. */
    padding: 2.5rem calc(var(--bs-gutter-x) * 0.5);
    /* With every level in the panel the biggest branch comes to 873px, and the panel hangs off a
       *sticky* header — anything past the viewport bottom cannot be scrolled to, it moves with the
       header. So the panel stops at the viewport and scrolls inside itself instead (10rem covers
       the header above it plus a little air). Measured on the panel, not guessed: 131px top +
       740px = 871 in a 900px window. `overscroll-behavior` keeps the page still once the panel's
       own scroll runs out. */
    max-height: calc(100vh - 10rem);
    overflow-y: auto;
    overscroll-behavior: contain;
}

.megamenu__groups {
    flex: 1 1 auto;
    min-width: 0;
    columns: 3;
    /* 2.5rem, not the mock-up's 3rem: at 3rem a column is 207px, and "Hydraulische
       Stapelschneider" needs 181 against the 179 a fifth-level row has left after two indents —
       it broke over two lines for two pixels. The narrower gutter buys each column 5px. */
    column-gap: 2.5rem;
    /* Not `stretch` (the flex default): with the panel's height capped above, a stretched multicol
       box gets a *definite* height, and a multicol that runs out of block space fragments sideways
       — a fourth column appeared on top of the Bestseller card instead of the panel scrolling.
       At `flex-start` the box keeps its natural height, three columns stay three, and the overflow
       is vertical where .megamenu__inner can scroll it. */
    align-self: flex-start;
}

.megamenu__group {
    margin-bottom: 1.75rem;
}

/* QM: "Abstände einrücken und Border weg" — the hairlines (a rule under every heading, a guide
   down every sub-list) drew a grid over a panel that already has three levels of indentation, so
   depth is carried by indent + weight alone now. */
.megamenu__grouptitle {
    display: block;
    margin-bottom: 0.5rem;
    padding-bottom: 0.4rem;
    /* Nick, trying it out: "nimm mal zum Test die underline bei der ersten Kategorie raus".
       The rule under every group heading is gone — the mock-up puts a plain Bootstrap
       `.border-bottom` there, but the heading is already the only 15px/800 navy line in its
       column, so the rule was saying a second time what the weight had said. Note it only ever
       spanned the TEXT, not the column: o3-theme's `.megamenu a { display: inline-block }` is
       (0,1,1) and outranks the `display: block` above, so each heading carried a different
       length of line — part of why the panel read as ruled rather than grouped.

       The `padding-bottom` stays: it is what keeps the heading off its first link, and dropping
       it with the border would close the gap by 7px in the same change. One line to put back. */
    break-after: avoid;
}

.megamenu__links li {
    margin-bottom: 0.5rem;
}

/* Third level and below (QM: a mega menu carries every level), one size down so depth reads
   without needing a second heading style.

   QM: "Bündig zu Neumaschinen" — the third level sits flush with the column heading, so only the
   levels BELOW it step in; its own 700 against the heading's 800 carries that step. And the gap
   above a sub-list is tighter than the one below it ("etwas weniger" / "etwas größer"), so a
   category and its children read as one block with air before the next category. */
.megamenu__links .megamenu__links {
    margin: 0.1rem 0 1.1rem;
    padding-left: 0.9rem;
}

.megamenu__links .megamenu__links a {
    font-size: 13px;
}

/* QM: the gap between a fourth-level title and its own list, down to what the third level has.
   It was 5.6px against 1.6px and not a margin at all — the label is an inline-block, so its li's
   line box adds the strut's descender space under it, and that strut is 4px taller than the 13px
   label needs. Zeroing the li's line-height drops the strut; the label carries its own
   line-height (from .megamenu a), so nothing about the text changes and the row pitch stays 30px.
   Descendant, not child: a fifth-level branch gets the same treatment. */
.megamenu__links .megamenu__links .megamenu__branch {
    line-height: 0;
}

/* Level 5 stays at the base 13px/400: the 300 it briefly had is superseded by the colour
   ladder below (#121212 -> #484848), which separates it from level 4 without thinning the
   type at the smallest size in the panel. */

/* QM: "Gruppieren, etwas weniger Abstand als hier" — leaf rows sit closer than the branches they
   hang off, so a run of them reads as one block. xl only: below that these are accordion rows and
   a 3px gap between 21px labels is not a tap target. */
@media (min-width: 1200px) {
    .megamenu__links .megamenu__links li {
        margin-bottom: 0.2rem;
    }

    /* QM: the fifth level again tighter than the fourth, "damit man besser sieht das die zusammen
       gehören und nicht zu Ebene 4". The li margin alone cannot do it — it is 3.2px already. What
       actually sets the row pitch here is o3-theme's `4px 0` on the inline-block link (the
       underline sweep's box) plus the line-height, so both come down: 30px pitch to 21px, against
       an indent of only 14px between the two levels. */
    .megamenu__links .megamenu__links .megamenu__links li {
        margin-bottom: 0.05rem;
    }

    .megamenu__links .megamenu__links .megamenu__links a {
        padding-top: 1px;
        padding-bottom: 1px;
        line-height: 1.3;
    }
}

/* `columns: 3` balances by content, so without this a category could be left in one track while
   the children it heads flow into the next. */
.megamenu__branch {
    break-inside: avoid;
}

/* Designer: "die Punkte rechts starten zu lassen und nicht links. Dann sitzen die Punkte alle
   an der blauen Box und nicht links am Rand."

   Downloads (4 groups) and Über uns (2) are short enough that the balanced box fills two of its
   three columns and leaves the third empty — 322px of white between the last entry and the
   Bestseller card, where Maschinen, Verbrauchsmaterial and Ersatzteile run right up to it. The
   mock-up solves the same case for the same menu with `justify-content: flex-end` over fixed
   thirds (shared-components.css:110, "let them keep a natural width and start from the right,
   leaving white space on the left"); this is that, against a multicol.

   The two values come from the script in tpl/widget/header/categorylist.tpl, because CSS has no
   predicate for "the content did not fill its columns" — a multicol box always claims the full
   width and always leaves the spare column on the fill side, and how many it actually used is
   only knowable once it is laid out. The column WIDTH is preserved, so the block still sits on
   the panel's three-column grid, just on its right-hand tracks. `direction: rtl` would have done
   the whole thing in two lines and was rejected: it fills from the right by reversing the column
   ORDER, so Downloads' four groups would read right-to-left.

   xl only — below that the panel is a single-column accordion with nothing to fit, and the class
   would otherwise survive a resize down. */
@media (min-width: 1200px) {
    .megamenu__groups--fitted {
        flex-grow: 0;
        columns: var(--bbb-mega-cols);
        width: var(--bbb-mega-width);
        margin-left: auto;
    }

    /* Designer: "warum sind bei Downloads eigentlich nicht drei in einer Reihe? Optisch macht
       es halt Sinn, weil die anderen ja auch 3 nebeneinander haben."

       Because a balanced multicol cannot do it. Downloads' four groups are a bare heading each
       — 39px, no children, unbreakable — so the shortest column height that fits them all in
       three tracks is two of them, and the balancer then fills greedily: 2 + 2 + 0. There is no
       3 + 1 in that model, and the empty third track is the hole the fitted rule above was
       trimming off.

       A panel whose groups carry no sub-list at all is a flat list of links, and a flat list
       wants a fixed grid rather than balanced flow. `:has()` asks precisely that, so the three
       panels that do have sub-lists keep the multicol they need: Verbrauchsmaterial's eleven
       groups would rag badly over rigid rows, and Maschinen's Neumaschinen has to be free to
       flow across all three columns.

       QM, on Über uns ("Kategorien rechts starten" + "hier ist dann platz" over the empty left
       column): the tracks are content-width, not `1fr`, and the row is justified to the end.
       `1fr` gave three short CMS titles a third of the panel each, which spread them from the far
       left and left the white space *inside* the tracks, where it reads as a hole rather than as
       margin. Content-width tracks put that space back on the left in one piece and park the
       titles against the Bestseller card — the same thing the fitted rule does for the multicol
       panels, and the same thing the mock-up's `justify-content: flex-end` does for its Über-uns
       row (shared-components.css:110).

       Then, on both flat panels: "da muss mehr abstand zwischen. und vier in eine reihe." So the
       column count is dropped entirely — `grid-auto-flow: column` puts a flat panel's groups in
       ONE row however many there are, which is what finally answers the older "warum sind bei
       Downloads eigentlich nicht drei in einer Reihe?" for its fourth entry too. The gap goes to
       4rem, from the 2.5rem the multicol panels use: those pack sub-lists under their headings,
       where a column gutter reads as structure, while here the headings ARE the content and 40px
       between two short words reads as a run-on.

       ponytail: 4rem is the widest that keeps Downloads' four titles on one line — 550px of text
       plus three gaps in a 782px box, i.e. 40px of slack. A fifth entry, or one longer than
       "Formulare/Datenblätter", overflows towards the Bestseller card rather than wrapping. If
       that happens, the honest fix is `grid-auto-flow: row` with `auto-fit` tracks, not a smaller
       gap — the gap is what QM asked for. */
    .megamenu__groups:not(:has(.megamenu__links)) {
        display: grid;
        grid-auto-flow: column;
        grid-auto-columns: max-content;
        justify-content: end;
        column-gap: 4rem;
    }

    /* "bei über uns würde ich die Abstände vergrößern. aber nur da, nicht bei den anderen
       Kategorien." Keyed to the entry count rather than to the category: a panel with no fourth
       group has room to spend, and the same rule keeps holding if a page is added or removed —
       naming Über uns would have made this a fact about the CMS tree instead of about the layout.

       Über uns' three titles carry 231px of text, so 10rem leaves them at 629..1180 with the
       Bestseller card still 48px clear and 231px of white on the left; Downloads has four and
       stays at 4rem, where it has 40px of slack (see the ponytail note above). Asked for twice —
       6rem was the first answer and was not enough. */
    .megamenu__groups:not(:has(.megamenu__links)):not(:has(> :nth-child(4))) {
        column-gap: 10rem;
    }
}

/* From xl the whole branch is open, so the accordion arrow has nothing to do. */
.megamenu__expand {
    display: none;
}

/* Mock-up sub-links: 14px / 400, muted, orange on hover. o3-theme has them at
   15px / 500 / near-black, which competed with the top-level labels above them. */
.megamenu a {
    font-size: 14px;
    font-weight: 400;
    color: var(--bbb-text-muted);
}

/* The mock-up's column headings: navy, bold, uppercase-free, a size up from the links. */
.megamenu a.megamenu__grouptitle {
    font-size: 15px;
    font-weight: 800;
    color: var(--bbb-navy);
}

/* QM: heading 800, the level under it 700. With the hairlines gone the third level is what tells
   the eye where one branch stops and the next starts, so it carries the weight instead of a rule.
   Keyed to the data-level the recursive include already writes — the fourth level and below stay
   at the base 400. */
.megamenu a[data-level="is-level-3"] {
    font-weight: 700;
}

/* One colour per level, as specified: navy heading, then #191c1e / #121212 / #484848 going
   down. The base above stays for the panel's CMS-page links, which carry no data-level.
   #121212 is o3-theme's body colour and #484848 is neither theme's — both are here on
   purpose, the ladder is meant to be read as a ladder. */
.megamenu a[data-level="is-level-3"] {
    color: var(--bbb-text);
}

.megamenu a[data-level="is-level-4"] {
    color: #121212;
}

.megamenu a[data-level="is-level-5"] {
    color: #484848;
}

/* Bestseller card — the mock-up's `.mh-mega-highlight`: ice-blue panel, 12px radius, the
   orange CTA. Fixed basis rather than a share of the panel so the category columns beside it
   keep their width whether a branch has two groups or eleven.

   The fill is the mock-up's exactly: ice at 50% with a solid ice hairline around it
   (shared-components.css:125), not the flat ice this used to paint. */
.megamenu__promo {
    flex: 0 0 280px;
    align-self: flex-start;
    padding: 1.5rem;
    border-radius: 0.75rem;
    background: rgba(225, 255, 255, 0.5);
    border: 1px solid var(--bbb-ice);
}

.megamenu__promo p {
    margin-bottom: 0.5rem;
}

/* Navy, not orange: the mock-up's label is `text-primary-brand` (html/index.html:323). The
   orange in that card belongs to the CTA alone. */
.megamenu__promo-label {
    font-size: 12px;
    font-weight: 700;
    letter-spacing: 0.05em;
    text-transform: uppercase;
    color: var(--bbb-navy);
}

.megamenu__promo-title {
    font-weight: 700;
    color: var(--bbb-navy);
}

.megamenu__promo-text {
    font-size: 14px;
    color: var(--bbb-text-muted);
}

.megamenu a.megamenu__promo-btn {
    margin-top: 0.5rem;
    padding: 0.5rem 1rem;
    font-size: 14px;
    font-weight: 700;
    color: #fff;
    background: var(--bbb-orange);
    border-radius: 0.375rem;
}

.megamenu a.megamenu__promo-btn:hover {
    color: #fff;
    background: var(--bbb-orange-dark);
}

/* o3-theme's underline sweep is on every `.megamenu a`; on a solid button it draws a bar
   across the label. */
.megamenu__promo-btn::after {
    content: none;
}

.megamenu a.current,
.megamenu a:focus-visible,
.megamenu a:hover {
    color: var(--bbb-orange);
}

/* o3-theme's underline sweep is keyed to --bs-primary; kept as the affordance but recoloured,
   since the mock-up's only hover signal is the orange text. */
.megamenu a::after {
    background: var(--bbb-orange);
}

/* --------------------------------------------------------------------------
   Below xl: one burger that collapses the nav IN PLACE (mock-up html/index.html, which is a
   plain `navbar-expand-lg`). The header row is logo + burger; the panel that drops out of it
   carries the category nav, then the search field, then the action icons.

   QM: "Mobil ganz andere Navigation. Andere Struktur und Aufbau, Bewegungen." This used to be a
   slide-in offcanvas holding a *second* copy of the navigation — different markup, different
   motion. Now there is one nav (widget/header/categorylist.tpl) and one search field, shown here
   and hidden at xl, and the offcanvas is gone from tpl/layout/header.tpl entirely.

   Scoped to :not(.is-minimal): the reduced checkout header renders no search and no nav at all,
   so hiding its actions there would leave a logo and no way to reach the basket or the account.
   -------------------------------------------------------------------------- */
.header__burger {
    display: none;
    color: var(--bbb-navy);
    background: transparent;
    border: 0;
    padding: 0.5rem;
}

.header__burger:hover {
    color: var(--bbb-orange);
}

@media (max-width: 1199.98px) {
    /* `:has(.header__mainnav)` throughout this block: the basket page keeps the full header class
       but renders no category nav at all, so there is nothing for the burger to collapse and
       nothing to hide the search and actions behind. Without the guard that page loses its basket
       and account icons on a phone, with a burger that does nothing. */
    header:not(.is-minimal):has(.header__mainnav) .header__burger {
        display: flex;
        align-items: center;
        order: 4;
        margin-left: auto;
    }

    /* Search and actions ride along in the collapsed panel, so they are tied to the nav's
       state rather than hidden outright. `:has()` on `.show`, which is what Bootstrap's collapse
       toggles; during the open/close transition it also sets `.collapsing`, hence both. */
    header:not(.is-minimal):has(.header__mainnav) .header__search {
        display: none;
    }

    /* The actions container is NOT hidden with `display: none`, it is collapsed to nothing.
       oxwMiniBasket renders `#offcanvas__basket` *inside* it, and that panel is what confirms an
       add-to-cart — QM: "Wenn man mobil etwas in den Warenkorb legt braucht man Popup, mit Info
       dass das Produkt in den Warenkorb gelegt wurde und Link zum Warenkorb". With the container
       display:none the panel got `.show` and Bootstrap laid its backdrop over the page, but the
       panel itself never painted: a phone tap on "In den Warenkorb" greyed the screen and showed
       nothing. `position: fixed` takes the panel out of this zero-size box, so it is unaffected. */
    header:not(.is-minimal):has(.header__mainnav) .header__actions {
        display: block;
        flex: 0 0 0;
        width: 0;
        height: 0;
        padding: 0;
    }

    header:not(.is-minimal):has(.header__mainnav) .header__actions > :not(.offcanvas) {
        display: none;
    }

    /* The magnifier joins the account and basket glyphs on ONE row (Nick's photo: three icons).
       `flex: 0 0 auto` instead of the `1 1 100%` this had, so it no longer claims a row of its
       own; `.header__actions` below it sits on the same line and the container's own 15px gap is
       what separates the search glyph from the account one. Order 7/8 keeps the search first. */
    header:not(.is-minimal):has(#header__mainnav.show) .header__search,
    header:not(.is-minimal):has(#header__mainnav.collapsing) .header__search {
        /* The actions' own `padding-bottom` is mirrored here, or the glyph sits 5px under their
           line: that padding is the menu's bottom spacing and it shortens the actions' content box
           to exactly icon height, so a sibling without it is centred against a taller box.
           Still `block` — as a flex parent the unclassed <form> inside stops filling the width and
           the opened field ends 67px short of the row. */
        display: block;
        padding-bottom: 1rem;
        order: 7;
        flex: 0 0 auto;
        margin: 0;
    }

    /* Opened, the field needs the width, so the search takes the whole row and the icons wrap
       under it. In flow, NOT the absolute strip the xl header uses: with the menu open this
       header is `overflow-y: auto` (it has to be, or a tall menu cannot be scrolled — see the
       note further down), and an absolutely positioned panel at `top: 100%` lands outside that
       box, i.e. reachable only by scrolling the header itself. */
    header:not(.is-minimal):has(#header__mainnav.show) .header__search.is-open,
    header:not(.is-minimal):has(#header__mainnav.collapsing) .header__search.is-open {
        flex: 1 1 100%;
        margin-bottom: 1rem;
    }

    /* The glyph keeps its own width beside the field rather than being squeezed by it. */
    header:not(.is-minimal) .header__search .input-group {
        flex-wrap: nowrap;
        gap: 0.75rem;
    }

    /* `flex: 1 1 auto`, not the `1 1 100%` this had: the two glyphs share the search's line and
       grow into whatever is left of it. No `gap` either — the row is three icons now, so the
       global `.header__actions { gap: 1rem }` and the container's own 15px lead-in put all three
       at the same distance from each other. */
    header:not(.is-minimal):has(#header__mainnav.show) .header__actions,
    header:not(.is-minimal):has(#header__mainnav.collapsing) .header__actions {
        display: flex;
        order: 8;
        flex: 1 1 auto;
        width: auto;
        height: auto;
        justify-content: flex-start;
        padding-bottom: 1rem;
    }

    header:not(.is-minimal):has(#header__mainnav.show) .header__actions > :not(.offcanvas),
    header:not(.is-minimal):has(#header__mainnav.collapsing) .header__actions > :not(.offcanvas) {
        display: flex;
    }

    /* Open the nav, open a category, open a branch and the header is 1435px tall in an 844px
       window — and it is `position: sticky; top: 0`, so it pins at the viewport top at EVERY
       scroll position. The 590px below the fold could never be reached: scrolling moved the page
       behind the menu instead. So while the collapse is shown the header stops at the viewport
       and scrolls inside itself.

       A flat 100dvh, not 100dvh + the 36px the header is pinned above the fold from md up: that
       offset only exists once the page HAS been scrolled, so at the top of the document the
       extra 36px would hang below the fold — and `overscroll-behavior` means the page cannot be
       nudged down to reach it. Being 36px short when pinned costs a sliver of menu height; being
       36px long costs reachability.

       Scoped to `.show`, not `.collapse`: Bootstrap adds the class only after its height
       transition finishes (and drops it before the closing one starts), so the animation still
       runs against an uncapped box. `contain` keeps the page behind still once the menu's own
       scroll runs out. */
    header:not(.is-minimal):has(#header__mainnav.show) {
        max-height: 100dvh;
        overflow-y: auto;
        overscroll-behavior: contain;
    }

    /* The nav takes the full width of a row of its own under the logo. Bootstrap's
       `.collapse:not(.show) { display: none }` handles the closed state and animates the height,
       which is the motion the mock-up has. */
    header:not(.is-minimal) .header__mainnav {
        order: 6;
        flex: 1 1 100%;
        margin: 0;
        border-top: 1px solid var(--bbb-border);
    }

    /* o3-theme's own inner `.navbar-collapse` is a Bootstrap collapse too, and its
       `navbar-expand-sm` only forces it open from 576px up — below that it would collapse the
       list inside the panel we just opened. Held open here; the outer collapse is the one that
       toggles. */
    header:not(.is-minimal) .header__mainnav .navbar-collapse {
        display: block !important;
    }

    header:not(.is-minimal) .header__mainnav .navbar {
        padding: 0;
    }

    /* One item per line, full width, with the caret pushed to the far end — the mock-up's
       stacked list rather than the desktop row. */
    header:not(.is-minimal) .header__mainnav .navbar-nav {
        flex-direction: column;
        align-items: stretch;
        gap: 0;
    }

    /* The mock-up's collapsed navbar: the caret sits right after the label (it is a Bootstrap
       `.dropdown-toggle::after`, which has nowhere else to go) and nothing rules the rows off
       from each other — the open panel's own card is what separates them. `0.5rem 1rem` is
       `.mh-nav-link`'s padding verbatim, so the labels start 16px in from the logo, as drawn. */
    header:not(.is-minimal) .header__mainnav .navbar-nav > .nav-item > .nav-link {
        justify-content: flex-start;
        padding: 0.5rem 1rem;
    }

    /* The mega menu panel is an absolutely positioned, container-wide card at xl. In a stacked
       list it has to be an ordinary block that pushes the items below it down, or it would cover
       them — but it stays a CARD: below lg the mock-up gives `.mh-mega-menu` the ice-grey
       surface, a 0.5rem radius and 1.25rem of padding, in flow under the label it belongs to
       (shared-components.css:147). That grey is what tells you where one category's panel ends
       and the next label starts, now that the rows carry no rules. */
    header:not(.is-minimal) .megamenu {
        position: static;
        width: auto;
        max-width: none;
        margin: 0.5rem 0 0.75rem;
        padding: 0;
        background: var(--bbb-surface-low);
        border-radius: 0.5rem;
    }

    header:not(.is-minimal) .megamenu::before,
    header:not(.is-minimal) .megamenu::after {
        content: none;
    }

    header:not(.is-minimal) .megamenu__inner {
        display: block;
        padding: 1.25rem;
        /* The accordion is in the page flow and scrolls with it — a scroll box inside a scroll box
           is the one thing worse than a long list on a phone. */
        max-height: none;
        overflow: visible;
    }

    header:not(.is-minimal) .megamenu__groups {
        columns: 1;
        padding: 0;
    }

    /* QM: "Mobil auch. Da gibt es dann pfeile." — the full branch is in the markup here too, but a
       stacked list of every level at once is 60 rows on a phone. So every row that has children
       collapses behind its own arrow: label and arrow share a line, the sub-list gets a line of
       its own. Closed is the default; the desktop panel is unaffected (the arrow is display:none
       there and the lists are always open). */
    header:not(.is-minimal) .megamenu__group,
    header:not(.is-minimal) .megamenu__branch {
        display: flex;
        flex-wrap: wrap;
        align-items: center;
    }

    header:not(.is-minimal) .megamenu__group > a,
    header:not(.is-minimal) .megamenu__branch > a {
        flex: 1 1 auto;
    }

    /* Designer: "Diese Schrift, Größe und Farbe" — pointing from our rows at the mock-up's
       "Heftmaschinen", which is `.small` + `.text-muted`, i.e. 14px/400 muted. The mock-up's
       panel has exactly two type styles, a heading and that item, so everything under the
       heading takes it and the desktop ladder (700, then #121212 -> #484848 by level) stops at
       xl. Five levels side by side need the ladder; a stacked accordion where one branch is
       open at a time has the indent and the arrow to say the same thing. */
    header:not(.is-minimal) .megamenu a[data-level="is-level-3"],
    header:not(.is-minimal) .megamenu a[data-level="is-level-4"],
    header:not(.is-minimal) .megamenu a[data-level="is-level-5"] {
        font-size: 14px;
        font-weight: 400;
        color: var(--bbb-text-muted);
    }

    /* The rule above out-specifies the panel's own hover/current orange (0,3,2 against 0,2,1),
       so that one is restated here rather than lost in the accordion. */
    header:not(.is-minimal) .megamenu a.current,
    header:not(.is-minimal) .megamenu a:focus-visible,
    header:not(.is-minimal) .megamenu a:hover {
        color: var(--bbb-orange);
    }

    /* The heading keeps the mock-up's rule, and the arrow takes the SAME padding and border so
       the line runs under it too, as drawn. It also fixes the alignment the designer marked
       ("Mittig"): `align-items: center` centred the arrow's box against a heading box that was
       taller by its padding + rule, which dropped the arrow ~7px. Stretched and padded alike,
       the arrow centres in the content box, i.e. on the heading's own line. The 4px top is
       o3-theme's `.megamenu a { padding: 4px 0 }`, matched so both start on the same line. */
    header:not(.is-minimal) .megamenu__grouptitle {
        margin-bottom: 0;
        padding-bottom: 0.5rem;
    }

    header:not(.is-minimal) .megamenu__group > .megamenu__expand {
        align-self: stretch;
        height: auto;
        padding: 4px 0 0.5rem;
        border-bottom: 1px solid var(--bbb-border);
    }

    /* Closed, a group is one row and needs no more air than the next one; open, the mock-up
       sets 1.5rem between a category block and the one after it. */
    header:not(.is-minimal) .megamenu__group,
    header:not(.is-minimal) .megamenu__branch {
        margin-bottom: 0.25rem;
    }

    header:not(.is-minimal) .megamenu__group.is-expanded {
        margin-bottom: 1.5rem;
    }

    header:not(.is-minimal) .megamenu__links .megamenu__links {
        margin: 0 0 0.5rem;
    }

    /* One pitch for every row in the accordion: a leaf is 28.3px of line box (14px x 1.45 plus
       o3-theme's 4px above and below) and a branch is the 32px arrow, so 4px on the leaf makes
       both 32. Without it the leaves — the rows with no arrow — got the most air of anything in
       the list. */
    header:not(.is-minimal) .megamenu__links li:not(.megamenu__branch) {
        margin-bottom: 0.25rem;
    }

    /* The 40px square was a tap target inside a 63px row; at the mock-up's pitch it IS the row
       height, so it comes down to 32 — the button keeps its 40px width, so the target stays
       well past the 24px minimum. */
    header:not(.is-minimal) .megamenu__expand {
        display: inline-flex;
        align-items: center;
        justify-content: center;
        flex: 0 0 auto;
        width: 2.5rem;
        height: 2rem;
        padding: 0;
        border: 0;
        background: transparent;
        color: var(--bbb-navy);
    }

    header:not(.is-minimal) .megamenu__expand svg {
        transition: transform 0.15s ease-out;
    }

    header:not(.is-minimal) .is-expanded > .megamenu__expand svg {
        transform: rotate(90deg);
    }

    header:not(.is-minimal) .megamenu__group > .megamenu__links,
    header:not(.is-minimal) .megamenu__branch > .megamenu__links {
        flex: 1 1 100%;
        display: none;
        /* Every level indents one step here, including the third — the desktop panel keeps that
           one flush with its heading ("Bündig zu Neumaschinen") because the heading's weight and
           colour already say where the level break is, and with all five levels side by side a
           fourth indent would eat the column. Under the heading every row is the same 14px muted
           list item, so the indent is what is left carrying depth. */
        padding-left: 0.9rem;
    }

    /* The mock-up's `mb-3` under the heading, minus the `pb-2` the heading already spends on its
       rule — the gap from heading text to first item comes out at the drawn 24px either way. */
    header:not(.is-minimal) .megamenu__group > .megamenu__links {
        margin-top: 1rem;
    }

    header:not(.is-minimal) .is-expanded > .megamenu__links {
        display: block;
    }

    /* The mock-up carries the Bestseller card into the collapsed navbar too — it is the last
       `col-md-4` in the row, so below lg it lands full width at the foot of the panel. `flex` and
       `align-self` are what gave it a column of its own beside the categories at xl; in this
       block container both are inert, so only the gap above it has to be said. */
    header:not(.is-minimal) .megamenu__promo {
        margin-top: 1.5rem;
    }

    /* Logo hard left, burger hard right. The logo's parent basis is a percentage in
       o3-theme (it shares the row with the actions there), which would leave the burger
       floating mid-row. */
    header:not(.is-minimal) .header__logo {
        flex: 0 1 auto;
    }
}

/* From xl the nav is a normal flex item again. Out-specifies Bootstrap's
   `.collapse:not(.show) { display: none }` (0,2,0), so no !important is needed — and
   deliberately not `display: flex`, the row is built from its `nav` child.

   `:not(.is-minimal)`: with `blDisableNavBars` off the category tree IS rendered on the order
   steps, and an unscoped rule would show the nav on the minimal checkout header from xl up
   while the burger — which those pages do not render — is the only way to reach it below. */
@media (min-width: 1200px) {
    header:not(.is-minimal) .header__mainnav.collapse {
        display: block;
    }
}

/* Utility bar (mock-up html/index.html): phone and e-mail grouped to the right.
   (The language/currency pickers used to sit here too; removed in header.tpl — the shop is
   German and Euro only.)

   o3-theme marks it `d-none d-lg-flex`, so it vanishes below 992px; the mock-up shows it
   from md up (`d-none d-md-block`) and it is the only navy element on a tablet-width
   page — losing it there is what made the header read as a different design. Revealed
   from md here rather than by editing header.tpl. */
@media (min-width: 768px) {
    /* Parent sets `display: grid !important` from lg; this file loads after main.min.css,
       so the tie on `.header__topinfo` resolves here regardless of query order.
       1.5rem is the mock-up's `gap-4` between the phone and e-mail groups. */
    .header__topinfo {
        display: flex !important;
        justify-content: flex-end;
        gap: 1.5rem;
    }

    /* The phone group is `d-none d-lg-flex` inside the bar, so revealing only the bar
       itself at md would leave just the e-mail in it. */
    .header__topinfo > .d-none {
        display: flex !important;
    }

    .header__topinfo > div:last-child {
        grid-column: auto;
    }

    /* header.tpl wraps the o3headerTopInfo CMS slot in .flex-grow-1, which would swallow
       all free space and re-scatter the three groups across the bar. */
    .header__topinfo > .flex-grow-1 {
        flex-grow: 0 !important;
        justify-content: flex-end !important;
    }
}

/* One-row header (mock-up html/index.html): logo | nav | search | actions.
   o3-theme already lays the header out as a wrapping flex row and orders the parts with
   `order`; it only stacks them because .header__mainnav asks for `flex: 1 1 100%`. So
   this is a flex-basis and order change, not a rebuild — no header.tpl override.
   .header__topinfo keeps its 100% basis and therefore still forms its own row above.

   Gated at xl, not lg, and measured rather than guessed: the three top-level category
   labels come to ~600px, so logo + nav + search icon + actions + gaps need ~975px of
   container. lg (992px) leaves ~962px — it misses by a hair and the action icons drop to
   a second row, which is exactly what this looked like at ~990px. xl leaves ~1140px,
   ~165px of slack, enough to absorb a fourth top-level category before it gets tight
   again. Below xl the parent's stacked rows take over: the nav gets its own full-width
   row there, so it can never wrap, and the search keeps its own row too — which is why
   the magnifier toggle in search.tpl is d-xl-flex rather than d-lg-flex.
   ponytail: a media query has to guess at the nav's width; if the category count keeps
   changing, swap this for a container query on <header> and drop the fixed breakpoint. */
@media (min-width: 1200px) {

    /* QM: "Abstände sind anders, hier geht die Navigation viel weiter" — the row stopped ~75px
       short of the container's right edge because nothing claimed the free space. The mock-up
       hangs it on the nav (`me-auto` on the <ul>), so the nav grows here too and the search and
       action icons end up flush right. */
    header:not(.is-minimal) .header__mainnav {
        /* Basis 0, not auto, and it is what makes the wider padding above safe: the header is a
           *wrapping* flex row, and wrapping is decided on the items' hypothetical sizes before
           any growing happens. With `auto` the nav contributed its full content width to that
           decision, the line came out 5px over 1110 and the action icons dropped to a second row.
           At basis 0 the nav never triggers the wrap; it takes the leftover instead — 1110 less
           the logo's 196, the magnifier's 23, the actions' 93 and the container's three 15px
           gaps, so ~753px against the ~726px the five labels need. A fallback-font overshoot
           (the nav renders ~48px wider before Hanken Grotesk arrives) now overflows the nav box
           for those few hundred milliseconds rather than reflowing the whole header.

           No left margin: the container's own 15px gap already separates it from the logo, and
           the nav-link's 1rem padding adds to that. */
        flex: 1 1 0;
        min-width: 0;
        order: 3;
    }

    .header__mainnav .navbar {
        padding-top: 0;
        padding-bottom: 0;
    }

    /* Parent spreads the top-level items edge to edge across their own row; the mock-up packs
       them left and lets the free space fall between the nav and the icons (`me-auto`).
       No inter-item gap on top of that: the nav-link's 1rem padding already puts 32px between
       two labels, so the mock-up's extra `gap-1` reads as nothing and costs 16px of a budget
       that has none to spare (see .header__mainnav above). */
    .header__mainnav .navbar-nav {
        justify-content: flex-start;
        gap: 0;
    }

    /* Search collapses to the magnifier (see tpl/widget/header/search.tpl), so it only
       claims icon width in the row — that is what keeps the action icons from wrapping
       to a second line around 1100px. */
    header:not(.is-minimal) .header__search {
        order: 4;
        flex: 0 0 auto;
        min-width: 0;
        /* Margin is set once, further down with the other xl icon-row spacing. */
    }


    /* Toggle styling and the panel's closed state are shared with every breakpoint now — see
       `.header__search-toggle` further up. */

    /* Revealed field is a strip under the header rather than an inline field, so opening
       it cannot reflow (and re-wrap) the header row. It anchors to <header>, which
       o3-theme already makes position: relative — but Bootstrap's .input-group is
       position: relative too and, being nearer, would otherwise capture the panel and
       size it to the collapsed magnifier's ~50px. */
    header:not(.is-minimal) .header__search .input-group {
        position: static;
    }

    /* `.input-group >` is here only to out-specify Bootstrap's
       `.input-group > :not(:first-child)…{ margin-left: -1px }`, which otherwise leaves
       margin-left at -1px and makes the auto right margin absorb all the free space —
       the panel then sits hard against the left edge instead of centred. Centring via
       left + translate rather than auto margins keeps that out of the picture entirely,
       and makes the backdrop's own `50% - 50vw` land exactly on the viewport edges. */
    header:not(.is-minimal) .header__search.is-open .input-group > .header__search-panel {
        display: flex;
        position: absolute;
        top: 100%;
        left: 50%;
        right: auto;
        transform: translateX(-50%);
        z-index: 1030;
        width: min(600px, 100%);
        margin: 0;
        padding: 1rem 0;
    }

    header:not(.is-minimal) .header__search.is-open .header__search-panel::before {
        content: "";
        position: absolute;
        inset: 0 calc(50% - 50vw);
        background: #fff;
        border-top: 1px solid var(--bbb-border);
        box-shadow: 0 12px 24px -12px rgba(0, 0, 0, 0.15);
        z-index: -1;
    }

    /* Field and button shape: none here. The strip only positions the panel — the pill
       itself is the mock-up's, shared with every breakpoint (see "Search bar" above). */

    /* The open panel must not print over the page. It has to stay absolutely positioned —
       putting it back in flow re-wraps the header row, which is the whole reason it is a strip
       (see the note above) — so the room is made by the page instead: `<main>` follows
       `<header>`, and `:has()` lets that sibling react to a class set deep inside the header.
       `~`, not `+`: o3-theme renders a widget between the two.

       The offset is the strip's own height expressed once, so the room and the strip cannot
       drift: field + the panel's 1rem top and bottom padding. Only needed at xl, where the panel
       is absolute; below that it is an in-flow row and already pushes the page down by itself. */
    header:not(.is-minimal):has(.header__search.is-open) ~ main {
        padding-top: var(--bbb-search-strip);
    }

    /* Slide rather than jump. The room and the strip move on the same curve, so the page reads
       as being pushed down BY the strip: `<main>` transitions its padding, and the panel is
       animated (not transitioned) because it is toggled with `display` — a transition cannot
       start from `display: none`, but a keyframe animation runs the moment the box appears, with
       no `@starting-style`/`allow-discrete` needed and no support caveat.

       An 8px lift instead of the strip's full height, and no clip-path: `clip-path: inset(…)`
       would have been the truer "pull out from under the header", but the panel's white band is
       its own `::before` stretched to the viewport with `inset: 0 calc(50% - 50vw)`, and a
       clip-path on the panel clips that pseudo-element to the panel's 600px box — the full-width
       band would end up cropped in the *final* state too. */
    main {
        transition: padding-top 0.22s ease;
    }

    header:not(.is-minimal) .header__search.is-open .input-group > .header__search-panel {
        animation: bbb-search-pullout 0.22s ease both;
    }

    @keyframes bbb-search-pullout {
        from {
            opacity: 0;
            transform: translateX(-50%) translateY(-8px);
        }
        to {
            opacity: 1;
            transform: translateX(-50%) translateY(0);
        }
    }

    /* The panel is centred with a transform, so the keyframes have to carry `translateX(-50%)`
       in both frames — animating `transform` replaces it wholesale, and dropping it would fling
       the strip 300px to the right for the duration. */
    @media (prefers-reduced-motion: reduce) {
        main,
        header:not(.is-minimal) .header__search.is-open .input-group > .header__search-panel {
            transition: none;
            animation: none;
        }
    }

    header:not(.is-minimal) .header__actions {
        order: 5;
    }

    /* Icon-only actions, as in the mock-up. The "Anmelden / Registrieren" label is
       ~200px and is what pushes the row over the wrap threshold. Hidden the
       visually-hidden way rather than with display:none, so the link keeps its
       accessible name for screen readers. */
    header:not(.is-minimal) .header__actions-account > span {
        position: absolute;
        width: 1px;
        height: 1px;
        padding: 0;
        margin: -1px;
        overflow: hidden;
        clip-path: inset(50%);
        white-space: nowrap;
        border: 0;
    }
}

/* --------------------------------------------------------------------------
   Footer — navy surface (sFooterColor); o3-theme hard-codes #121212 text.
   -------------------------------------------------------------------------- */
footer {
    color: #fff;
}

footer a {
    color: rgba(255, 255, 255, 0.7);
}

footer a:hover {
    color: var(--bbb-orange);
}

/* `!important` is load-bearing, not habit: the parent bundle ships
   `.footer__box-title { … margin-bottom: 18px !important }` (its only declaration of the class),
   so a plain 1.5rem here loses despite the extra `footer` in the selector. */
footer .footer__box-title {
    color: var(--bbb-orange);
    text-transform: uppercase;
    letter-spacing: 0.1em;
    font-size: 13px;
    margin-bottom: 1.5rem !important;
}

footer .text-muted,
footer .small {
    color: rgba(255, 255, 255, 0.6) !important;
}

footer hr,
footer .border-top,
footer .border-bottom {
    border-color: rgba(255, 255, 255, 0.1) !important;
}

/* A navy CTA disappears on the navy footer, so the footer CTA takes the
   accent orange (which is what the mock-ups use for promotional buttons). */
footer .btn-primary {
    --bs-btn-bg: var(--bbb-orange);
    --bs-btn-border-color: var(--bbb-orange);
    --bs-btn-hover-bg: var(--bbb-orange-dark);
    --bs-btn-hover-border-color: var(--bbb-orange-dark);
    --bs-btn-active-bg: var(--bbb-orange-dark);
    --bs-btn-active-border-color: var(--bbb-orange-dark);
}

/* Four-column footer, as in the mock-up. tpl/layout/footer.tpl supplies the columns
   (branding | Informationen | Service | Marken); everything here undoes o3-theme's
   centred-logo-band styling, which assumed the logo had a row of its own.

   The old CSS background-swap for the white logo is gone: footer.tpl now exists and
   points <img> at logo_bbb_white.png directly. */
footer > .container-xxl {
    padding-top: 3rem;
    padding-bottom: 1.5rem;
}

footer > .container-xxl > .row {
    --bs-gutter-y: 2.5rem;
}

/* Parent centres every footer column (`.footer__box { align-items: center }`) because it
   had a single stack per column; the mock-up's columns are left-aligned lists. */
footer .footer__box {
    align-items: flex-start;
    text-align: left;
    color: gray !important;
}

/* Below lg the parent draws hairlines between its three stacked columns
   (`.footer__box:nth-child(1)/(2)`, reset to `unset` from lg up). With four columns
   those land under the branding column and around Informationen only — a rule under one
   half of a 2x2 grid reads as a bug, and they are its near-black hairline, not white. */
footer .footer__box:nth-child(1),
footer .footer__box:nth-child(2) {
    border-top: 0;
    border-bottom: 0;
    color: gray !important;

}

/* Was a full-width band with a bottom hairline and a fixed 95px height; it is the first
   item of the branding column now. 85px is the mock-up's logo height. */
footer .footer__logo {
    display: block;
    height: auto;
    border-bottom: 0;
    margin-bottom: 1.5rem;
}

footer .footer__logo img {
    max-height: none;
    /* 60, not the 85 this used to be, and the footer logo does not change size: the three logo
       PNGs carried 52 transparent rows above the wordmark (see the header note) and have been
       cropped to 500x127. 85 x 127/179 = 60.3, so the visible wordmark renders exactly as
       before — what goes away is the empty band this fixed height was partly spending on
       nothing. */
    height: 60px;
    width: auto;
}

footer .footer__brand-claim {
    color: rgba(255, 255, 255, 0.5);
    font-style: italic;
    font-size: 0.875rem;
    margin-bottom: 1.5rem;
}

/* Nick: "thats a little bit dark ngl" — and measured, it was: the address and the phone inherit
   o3-theme's flat grey #808080, which this block's own `opacity: 0.75` then dims to rgb(96,106,115)
   on the navy. That is **2.70:1**, under WCAG AA (4.5) and even under the 3.0 for large text — the
   e-mail line right below it looked brighter because it is a link and carries the footer's own
   white instead.

   Set to that same white, so the whole block is one colour system. With the 0.75 the mock-up also
   has (`opacity-75` on its address box), the text lands at rgba white 0.525 -> rgb(134,153,170),
   **5.07:1** — the Entwurf's own value, and still a step below the 7.88:1 of the column links, so
   the address stays secondary to them. Dropping the `opacity` line would take it to those 7.88. */
footer .footer__brand-contact {
    border-top: 1px solid rgba(255, 255, 255, 0.1);
    padding-top: 1.5rem;
    font-size: 0.875rem;
    /* Mock-up: white text in an `opacity-75` box. A 70% white here on top of that opacity
       dimmed the address twice, to ~52%. */
    color: #fff;
    opacity: 0.75;
}

/* The e-mail is a link; `footer a` would dim it below the address it sits under. */
footer .footer__brand-contact a {
    color: inherit;
}

footer .footer__brand-claim p:last-child,
footer .footer__brand-contact p:last-child {
    margin-bottom: 0;
}

/* The Marken column is CMS content (tro_footer_marken) and ships its own nested
   `.row > .col-md-12`. Zeroing the gutter kills both the negative row margins — which
   would push the list past the column edge — and the column's padding, in one property. */
footer .footer__brands .row {
    --bs-gutter-x: 0;
}

footer .footer__brands p {
    margin-bottom: 0.5rem;
}

/* Parent paints the social circles in --bs-primary, which is now the same navy as the
   footer itself, i.e. invisible. */
footer .footer__box-social {
    width: 40px;
    height: 40px;
    margin-top: 1.5rem;
    background-color: rgba(255, 255, 255, 0.1);
}

footer .footer__box-social:hover {
    background-color: var(--bbb-orange);
}

/* Parent sets `padding: 24px 10px` here, so the bottom bar sat 5px further out than the
   columns above it — visible as a misalignment between the copyright and the logo.

   QM ("Line fehlt", "Verschieben"): the mock-up's bottom bar is
   `border-top border-white border-opacity-10 pt-4 d-flex justify-content-between`, and its
   copyright sits on the LEFT. Two things were missing: the hairline over the bar, and the
   order — o3-theme puts the VAT footnote first, so the copyright ended up on the right.
   `row-reverse` rather than reordering the template: the two are independent notes, so the
   swap is presentation, and it keeps both blocks (and their comments) where the parent
   theme has them. */
footer .footer__legal .container-xxl {
    position: relative;
    height: auto;
    flex-direction: row-reverse;
    padding-block: 1.5rem;
    padding-inline: calc(var(--bs-gutter-x) * 0.5);
}

/* The line, not a `border-top`: a border is painted on the border box, i.e. 15px wider on
   each side than the copyright and the logo it is supposed to line up with (the container's
   own inline padding). A pseudo-element can be held to the content box. */
footer .footer__legal .container-xxl::before {
    content: "";
    position: absolute;
    top: 0;
    left: calc(var(--bs-gutter-x) * 0.5);
    right: calc(var(--bs-gutter-x) * 0.5);
    border-top: 1px solid rgba(255, 255, 255, 0.1);
}

/* --------------------------------------------------------------------------
   Product listing pages — QM went through this page against
   html/Produktuerbersicht.html. Five of the six points live here; the sixth
   (the cards) is in the product-card section below.
   -------------------------------------------------------------------------- */

/* QM: "Breadcrumb: Schrift ist anders". o3-theme renders it at the inherited 17px with the
   current page in the same near-black as everything else, so the trail reads as body copy.
   The mock-up's `.mh-breadcrumb` is 14px with the current page picked out in navy bold.
   Not scoped to the listing: this is the same breadcrumb on every page of the shop. */
.breadcrumb {
    font-size: 14px;
    /* Separator: a slash, as in every mock-up. o3-theme leaves `--bs-breadcrumb-divider` unset,
       so Bootstrap falls back to the chevron SVG o3 wrote into that fallback — setting the
       variable is what the fallback is there for, and it beats redeclaring the `::before`. */
    --bs-breadcrumb-divider: "/";
}

.breadcrumb .breadcrumb-item a {
    color: #6c757d;
}

.breadcrumb .breadcrumb-item a:hover {
    color: var(--bbb-orange);
}

.breadcrumb .breadcrumb-item.active,
.breadcrumb .breadcrumb-item:last-child {
    color: var(--bbb-navy);
    font-weight: 700;
}

/* QM: "h1=40px (nicht 32)" — the mock-up's `display-6`. Navy too: every other heading in this
   theme is navy (start page, mega menu, cards) and the page title was the one left at
   o3-theme's near-black. `.title-big` is o3-theme's page-title class, so this is every
   listing, search and manufacturer page, not just this one. */
h1.title-big {
    font-size: 2.5rem;
    color: var(--bbb-navy);
}

/* The category's short description under the page title (page/list/list.tpl, bbb-theme copy):
   the mock-up's `<p class="text-muted mb-0">` in an 8-of-12 column, i.e. body size, muted,
   capped so a long sentence does not run the full row. */
.alist__lead {
    max-width: 66%;
    margin: 0 0 1.5rem;
    color: var(--bbb-text-muted);
}

/* QM on the detail page's "Ähnliche Produkte": "Verkehrte Font weight + size". Measured against
   the mock-up served over http (html/index.html, its "Aktuelle Angebote" / "Bestseller"), the
   size and the weight already agree — 32px / 600 on both sides, on this page and on the start
   page. What did NOT agree is the colour: the mock-up's section headings are the brand navy
   (`text-primary-brand`, #00284D) and ours were left at o3-theme's near-black #191c1e, which is
   the same divergence the page title had before the "h1=40px" round above. Section headings now
   follow the page title. If QM wants a different weight or size after this, it needs a number:
   the current pair is what the Entwurf renders. */
h2.title-big {
    color: var(--bbb-navy);
}

/* The search results page builds its own h1 instead of using .title-big, and it was already
   40px — only the colour was left at the parent's near-black. Same page title, same colour. */
body.cl-search h1 {
    color: var(--bbb-navy);
}

/* QM: "Keine Sortierung". The mock-up's listing toolbar carries the per-page control alone.
   Hidden rather than removed from widget/locator/listlocator.tpl: that file is the parent's
   and shared with the other locator placements, and display:none takes the control out of the
   accessibility tree too, so nothing is left half-present. The sort links themselves still
   work if a ?listorderby= URL is hit directly — this drops the picker, not the feature.
   ponytail: CSS switch, override listlocator.tpl if the bar ever needs real restructuring. */
.alist__locatorbar .alist__orga-sort {
    display: none;
}

/* QM: "Keine Linien" — o3-theme rules the toolbar off above and below; the mock-up has neither.
   Since the bar is now a single right-aligned control, the rules were also the only thing
   giving it the width of a band. */
.alist__locatorbar {
    border-top: 0;
    border-bottom: 0;
}

/* QM: "Optisch anpassen" — the mock-up's per-page control is a white bordered button, not the
   bare text-and-caret o3-theme leaves it as. */
.alist__locatorbar .alist__locator-perpage .btn {
    padding: 0.5rem 1.5rem;
    font-size: 14px;
    font-weight: 700;
    color: var(--bbb-navy);
    background: #fff;
    border: 1px solid var(--bbb-border);
    border-radius: 0.375rem;
}

.alist__locatorbar .alist__locator-perpage .btn:hover {
    border-color: var(--bbb-navy);
}

/* --------------------------------------------------------------------------
   Product cards — mock-up look: soft border, 12px radius, hover lift.
   -------------------------------------------------------------------------- */
.component__productbox-frame {
    border: 1px solid var(--bbb-border);
    border-radius: 0.75rem;
    /* No padding on the card itself — the picture and .component__productbox-body each carry
       their own 24px, exactly as the mock-up's `.product-card` does (padding 0, overflow
       hidden, wrapping `.product-image-wrapper.p-4` and `.p-4.border-top`). That is the only
       way the divider under the photo can reach both card edges. `overflow: hidden` so the
       ice-blue picture block is clipped by the radius instead of squaring off the corners. */
    padding: 0;
    overflow: hidden;
    background: var(--bbb-ice);
    transition: box-shadow 0.3s cubic-bezier(0.165, 0.84, 0.44, 1),
                transform 0.3s cubic-bezier(0.165, 0.84, 0.44, 1);
}

.component__productbox-frame:hover {
    box-shadow: 0 12px 24px -6px rgba(0, 0, 0, 0.12);
    transform: translateY(-4px);
}

.component__productbox-price .lead,
.component__productbox-price strong {
    color: var(--bbb-navy);
    font-size: 1.25rem;
    font-weight: 700;
}

/* The sliders put .component__productbox straight into .splide__list, so the tiles
   get no .component__productbox-frame and end up unframed and vertically ragged
   (short titles float, prices land at different heights). Framing the slide itself
   gives the same card as the PLP without touching list_splide.tpl. */
.component__productslider .splide__list {
    align-items: stretch;
}

/* QM: "Mobil: Boxen untereinander". The start-page rows hand their markup back below sm
   (`destroy: true`, see tpl/page/shop/start.tpl): Splide strips its inline widths and the list
   falls back to plain block flow, so the cards already stack full width — but with the gap gone
   too, so they touch. Only the spacing is restored here.

   Scoped to the start page rather than to a Splide state class: `destroy` leaves `is-initialized`
   on the element, so there is nothing on the DOM that distinguishes a destroyed slider from a
   mounted one. The start page is where all three `destroy: true` rows live (they are the only
   users of `$bbbRowSplide`), and the related-product sliders elsewhere stay mounted at
   `perPage: 1`, where a margin here would be added on top of Splide's own gap. */
@media (max-width: 575.98px) {
    body.cl-start .component__productslider .splide__list > * + * {
        margin-top: 1rem;
    }
}

.component__productslider .component__productbox {
    display: flex;
    flex-direction: column;
    /* Same two-block anatomy as the framed card above — see the note there. */
    padding: 0;
    overflow: hidden;
    border: 1px solid var(--bbb-border);
    border-radius: 0.75rem;
    background: var(--bbb-ice);
    transition: box-shadow 0.3s cubic-bezier(0.165, 0.84, 0.44, 1),
                transform 0.3s cubic-bezier(0.165, 0.84, 0.44, 1);
}

.component__productslider .component__productbox:hover {
    box-shadow: 0 12px 24px -6px rgba(0, 0, 0, 0.12);
    transform: translateY(-4px);
}

/* The progress bar goes EVERYWHERE. `list_splide.tpl` renders `.splide__progress` after the
   track, and it paints a 1px `rgba(17, 34, 17, .133)` rule the full width of the row — a
   near-black hairline under every product slider, which the mock-ups have nowhere. It is also
   redundant: the arrows already say the row scrolls. Not scoped to a page, because there is no
   width or page at which that line is wanted; splide's own options cannot switch it off since
   the template hard-codes the markup. `content`-free element, so `display: none` is the only
   lever here. */
.component__productslider .splide__progress {
    display: none;
}

/* The two start-page rows are a static four-up grid, so the arrows go there too. Splide's
   `arrows: false` (passed from start.tpl) only stops it *generating* them, and
   list_splide.tpl hard-codes the markup, so they survive the option. Scoped to the start page:
   the same slider carries related products, the thank-you page and the notice list, where
   there can be more items than fit and the arrows are meaningful. */
body.cl-start .component__productslider .splide__arrows {
    display: none;
}

/* Fixed 390:245 image box (the mock-up's ratio) so titles across a row start on the same
   line regardless of how tall each product photo is. Applies to the category grid as well
   as the sliders — the parent lets the picture flex there, which left the tiles ragged. */
.component__productbox-picture {
    flex: 0 0 auto;
    aspect-ratio: 390 / 245;
}

.component__productbox-picture-link,
.component__productbox-picture-link img {
    height: 100%;
    object-fit: contain;
}

/* Price + CTA sit on the card's bottom edge whatever the title wraps to. */
.component__productslider .component__productbox-price,
.component__productslider .component__tobasket {
    margin-top: auto;
}

/* Mock-up puts the product photo on the same ice blue the photography itself is shot
   against, so the letterboxed area around a non-4:3 image blends into the picture
   instead of framing it in white. */
.component__productbox-picture {
    background: var(--bbb-ice);
}

/* Card text: name, then the descriptive subtitle, then the price block (see
   tpl/widget/product/listitem_grid.tpl). */
/* Mock-up: title tight to its subtitle, then a clear 24px before the price block. */
.component__productslider .component__productbox-title,
.component__productbox-frame .component__productbox-title {
    margin-bottom: 0.25rem;
}

/* The mock-up's two card blocks. Each pads itself, so the divider between them spans the whole
   card — which is what it took to bring the hairline back; it had been dropped while the padding
   still sat on the card and it could only ever stop 24px short of both edges. */
.component__productbox-picture {
    padding: 1.5rem;
    margin-bottom: 0 !important;
}

.component__productbox-body {
    padding: 1.5rem;
    border-top: 1px solid var(--bbb-border);
    /* Flex column and growing, so .component__productbox-footer's `margin-top: auto` still
       pins the price row to the card's bottom edge now that the card is no longer its parent. */
    display: flex;
    flex-direction: column;
    flex: 1 1 auto;
}

.component__productbox-subtitle {
    margin-top: 0;
    margin-bottom: 1.5rem;
    font-size: 0.875rem;
    line-height: 1.35;
    color: #6c757d;
    display: -webkit-box;
    -webkit-line-clamp: 2;
    -webkit-box-orient: vertical;
    overflow: hidden;
}

.component__productbox-price-from {
    display: block;
    font-size: 0.875rem;
    color: #6c757d;
    line-height: 1.3;
}

.component__productbox-price-note {
    display: block;
    font-size: 11px;
    color: #6c757d;
    line-height: 1.3;
}

/* Price left, action right, both sitting on the card's bottom edge (mock-up). The row is
   pushed down by margin-top:auto so cards in a row stay aligned however the titles wrap. */
.component__productbox-footer {
    display: flex;
    justify-content: space-between;
    align-items: flex-end;
    gap: 0.75rem;
    margin-top: auto;
}

.component__productbox-action {
    flex: 0 0 auto;
}

/* The quick-add form was an absolutely positioned hover overlay on the picture; in the
   footer it is a normal inline element again. */
.component__productbox-action .component__productbox-quickadd {
    position: static;
    inset: auto;
    opacity: 1;
    pointer-events: auto;
    background: none;
    overflow: visible;
}

/* Only the submit survives as the mock-up's 42x34 icon button — quantity stepper and input
   belong on the detail page, not on a card this size. */
.component__productbox-action .component__tobasket {
    border: 0;
    padding: 0;
}

.component__productbox-action .component__tobasket-minus,
.component__productbox-action .component__tobasket-plus,
.component__productbox-action .component__tobasket-input {
    display: none;
}

.component__productbox-action .component__tobasket-submit,
.component__productbox-action .btn-primary {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 42px;
    height: 34px;
    padding: 0;
    border-radius: 4px;
    /* Both buttons label themselves in text. Collapsing the font size hides the label
       without hiding the icon, which is restored to size below — the alternative would be
       editing TroRequestArticleQuotation's own block template. */
    font-size: 0;
    white-space: nowrap;
    /* QM: "icon mittig". The quote button's label is a text NODE, not an element, so
       font-size: 0 shrinks it to nothing but it stays a flex item — and the button's 8px gap
       was still being spent between it and the icon. 20px of icon plus an 8px gap centred in
       42px put the envelope 4px left of centre. The cart and details buttons never showed it:
       their labels are `<span>`s that are display: none, so there was no second item to space
       against. The listing pages set their own 0.5rem back (see below), where the label is
       spelled out and the gap is doing its job. */
    gap: 0;
}

/* --- One icon set for all three card actions -------------------------------
   QM, third round on these buttons: "1. Unterschiedliche Borderdicke. Das muss einheitlich
   sein. 2. Größe ist noch zu klein, siehe Vorlage." Both are about the icons, not the button:
   measured against html/index.html, the mock-up's button is 42 x 34 with a 1px border, which
   is exactly what this theme already draws. What differed was everything inside it, because
   the three actions came from three unrelated sources:

     cart      o3-theme's svg, 52-unit viewBox, stroke 1.5, scaled 1.4  -> ink  9.5 x 12.5
     details   this theme's chevron svg, 24-unit viewBox, stroke 1.8    -> ink  5.5 x 11
     quote     TroRequestArticleQuotation's <i>, painted as a mask      -> ink 16.7 x 12.5 (no stroke)

   Three stroke weights and three optical sizes in one row of cards — "unterschiedliche
   Borderdicke" is the line weight, and all three are well under the mock-up's ink.

   The mock-up draws all of them as one 24px `material-symbols-outlined` glyph
   (`shopping_cart`, and `mail` on the quote-only card). So do we now, from the Material Icons
   Outlined font the designer shipped and this theme self-hosts — the same font as the header
   icons, the basket ✕ and the basket's update button. Uniform weight and uniform 24 x 24 by
   construction, and it deletes the per-svg tuning this used to need: the 1.4 transform, the
   non-scaling-stroke, the hidden 52x52 tint rect and the mail mask are all gone with it.

   `font-size` in px, not em: the button sets `font-size: 0` to hide its text label, so a
   relative size would resolve to nothing.
   -------------------------------------------------------------------------- */
.component__productbox-action .component__tobasket-submit svg,
.component__productbox-action .component__productbox-details svg,
.component__productbox-action .fa-envelope {
    display: none;
}

.component__productbox-action .btn-primary::before {
    font-size: 24px;
}

.component__productbox-action .component__tobasket-submit::before {
    content: "shopping_cart" / "";
}

.component__productbox-action .component__productbox-details::before {
    content: "chevron_right" / "";
}

/* TroRequestArticleQuotation's button is the only `.btn-primary` in this slot that is neither
   the cart nor the details link, and it renders inside its own `.actions` wrapper — which is
   what the `:has(.actions)` rules below already key off. Mock-up: `mail`. */
.component__productbox-action .actions .btn-primary::before {
    content: "mail" / "";
}

.component__productbox-action .component__tobasket-submit span {
    display: none;
}

.component__productbox-action .actions,
.component__productbox-action .btn-group {
    display: block;
}

/* The details fallback, the add-to-cart form and the quotation button all occupy this slot,
   and only the module can decide whether it renders. When it does, it wins: quotation-required
   means the article is not sold straight from the listing — TroRequestArticleQuotation's own
   block replaces the parent's buy action, and since this theme lifted the cart form out of
   that block it would otherwise survive alongside the quote button. */
.component__productbox-action:has(.actions) .component__productbox-details,
.component__productbox-action:has(.actions) .component__productbox-quickadd {
    display: none;
}

/* listitem_grid.tpl now always renders the details link (see the note there). Off the listing
   pages it is still only the fallback, so anything that actually acts on the card takes
   priority over it. */
.component__productbox-action:has(.component__productbox-quickadd) .component__productbox-details {
    display: none;
}

.component__productbox-details span {
    display: none;
}

/* --------------------------------------------------------------------------
   QM: "Die Boxen sehen hier anders aus als auf der Startseite. Bitte anpassen."

   That is about the mock-ups, not about us: html/index.html's card ends in one small icon
   button beside the price, html/Produktuerbersicht.html's ends in two full-width labelled
   buttons — "Details ansehen" over "In den Warenkorb" (or "Angebot anfordern"). This theme
   had the compact card on both. The compact one is right for the start page and stays; the
   listing pages get the mock-up's stacked version.

   Scoped to the two list classes rather than to `.component__productbox-frame`: the frame is
   also what the notice list and "customers also bought" use, where the compact card is what
   fits. Everything here reverses a collapse the compact card does, so the labels come back
   rather than being written twice.
   -------------------------------------------------------------------------- */
body.cl-alist .component__productbox-footer,
body.cl-search .component__productbox-footer {
    display: block;
}

body.cl-alist .component__productbox-action,
body.cl-search .component__productbox-action {
    display: grid;
    gap: 0.5rem;
    /* "can you do here a little bit less space?" — the band between the VAT line and the first
       button, which was the mock-up's own `mt-4` (24px, html/Produktuerbersicht.html) and measured
       25.5px on the card he photographed. 1rem is the whole change; the 0.5rem between the two
       buttons is the mock-up's `gap-2` and stays, being 8px already. */
    margin-top: 1rem;
}

/* Both buttons full width and labelled again — the compact card sizes them to a 42x34 icon
   and zeroes the font to hide the text. */
body.cl-alist .component__productbox-action .component__tobasket-submit,
body.cl-alist .component__productbox-action .btn-primary,
body.cl-search .component__productbox-action .component__tobasket-submit,
body.cl-search .component__productbox-action .btn-primary {
    width: 100%;
    height: auto;
    /* QM, three times: "etwas weniger Abstand zu dem Text" / "das icon kann etwas näher ran"
       between the cart glyph and the label. The first round answered with a measurement — the
       icon unification had already taken the optical gap from 14.3px to 10.3px, i.e. onto the
       mock-up's 10.4px — and changed nothing. Each round since has gone tighter than the
       Vorlage: 0.375rem was 8.3px optical, 0.25rem 6.3px, and this round 0.125rem — 4.3px,
       about as close as the glyph's own side bearing allows before the two touch. */
    gap: 0.125rem;
    padding: 0.5rem 1rem;
    font-size: 14px;
    font-weight: 700;
    border-radius: 0.375rem;
}

body.cl-alist .component__productbox-action .component__tobasket-submit span,
body.cl-alist .component__productbox-details span,
body.cl-search .component__productbox-action .component__tobasket-submit span,
body.cl-search .component__productbox-details span {
    display: inline;
}

/* The glyph is a `::before` now (see the icon block above), so it already comes first — which
   is what the mock-up shows and what the `flex-direction: row-reverse` that used to live here
   was working around while the cart was an svg after the label.

   Size: the mock-up's labelled buy button carries `material-symbols-outlined fs-5`, i.e. 20px,
   against the 24px of the compact card's icon-only button (html/Produktuerbersicht.html). */
body.cl-alist .component__productbox-action .btn-primary::before,
body.cl-search .component__productbox-action .btn-primary::before {
    font-size: 20px;
}

/* The chevron was the label's stand-in on the compact card; with "Details ansehen" spelled out
   the mock-up's outline button carries no icon at all. */
body.cl-alist .component__productbox-details::before,
body.cl-search .component__productbox-details::before {
    content: none;
}

/* The mock-up's first button is the outline one, the buy action the filled one — the details
   link is `.btn-primary` in the markup, so it is recoloured rather than re-classed. */
body.cl-alist .component__productbox-details,
body.cl-search .component__productbox-details {
    color: var(--bbb-navy);
    /* QM: "hintergrund auf weiß". Transparent let the card's ice-blue body through, so the
       outline button read as a framed area of the card rather than as a button; the mock-up's
       `Details ansehen` is white on that same ice-blue card. */
    background: #fff;
    border: 1px solid var(--bbb-navy);
}

/* Orange, like every other hover in the shop — the outline button's own rule above out-specifies
   the .btn-outline-primary variables, so it has to say it again. */
body.cl-alist .component__productbox-details:hover,
body.cl-search .component__productbox-details:hover {
    color: #fff;
    background: var(--bbb-orange);
    border-color: var(--bbb-orange);
}

/* Details sits above the buy action, whichever of the two fills the slot (the quotation
   button comes from TroRequestArticleQuotation's own block, after the cart form). */
body.cl-alist .component__productbox-details,
body.cl-search .component__productbox-details {
    order: -1;
}

/* …and on the listing it is shown alongside them, not instead of them. */
body.cl-alist .component__productbox-action:has(.component__productbox-quickadd) .component__productbox-details,
body.cl-alist .component__productbox-action:has(.actions) .component__productbox-details,
body.cl-search .component__productbox-action:has(.component__productbox-quickadd) .component__productbox-details,
body.cl-search .component__productbox-action:has(.actions) .component__productbox-details {
    display: inline-flex;
}

/* A reduced price is the mock-up's one orange price. :has() keys off the <del> the parent
   only renders when there is a crossed-out original, so no template flag is needed. */
.component__productbox-price:has(.component__productbox-price-old) .lead {
    color: var(--bbb-orange);
}

/* --------------------------------------------------------------------------
   Start page — sections (see tpl/page/shop/start.tpl)
   -------------------------------------------------------------------------- */

/* Hero runs edge to edge in the mock-up. The banners are 1170x370, so at a 1440 viewport
   full-bleed lands the slider at ~455px tall — the mock-up's 450px, without forcing a
   height and cropping the artwork. Bleeding the element itself (not a ::before) because
   the slide images have to grow with it. */
.start__promoslider {
    margin-inline: calc(50% - 50vw);
    /* o3-theme leaves 5px above the slider, which shows as a white sliver between the
       header and the hero. The mock-up runs them flush. */
    margin-top: 0;
}

/* QM: "Maximale Slider Höhe soll 450px sein". Full-bleed means the height tracks the
   viewport width, so past ~1420px the hero kept growing (~600px on a 1900px screen).
   Capped on the images, not the track: the slides carry `d-block w-100` from
   widget/promoslider.tpl, so height is intrinsic and the track just follows them.
   object-fit centre-crops the overflow rather than squashing the 1170x370 artwork. */
.start__promoslider .splide__slide img,
.start__promoslider img.splide__slide {
    max-height: 450px;
    object-fit: cover;
    object-position: center;
}

/* o3-theme also puts 50px below the slider. In the mock-up the hero sits directly on the
   ice-blue welcome band, so that gap reads as a stray white stripe between two full-bleed
   blocks. Dropped only when a coloured band actually follows — the band brings its own
   padding, whereas an unbanded product slider has no top margin at all and its content
   would end up touching the hero.

   `~`, not `+`: start.tpl inserts the fixed-banner slider between the sections whenever any
   of fixedBannerSmall1..3 is configured, and an adjacent-sibling test silently stops matching
   the moment someone sets one in the admin. Trade-off of the wider combinator: with a banner
   configured it now sits flush on the band above it (the banner carries the parent's 80px
   below it and nothing above), where before it had this 50px of air. */
.start__promoslider:has(~ .start__welcome),
.start__promoslider:has(~ .start__band-low) {
    margin-bottom: 0;
}

/* Mock-up alternates surfaces down the page: ice blue welcome, grey offers, white
   bestsellers. Same measured bleed as the other bands. */
.start__band-low {
    position: relative;
    padding: 3rem 0;
}

.start__band-low::before {
    content: "";
    position: absolute;
    inset: 0 calc(50% - 50vw);
    background: var(--bbb-surface-low);
    z-index: -1;
}

/* Section head: the parent puts an <hr> under the heading; the mock-up runs the rule on
   from the end of the heading text. start.tpl wraps the pair so this can be one flex row. */
.start__sectionhead {
    display: flex;
    align-items: center;
    gap: 1.5rem;
    margin-bottom: 2.5rem;
}

.start__sectionhead h2 {
    margin-bottom: 0;
    flex-shrink: 0;
}

.start__sectionhead hr {
    flex: 1 1 auto;
    margin: 0;
    opacity: 1;
    border-top: 1px solid var(--bbb-border);
}

/* The offers head in the mock-up is the other of its two head patterns: heading stacked over a
   muted sub-line on the left, "Alle Angebote →" on the right, and no rule
   (`d-flex justify-content-between align-items-end`). Bestseller keeps the run-on rule above. */
.start__sectionhead--stacked {
    justify-content: space-between;
    align-items: flex-end;
}

.start__sectionhead--stacked h2 {
    margin-bottom: 0.25rem;
}

.start__sectionhead-sub {
    margin-bottom: 0;
    color: #6c757d;
}

/* Mock-up: `text-primary-brand text-decoration-none fw-bold small` + a chevron. */
.start__sectionhead-more {
    display: inline-flex;
    align-items: center;
    gap: 0.25rem;
    flex-shrink: 0;
    font-size: 0.875rem;
    font-weight: 700;
    color: var(--bbb-navy);
    text-decoration: none;
}

.start__sectionhead-more:hover {
    color: var(--bbb-orange);
}

/* --------------------------------------------------------------------------
   Start page — welcome band
   -------------------------------------------------------------------------- */

/* Full-bleed ice-blue surface behind a container-bound row. `50% - 50vw` breaks
   out of any centred container width, which is why this does not need the
   breakpoint-by-breakpoint calc that o3-theme's .start__manufacturer uses. */
.start__welcome {
    position: relative;
    padding: 3.5rem 0;
    margin-bottom: 3.5rem;
}

/* Two full-bleed bands meet here (ice blue above, grey below), so the margin between them
   shows as a white stripe. The mock-up butts them straight together. Kept when an unbanded
   section follows instead — those have no top margin of their own.

   `~` for the same reason as the slider above: with a fixed banner configured, start.tpl puts
   it between the two bands and `+` would stop matching. Same trade-off — the banner then sits
   flush on this band. */
.start__welcome:has(~ .start__band-low) {
    margin-bottom: 0;
}

.start__welcome::before {
    content: "";
    position: absolute;
    inset: 0 calc(50% - 50vw);
    background: var(--bbb-ice);
    z-index: -1;
}

/* QM (mobile, 390px): "Abstand" — below lg the news card stacks under the benefit boxes and the
   two touched, ice-blue card straight into navy card with no gap at all.

   tpl/page/shop/start.tpl asks for it with `row g-5`, but o3-theme purges Bootstrap's gutter
   utilities out of main.min.css — there is no `.g-5` rule left in the bundle, so the class is
   inert and `.row`'s own `--bs-gutter-y: 0` stands. (That `g-5` is the only gutter utility any
   template in either theme uses, so this one row is the whole of the problem.)

   Only the vertical gutter is restored, not `g-5`'s horizontal 3rem: the column gap at desktop is
   the parent's 1.875rem today, nobody has complained about it, and widening it would change a
   layout QM has already signed off. */
.start__welcome .row {
    --bs-gutter-y: 3rem;
}

.start__welcome-intro {
    margin-bottom: 2.5rem;
}

/* The intro is WYSIWYG content, so it is styled by element, not by class:
   an eyebrow line, the page headline, a lead paragraph, then fine print. */
.start__welcome-intro > p:first-child {
    color: var(--bbb-orange);
    font-weight: 700;
    text-transform: uppercase;
    letter-spacing: 0.12em;
    font-size: 14px;
    margin-bottom: 1rem;
}

.start__welcome-intro h1 {
    color: var(--bbb-navy);
    font-size: clamp(1.9rem, 3.2vw, 2.8rem);
    line-height: 1.15;
    margin-bottom: 1.5rem;
}

.start__welcome-intro h1 + p {
    font-size: 1.25rem;
    line-height: 1.5;
}

.start__welcome-intro p:last-child {
    opacity: 0.75;
}

/* Service badges: o3-theme draws them as one strip with hairline top/bottom
   borders; the mock-up wants three separate white cards. */
.start__welcome .start__benefits {
    border: 0;
    padding: 0;
    display: flex;
    flex-wrap: wrap;
    gap: 1rem;
}

.start__welcome .start__benefits-box {
    flex: 1 1 220px;
    justify-content: flex-start;
    align-items: center;
    gap: 1rem;
    padding: 1.5rem;
    background: #fff;
    border-radius: 0.75rem;
    box-shadow: 0 2px 8px -2px rgba(0, 0, 0, 0.08);
    width: auto;
    transition: background 0.3s ease;
}

.start__welcome .start__benefits-box:hover {
    background: var(--bbb-surface-low);
}

.start__welcome .start__benefits-box img,
.start__welcome .start__benefits-box svg {
    flex-shrink: 0;
    width: 2rem;
    height: 2rem;
}

.start__welcome .start__benefits-box strong {
    display: block;
    color: var(--bbb-navy);
    font-size: 0.875rem;
}

.start__welcome .start__benefits-box p {
    margin-bottom: 0;
    font-size: 0.875rem;
    color: #6c757d;
}

/* Editorial news tile — navy card, full column height. */
.start__newscard {
    height: 100%;
    padding: 2.5rem;
    border-radius: 1rem;
    background: var(--bbb-navy);
    color: #fff;
    box-shadow: 0 1rem 3rem rgba(0, 0, 0, 0.175);
}

.start__newscard h2,
.start__newscard h3 {
    color: #fff;
    font-size: 1.5rem;
    margin-bottom: 1rem;
}

.start__newscard img {
    width: 100%;
    height: auto;
    border-radius: 0.5rem;
    margin-bottom: 1.25rem;
}

.start__newscard .badge {
    background: var(--bbb-orange);
    margin-bottom: 1rem;
}

/* Date line: calendar glyph + date, bold, above a faint rule like the mock-up. */
.start__newscard-date {
    display: flex;
    align-items: center;
    gap: 0.5rem;
    font-weight: 700;
    font-size: 0.875rem;
    margin-top: 1.5rem;
    padding-top: 1.5rem;
    border-top: 1px solid rgba(255, 255, 255, 0.1);
}

.start__newscard a {
    display: inline-flex;
    align-items: center;
    gap: 0.25rem;
    color: var(--bbb-orange);
    font-weight: 700;
    text-decoration: none;
}

/* The mock-up's "Details erfahren →" long arrow — QM 2026-09-04: chevrons only, everywhere. */
.start__newscard a::after {
    content: "chevron_right" / "";
    font-size: 1.25rem;
}

.start__newscard a:hover {
    color: #fff;
}

/* ==========================================================================
   Product detail page — mock-up html/produktdetailseite-2.html.

   o3-theme lays the hero out as a named CSS grid on the product form
   (`grid-template-areas: "pictures . title" / "pictures . info" /
   "pictures . wrapper"`), which makes the mock-up's arrangement reachable by
   redeclaring the grid rather than copying its 331-line productmain.tpl.
   Tokens below are measured off the mock-up, not estimated.
   ========================================================================== */

/* Mock-up hero is two equal columns with a g-5 gutter; o3-theme's is 58% / 9% / 33%,
   which left the whole purchase side in a 366px strip. The middle column is the
   parent's spacer (the `.` in its grid areas), so this is one declaration.
   Area order also swaps: the Art-Nr / stock / rating meta line moves *above* the
   title, as in the mock-up. */
form.js-oxProductForm.details {
    grid-template-columns: minmax(0, 1fr) 3rem minmax(0, 1fr);
    /* A fourth row for the short description productmain.tpl adds — it must be placed
       explicitly, or grid auto-placement drops it into the first free cell, which is under the
       picture. */
    grid-template-areas:
        "pictures . info"
        "pictures . title"
        "pictures . shortdesc"
        "pictures . wrapper"
        "pictures . .";
    /* The fifth, empty row is what keeps the purchase side packed at the top. The picture spans
       every row, so its height sets the grid's height — and with only auto rows the surplus was
       distributed *into* the content rows: on a tall photo the short-description row stretched to
       236px for 101px of text, leaving a hole between the description and the price. `1fr` on a
       trailing spacer row absorbs that surplus instead. */
    grid-template-rows: auto auto auto auto 1fr;
}

/* QM: "Produkttext erscheint mobil unterhalb Bilder."

   o3-theme lays the product page out with a named CSS grid from md up (pictures left, title /
   info / wrapper right), and below md it just falls back to DOM order — which is title, article
   number, short description, and only *then* the photo. On a phone that meant three blocks of
   text before the visitor saw what they were looking at.

   Flex column with explicit orders below md, because the element is `display: block` there:
   photo first, then the text, then the price and the cart button. The grid at md and up is
   untouched — `order` is inert on a grid item that is placed by area. */
@media (max-width: 767.98px) {
    .details {
        display: flex;
        flex-direction: column;
    }

    .details__picture {
        order: 1;
    }

    .details__title {
        order: 2;
    }

    .details__info {
        order: 3;
    }

    .details__shortdesc {
        order: 4;
    }

    .details__wrapper {
        order: 5;
    }

    /* The photo carries the gap to the title now; the title's own top margin would double it. */
    .details__picture {
        margin-bottom: 1.5rem;
    }
}

/* Mock-up: 16px Inter, #43474E, line-height 1.6. */
.details__shortdesc {
    grid-area: shortdesc;
    color: var(--bbb-text-variant);
    line-height: 1.6;
    margin-bottom: 1.5rem;
}

/* Mock-up: 40px / 700, navy, display face, tight leading. o3-theme has 1.5rem / 700
   in near-black. */
.details__title {
    font-family: var(--bbb-font-display);
    /* The mock-up's 40px, but only where there is measure for it. Its sample title is short
       ("NAGEL Citoborma 111, 1-Spindelpapierbohrmaschine"); the real ones carry the full
       specification, and at 40px in a 360px phone column this one ran to five lines and 192px.
       Scaled by viewport with a 40px ceiling, so the design value is what a desktop gets. */
    font-size: clamp(1.75rem, 1.1rem + 2.2vw, 2.5rem);
    font-weight: 700;
    line-height: 1.2;
    color: var(--bbb-navy);
    margin-bottom: 0.5rem;
}

/* The meta line above the title: 14px, muted, as the mock-up's "ART-NR: 1162000".
   Rating and stock stay in it — the mock-up simply does not draw them, but they are
   real features and this is where they belong. */
.details__info {
    align-items: center;
    gap: 1rem;
    font-size: 0.875rem;
    color: var(--bbb-text-muted);
    margin-bottom: 0.5rem;
}

.details__info-artnum {
    text-transform: uppercase;
    font-weight: 700;
    color: inherit;
}

/* Price block bounded by hairlines top and bottom (mock-up), 24px inside. */
.details__price {
    padding-block: 1.5rem;
    border-top: 1px solid var(--bbb-border);
    border-bottom: 1px solid var(--bbb-border);
    margin-bottom: 1.5rem;
    gap: 0;
}

/* The parent reserves `calc(100% - 160px)` on every price line for the absolutely
   positioned share block (see .details__share below). With that block back in the
   flow the reservation only truncates the price. */
.details__price-old,
.details__price-main,
.details__price-vat {
    width: 100%;
}

.details__price-old {
    font-size: 0.875rem;
}

/* Mock-up: 32px / 700. Parent: 2rem / 800 — same size, one step heavier. */
.details__price-main {
    font-weight: 700;
    color: var(--bbb-navy);
}

.details__price-vat {
    font-size: 0.875rem;
    font-style: italic;
}

/* THE GREY SQUARE OVER THE PRICE, fixed. o3-theme floats the share/wishlist block
   `position: absolute; width: 160px; bottom: 112px; right: 0` with 72x72 grey tiles —
   which is what has been overlapping the price block (a known open item). The mock-up
   has these as a row of small text links under the CTA, so putting the block back in
   the flow is both the fix and the design. */
.details__share {
    position: static;
    width: 100%;
    justify-content: flex-start;
    margin-top: 1.5rem;
    padding-top: 1rem;
    border-top: 1px solid var(--bbb-border);
    order: 3;
    font-size: 0.875rem;
}

.details__share > a {
    width: auto;
    height: auto;
    background-color: transparent;
    color: var(--bbb-navy);
    gap: 0.25rem;
}

.details__share > a:hover {
    color: var(--bbb-orange);
}

/* Quantity stepper + CTA on one 48px-tall row (mock-up). The parent's 52px tiles with
   10px margins come to 72px and dwarf the row.

   QM: "da fehlt ein weißer BG und Rand". o3-theme paints the stepper buttons in
   --bs-secondary and outlines nothing, and it has a *separate* `:disabled` rule
   (`background-color: #f5f5f5; border: none`) that outranked the plain-class override here —
   which is why the minus button, disabled at quantity 1, stayed grey while the plus went
   white. Hence the `:disabled` selectors: same specificity as the parent's, and this file
   loads after it.

   The mock-up (html/produktdetailseite-2.html) draws the group as one bordered pill:
   `.qty-btn { border: 1px solid #ced4da; background: white }` with the input carrying only
   the top and bottom edge. Bordering the three parts rather than .component__tobasket
   itself, because this theme's markup keeps the orange CTA inside that same element — a
   border there would loop around the button too.

   QM, second round: "Rahmen fehlt". It was not missing — it was drawn in --bbb-border (#e2e8f0),
   the hairline this theme uses for surfaces on the ice-blue and grey panels, and against the white
   purchase column that is invisible at 1x. The mock-up's own control border is a full step darker
   (`.qty-btn { border: 1px solid #ced4da }`), which is what makes it read as a control rather than
   as a seam; taken literally here, on the three stepper cells only. */
.details__wrapper .component__tobasket-minus,
.details__wrapper .component__tobasket-plus,
.details__wrapper .component__tobasket-minus:disabled,
.details__wrapper .component__tobasket-plus:disabled {
    width: 42px;
    height: 46px;
    margin: 0;
    background-color: #fff;
    border: 1px solid #ced4da;
}

/* The three cells' borders and radii are set together further down, where the stepper became
   its own bordered group. */
.details__wrapper .component__tobasket-input {
    width: 60px;
    flex: 0 0 auto;
    background-color: #fff;
}

/* Mock-up's main CTA is the accent orange, not the navy the card buttons use — it is
   the one primary action on the page. Scoped to the details tobasket so the cards and
   every other btn-primary keep the navy. */
.details__wrapper .component__tobasket-submit {
    background-color: var(--bbb-orange);
    border-color: var(--bbb-orange);
    font-weight: 700;
    font-size: 1rem;
    justify-content: center;
    min-height: 46px;
}

.details__wrapper .component__tobasket-submit:hover,
.details__wrapper .component__tobasket-submit:focus {
    background-color: var(--bbb-orange-dark);
    border-color: var(--bbb-orange-dark);
}

/* Image frame — the mock-up's `.product-image-container`: ice-blue, 24px padding, hairline
   border, 12px radius, shadow-sm. o3-theme leaves the picture unframed and unconstrained, so a
   portrait photo (531x742 on this product) towered over the purchase column beside it.

   Square box with the image contained, rather than the mock-up's odd inline `height: 67%`:
   its box comes out 534x516, i.e. near enough square, and a fixed ratio is what keeps a
   portrait and a landscape product looking like the same shop. Same reasoning as the fixed
   390:245 box on the cards. The photos are shot on this ice blue, so the letterboxed area
   blends into the picture instead of framing it. */
.details__picture-main {
    display: flex;
    align-items: center;
    justify-content: center;
    aspect-ratio: 1 / 1;
    padding: 1.5rem;
    background: var(--bbb-ice);
    border: 1px solid var(--bbb-border);
    border-radius: 0.75rem;
    box-shadow: 0 2px 4px rgba(0, 0, 0, 0.075);
    overflow: hidden;
}

.details__picture-main .details__picture-zoom-link,
.details__picture-main img {
    width: 100%;
    height: 100%;
    object-fit: contain;
}

/* With several pictures the img sits in splide's track/list/slide. splide's own CSS gives
   the list `height: 100%` and the slide stretches to it, but the TRACK has no height, so the
   img's `height: 100%` had nothing to resolve against and fell back to its intrinsic aspect:
   full width, 896px tall in a 786px square, bottom clipped by `overflow: hidden`
   (QM 2026-09-03: "Warum ist das abgeschnitten?"). One definite height closes the chain. */
.details__picture-main .splide__track {
    height: 100%;
}

/* Thumbnail strip (mock-up: 100px tall tiles on the same ice blue, hairline, 6px radius,
   8px padding). Only rendered when a product has more than one picture. */
.details__picture-thumbnails {
    margin-top: 1rem;
}

.details__picture-thumbnails img {
    /* Square tiles: splide sets the width (perPage), the height follows it. On phones
       splide's inline fixedWidth/fixedHeight 66 win over this, and are square anyway. */
    aspect-ratio: 1 / 1;
    height: auto;
    padding: 0.5rem;
    background: var(--bbb-ice);
    border: 1px solid var(--bbb-border);
    border-radius: 0.375rem;
    object-fit: contain;
}

.details__picture-thumbnails img.is-active {
    border-color: var(--bbb-navy);
}

/* Thumbnail arrows. QM: "Das sind die verkehrten", pointing at the two white 40px discs that
   splide's own style (o3-theme's `.splide__arrow`: circle, drop shadow, `position: absolute`,
   `left/right: -20px`) parks ON TOP of the first and last tile.

   The mock-up puts them in their own column either side of the strip — a bare `chevron_left` /
   `chevron_right` Material glyph at `fs-3`, no disc, no shadow, nothing overlapping. Same icon
   font the header, the card buttons and the basket already use (see the @font-face block at the
   bottom of this file), so this is one more consumer of it rather than a new drawing.

   Laid out with the strip as a flex row and `display: contents` on splide's arrow wrapper, which
   lifts the two buttons into that row without touching splide's markup; `order` then puts the
   track between them. */
.details__picture-thumbnails {
    display: flex;
    align-items: center;
    gap: 0.5rem;
}

.details__picture-thumbnails .splide__arrows {
    display: contents;
}

.details__picture-thumbnails .splide__track {
    order: 1;
    flex: 1 1 auto;
    /* Without it the track refuses to shrink below its content and pushes the next arrow out. */
    min-width: 0;
}

.details__picture-thumbnails .splide__arrow--next {
    order: 2;
}

/* Fewer thumbnails than fit: keep the strip centred under the picture instead of left-aligned
   (splide only adds `is-overflow` when the list is wider than the track). */
.details__picture-thumbnails:not(.is-overflow) .splide__list {
    justify-content: center;
}

.details__picture-thumbnails .splide__arrow {
    position: static;
    /* splide's `top: 50%; transform: translateY(-50%)` — `top` is inert once static, but the
       transform still lifted the arrows 14px above the strip's centre. */
    transform: none;
    flex: 0 0 auto;
    width: auto;
    height: auto;
    padding: 0;
    background: none;
    box-shadow: none;
    color: var(--bbb-navy);
}

.details__picture-thumbnails .splide__arrow:hover {
    color: var(--bbb-orange);
}

.details__picture-thumbnails .splide__arrow svg {
    display: none;
}

.details__picture-thumbnails .splide__arrow--prev::before {
    content: "chevron_left" / "";
}

.details__picture-thumbnails .splide__arrow--next::before {
    content: "chevron_right" / "";
}

/* --------------------------------------------------------------------------
   Detail tabs — mock-up: left-aligned row, then a white card for the panel.
   o3-theme centres them and gives every link a fixed 215px width, which made the four
   labels wrap onto two rows; the panel itself has no surface at all.
   -------------------------------------------------------------------------- */
.details__tabs,
main .nav-tabs {
    justify-content: flex-start;
    flex-wrap: nowrap;
    max-width: 100%;
    width: 100%;
    overflow-x: auto;
}

main .nav-tabs .nav-link {
    width: auto;
    flex: 0 0 auto;
    padding: 0.5rem 1rem;
    font-size: 1rem;
    font-weight: 700;
    color: var(--bbb-text-variant);
    text-align: left;
    white-space: nowrap;
    /* Transparent rather than absent, as in the mock-up: the active tab's 2px underline then
       replaces a border that is already there instead of adding 2px to that one tab's height. */
    border: 0;
    border-bottom: 2px solid transparent;
}

/* QM: "graues Ding weg machen" — the active tab sat in a grey box. That is Bootstrap's own
   `--bs-nav-tabs-link-active-bg` painting the tab like a folder, which is the look this rule
   was already overriding at the bottom edge and nowhere else. The mock-up's tab is an
   underline and nothing more (produktdetailseite-2.html: `background: none` on `.active`). */
main .nav-tabs .nav-link.active {
    color: var(--bbb-navy);
    background: none;
    border-bottom-color: var(--bbb-navy);
}

/* QM: the datasheet rows should read like the mock-up's Downloads block
   (html/produktdetailseite-2.html) — a navy heading over a plain list whose rows carry a PDF
   icon, rather than bare links divided by hairlines. The markup comes from this theme's own
   page/details/inc/media.tpl; the icon is drawn here so a row needs nothing but its link. */
.details__media-heading {
    margin-bottom: 1rem;
    font-size: 1.125rem;
    font-weight: 700;
    color: var(--bbb-navy);
}

.details__media-downloads li + li {
    margin-top: 0.5rem;
}

.details__media-downloads a {
    display: inline-flex;
    align-items: center;
    gap: 0.5rem;
    color: var(--bbb-text);
    text-decoration: none;
}

.details__media-downloads a::before {
    content: "";
    flex: 0 0 auto;
    width: 20px;
    height: 20px;
    background: url("../../img/icon-pdf.svg") center / contain no-repeat;
}

.details__media-downloads a:hover {
    color: var(--bbb-orange);
}

.details__media-video {
    margin-top: 1.5rem;
}

main .tab-content {
    background: #fff;
    padding: 1.5rem;
    border: 1px solid var(--bbb-border);
    border-radius: 0.375rem;
    box-shadow: 0 2px 4px rgba(0, 0, 0, 0.075);
    margin-top: 1.5rem;
}

/* The panes are `col-12 col-lg-8` in o3-theme, i.e. two thirds of the panel; the mock-up's
   content fills it. */
main .tab-content > .tab-pane {
    width: 100%;
    max-width: 100%;
    flex: 0 0 100%;
    padding: 0;
}

/* Specification pane. The mock-up shows a striped two-column table (label left, value right);
   o3-theme renders no table at all — the attributes are flat `<div><span>label</span> value</div>`
   pairs, which is why an earlier `table`-based rule here did nothing. Same result from the real
   markup: each pair becomes a two-column striped row.

   The mock-up's "Technische Daten / Wert" header row has no equivalent in the data and is not
   faked here. */
/* o3-theme flows the attribute pairs into newspaper columns (`column-width: 200px`), so each
   pair was only ~240px wide and the two-column rows below collapsed to 100px tracks. The
   mock-up's specification is one full-width table. */
.details__attributes {
    columns: auto;
    column-width: auto;
}

.details__attributes > div {
    display: grid;
    grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
    gap: 1rem;
    padding: 0.5rem 0.75rem;
}

.details__attributes > div:nth-of-type(odd) {
    background-color: var(--bbb-surface);
}

/* The label is the only element in the pair; the value is the div's own text node, so it cannot
   be selected — hence text-align on the row and a left-aligned label. */
.details__attributes > div {
    text-align: right;
}

.details__attributes > div > span {
    text-align: left;
    font-weight: 700;
    color: var(--bbb-navy);
}

/* Recommendation section headings: mock-up puts a hairline under each, 16px below the text.
   Selected through the slider wrapper the four sections actually use (see
   tpl/page/details/inc/related_products.tpl) — they carry no section class of their own — and
   scoped to the detail page so the start page's rows, which draw a rule beside the heading
   instead, are not touched. */
body.cl-details .component__productslider > h2.title-big,
body.cl-details .component__productgrid > h2.title-big {
    padding-bottom: 1rem;
    border-bottom: 1px solid var(--bbb-border);
}

/* "Ähnliche Produkte" is a plain grid rather than a slider (see
   tpl/page/details/inc/related_products.tpl): its tiles come from widget/product/list.tpl and
   already carry the PLP's .component__productbox-frame, so it must NOT also take
   .component__productslider, whose card styling on .component__productbox would draw a second
   border around the first. Only the section box is shared — o3-theme's 80px gap and the
   content margin. */
.component__productgrid {
    margin-bottom: 80px;
    padding-left: var(--content-inner-margin, 0px);
    padding-right: var(--content-inner-margin, 0px);
}

/* Interaction bar and trust line (productmain.tpl additions). The mock-up has them as small
   navy text rows under the CTA — the parent had only a bare icon here. */
.details__share > a {
    display: inline-flex;
    align-items: center;
    text-decoration: none;
    white-space: nowrap;
}

.details__share svg path,
.details__share svg circle {
    stroke: currentColor;
}

.details__trust {
    display: flex;
    align-items: center;
    gap: 0.375rem;
    margin-top: 1rem;
    font-size: 0.875rem;
    color: var(--bbb-navy);
    width: 100%;
    order: 2;
}

/* Variant / select-list dropdowns on the detail page.

   Measured against the mock-up's `.form-label` + `.form-select` pair:
     label   14px / 700 / uppercase / muted, 8px below
     select  38px tall, 6px radius, padding 6px 36px 6px 12px
   o3-theme wraps the pair in `.details__variants` with 16px padding *and* a 1px border, and
   its select is 46px with square corners — a bordered box around a bordered control, and the
   whole thing a third taller than the design. The native option list inherits the control's
   font-size, so this also shrinks the open dropdown. */
form.js-oxProductForm.details .details__variants {
    padding: 0;
    border: 0;
    margin-bottom: 1rem;
}

form.js-oxProductForm.details .form-label {
    margin-bottom: 0.5rem;
    font-size: 0.875rem;
    font-weight: 700;
    text-transform: uppercase;
    color: var(--bbb-text-muted);
}

/* QM: "#fff" on the variant picker. Bootstrap paints a `.form-select` in `--bs-body-bg`, which
   this theme sets to the page grey (#f7f9fb) — so the one control in the purchase column came out
   the same colour as the page behind it, with only the hairline separating them. The mock-up's
   `.form-select` is the Bootstrap default, i.e. white.

   Scoped, as the same repair already is on the account and checkout forms further down. The cause
   is shared (`--bs-body-bg`), so the theme-wide alternative is to give the token back its white
   and paint the page from `--bbb-surface` instead — a bigger change than one QM note, since
   `--bs-body-bg` also backs modals, dropdowns and every `.form-control`. */
form.js-oxProductForm.details .form-select {
    min-height: 0;
    padding: 0.375rem 2.25rem 0.375rem 0.75rem;
    border-radius: 0.375rem;
    border-color: var(--bbb-border);
    background-color: #fff;
    background-position: right 0.75rem center;
}

/* The add-to-cart row was 76px against the mock-up's 48px, and the cause is the same 52x52
   cart glyph that had to be tamed on the product cards: o3-theme ships it at its literal
   52px, so 52 + 20px padding + border = 74.

   QM: "Verkehrtes Icon". Scaled down to 20px it was still the wrong drawing — o3-theme's glyph
   is a shopping **bag**, the mock-up's is `shopping_cart`, exactly the difference already
   settled for the header icons and the product cards. Drawn from the same self-hosted Material
   Icons Outlined font as those, which also deletes the four rules that used to tame the svg
   (the 52-unit viewBox, its `<rect>` tint panel, `non-scaling-stroke` and the reordering: a
   `::before` is the first flex item by construction, so the glyph leads the label as the
   mock-up has it). */
.details__wrapper .component__tobasket-submit {
    padding-block: 0;
    /* The mock-up types the label in caps ("IN DEN WARENKORB"); doing it in CSS keeps the
       shop's own TOBASKET ident intact and translatable. */
    text-transform: uppercase;
    gap: 0.5rem;
}

.details__wrapper .component__tobasket-submit svg {
    display: none;
}

/* 24px, i.e. the block default below — the same size as the mock-up's
   `.material-symbols-outlined` here and as the cart icon on the product cards. */
.details__wrapper .component__tobasket-submit::before {
    content: "shopping_cart" / "";
}

/* Parent gives the label `flex-grow: 1`, which pushes icon and text to opposite ends of the
   button instead of centring them as a pair. */
.details__wrapper .component__tobasket-submit span {
    flex: 0 0 auto;
    padding: 0;
}

/* --------------------------------------------------------------------------
   Purchase column, matched to the zoomed mock-up of the hero's right side.
   -------------------------------------------------------------------------- */

/* With the rating and stock badge out of the meta line (productmain.tpl) the Art-Nr is on its
   own, so the row no longer needs to space three items apart. */
.details__info {
    justify-content: flex-start;
}

/* Mock-up: the quantity stepper is its **own** bordered group with a divider between each cell,
   then a gap, then the orange button. o3-theme draws one border around the whole thing —
   stepper and button inside a single frame. */
.details__wrapper .component__tobasket {
    border: 0;
    border-radius: 0;
    overflow: visible;
    align-items: stretch;
}

.details__wrapper .component__tobasket-minus {
    border-radius: 0.375rem 0 0 0.375rem;
}

.details__wrapper .component__tobasket-plus {
    border-radius: 0 0.375rem 0.375rem 0;
}

/* Top and bottom only — its neighbours' borders supply the two dividers, exactly as the
   mock-up's `.quantity-input` (`border-width: 1px 0`) does. Same #ced4da as the two buttons. */
.details__wrapper .component__tobasket-input {
    border-top: 1px solid #ced4da;
    border-bottom: 1px solid #ced4da;
}

/* QM: "Icons entsprechen nicht dem Entwurf". o3-theme's stepper glyphs are two svgs on a
   20-unit viewBox — the minus a line across all 20 units, so it renders as a 20px dash rather
   than a minus sign, and the plus a 20x20 cross. Next to each other they are neither the same
   optical size nor the same weight, and neither is the mock-up's plain "-" / "+".

   Replaced from the icon font the rest of this theme's controls already draw from, the same move
   that settled the three-sources icon row on the product cards (tasks/lessons.md, 2026-08-13):
   `remove` and `add` are one drawing at one weight, and they scale with the button.

   `:disabled` keeps o3-theme's grey — it used to come from its `:disabled svg path { stroke: #888 }`,
   which has nothing left to paint now. The minus is disabled at quantity 1, i.e. on page load. */
.details__wrapper .component__tobasket-minus svg,
.details__wrapper .component__tobasket-plus svg {
    display: none;
}

.details__wrapper .component__tobasket-minus::before {
    content: "remove" / "";
}

.details__wrapper .component__tobasket-plus::before {
    content: "add" / "";
}

/* QM, third round on this cell: "it still missing". The border really was gone — and no amount of
   specificity was going to bring it back, because o3-theme ships

       .btn:active, .btn:disabled { border-color: transparent !important }

   and the minus is disabled at quantity 1, i.e. every time the page loads. `!important` is the
   only thing that answers an `!important`; kept to these two cells rather than lifted off `.btn`,
   where the parent presumably wants it.

   `opacity` goes with it: Bootstrap fades a disabled `.btn` to 0.65, which would have left this
   one cell's frame a step lighter than the other two — the same "is there a border or isn't
   there" the report is about. The disabled state still reads, in the grey glyph. */
.details__wrapper .component__tobasket-minus:disabled,
.details__wrapper .component__tobasket-plus:disabled {
    color: #888;
    border-color: #ced4da !important;
    opacity: 1;
}

/* Gap between the stepper group and the button. Set as a margin rather than a flex `gap` on the
   container, which would also separate the three stepper cells and break the shared borders. */
.details__wrapper .component__tobasket-submit {
    margin-left: 0.75rem;
    border-radius: 0.375rem;
}

/* "UVP" sits inside the <del> so it is struck through with the figure (mock-up). */
.details__price-old {
    height: auto;
}

/* "nur" is the same small plain label as "ab" before it (QM 2026-09-08): both spans fall under
   o3-theme's `.details__price-main span` (1rem, #121212, regular), so no rule of our own here. */

/* Three evenly spaced links in the interaction bar (mock-up: Empfehlen · Auf den Merkzettel ·
   Fragen zum Artikel).

   QM: "Verkehrte Font size + bold machen". Bold here; the size is left at the 0.875rem the
   mock-up itself specifies for this row (`small`, and measured at 14px in the render QM
   annotated) until QM says which way it should move — see tasks/todo.md. */
.details__share {
    flex-wrap: wrap;
    gap: 0.5rem 2rem;
    font-weight: 700;
}

/* --------------------------------------------------------------------------
   Basket line items — mock-up html/Wk1.html.
   -------------------------------------------------------------------------- */

/* The plus button was **ice blue**: o3-theme paints the stepper's buttons in
   `var(--bs-secondary)`, and this theme's secondary *is* the ice blue (theme.php). Next to a
   grey disabled minus that read as an accidental highlight. Both cells now match the detail
   page's stepper — white with hairline dividers. Theme-wide rather than scoped, because the
   only other place the stepper appears is the card quick-add, which hides these two buttons. */
.component__tobasket-minus,
.component__tobasket-plus {
    background-color: #fff;
}

.component__tobasket-minus:disabled,
.component__tobasket-plus:disabled {
    background-color: var(--bbb-surface);
}

main .component__basketarticle .component__tobasket {
    border-color: var(--bbb-border);
    border-radius: 0.375rem;
    overflow: hidden;
    flex: 0 0 auto;
}

/* --- The five-column table -------------------------------------------------
   Mock-up: Bild | Artikeldetails | Einzelpreis | Menge | Gesamtbetrag, with a header row.
   o3-theme renders one flex row per item with everything after the picture nested inside
   `.component__basketarticle-info`.

   That nesting is dissolved with `display: contents` rather than by restructuring the
   template: the info wrapper carries no semantics and no styling worth keeping, so letting
   its children become grid items of the row is the whole change. The alternative — a real
   <table> — would mean rewriting the parent's item template around the `tobasket.tpl`
   include and the module block hooks it carries.

   The track widths live in one custom property so the header and the rows cannot drift.
   -------------------------------------------------------------------------- */
.basket__articles {
    --bbb-basket-cols: 100px minmax(0, 1fr) 150px 150px 130px;
}

.component__basketheader,
main .component__basketarticle {
    display: grid;
    grid-template-columns: var(--bbb-basket-cols);
    column-gap: 1rem;
    align-items: center;
}

.component__basketheader {
    padding-bottom: 1rem;
    border-bottom: 1px solid var(--bbb-border);
    font-size: 0.875rem;
    font-weight: 600;
    color: #666;
}

.component__basketheader > span:nth-child(3),
.component__basketheader > span:nth-child(5) {
    text-align: right;
}

.component__basketheader > span:nth-child(4) {
    text-align: center;
}

main .component__basketarticle {
    /* Three explicit rows so the cells that span the row group can say `1 / 4`. Column 2
       stacks title / Art-Nr / Entfernen down them; a missing one just collapses to 0. */
    grid-template-rows: auto auto auto;
    row-gap: 0.25rem;
    padding-block: 1.5rem;
    border-bottom: 1px solid var(--bbb-border);
}

main .component__basketarticle > :first-child {
    grid-column: 1;
    grid-row: 1 / 4;
}

main .component__basketarticle-info {
    display: contents;
}

/* Mock-up: the thumbnail sits on the ice blue it was shot against, in a hairline frame —
   the same treatment as the product cards. o3-theme leaves it a bare 74px image. */
main .component__basketarticle img {
    width: 80px;
    height: 80px;
    padding: 0.25rem;
    background: var(--bbb-ice);
    border: 1px solid var(--bbb-border);
    border-radius: 0.25rem;
    object-fit: contain;
}

main .component__basketarticle-title {
    grid-column: 2;
    grid-row: 1;
    font-weight: 700;
    color: var(--bbb-navy);
    text-decoration: none;
    /* The title track is `minmax(0, 1fr)`, so it never grows for its content — and this
       catalogue is full of words no browser can break ("Spindelpapierbohrmaschine"), which
       then spill out of the cell and print over the next column. Wrap mid-word rather than
       overflow; `anywhere` (not `break-word`) so the long word also stops the track from
       being sized by it. */
    overflow-wrap: anywhere;
}

/* Article number, restored in the template override. Mock-up: small and muted, directly
   under the title. The unit price used to live here too and is now its own column. */
main .component__basketarticle-details {
    grid-column: 2;
    grid-row: 2;
    align-items: baseline;
    font-size: 0.875rem;
    color: #6c757d;
}

main .component__basketarticle-unitprice {
    grid-column: 3;
    grid-row: 1 / 4;
    text-align: right;
}

main .component__basketarticle .component__basketquantity {
    grid-column: 4;
    grid-row: 1 / 4;
    justify-self: center;
}

main .component__basketarticle-price {
    grid-column: 5;
    grid-row: 1 / 4;
    text-align: right;
    color: var(--bbb-navy);
    font-weight: 700;
}

/* Mock-up removes an item with a red "✕ Entfernen" text button under the article number.
   o3-theme's is a grey bin icon pulled out of the row with negative margins so that it
   overlaps the product name's line. */
main .component__basketarticle-remove {
    grid-column: 2;
    grid-row: 3;
    justify-self: start;
    display: inline-flex;
    align-items: center;
    gap: 0.25rem;
    margin: 0;
    padding: 0;
    border: 0;
    background: none;
    font-size: 0.75rem;
    line-height: 1.2;
    color: var(--bs-danger);
}

main .component__basketarticle-remove:hover,
main .component__basketarticle-remove:focus {
    color: #a71d2a;
    text-decoration: underline;
}

/* On a quotation-only product, TroRequestArticleQuotation replaces the cart row with
   `.tobasketFunction > button.btn-primary.submitButton.largeButton` — which came out as a 185px
   navy button floating in a 531px column, i.e. reading as a secondary action. It *is* the page's
   primary action, so it takes the mock-up's CTA treatment: full width, accent orange, same height
   and radius as "IN DEN WARENKORB".

   Scoped to .details__wrapper: on the product cards the same module renders a small navy mail
   icon, which is right there and must not change. */
.details__wrapper .tobasketFunction {
    flex: 0 0 100%;
    width: 100%;
}

.details__wrapper .tobasketFunction .btn-primary {
    display: flex;
    align-items: center;
    justify-content: center;
    width: 100%;
    min-height: 46px;
    border-radius: 0.375rem;
    font-weight: 700;
    text-transform: uppercase;
    --bs-btn-bg: var(--bbb-orange);
    --bs-btn-border-color: var(--bbb-orange);
    --bs-btn-hover-bg: var(--bbb-orange-dark);
    --bs-btn-hover-border-color: var(--bbb-orange-dark);
    --bs-btn-active-bg: var(--bbb-orange-dark);
    --bs-btn-active-border-color: var(--bbb-orange-dark);
}

/* --------------------------------------------------------------------------
   Every dark hairline o3-theme draws across the header — removed, not hidden.

   `content: none` rather than `display: none`: with `display: none` the pseudo-element is still
   generated and only kept from painting, which is what left a black line flashing into view before
   the override settled. With `content: none` it is never created at all.

   Three separate sources, and overriding only one of them is why a line survived:
     1. `.header__mainnav::after`  — 1px rgba(17,34,17,.13) across the nav row. It made sense while
        the nav was its own row; on the one-row header it cuts straight through it.
     2. `header.is-minimal::after` — the same hairline under the reduced checkout header.
     3. `.cl-basket header`        — a real `border-bottom` in that colour, only on the basket page,
        at (0,2,1) specificity so a bare `header { }` rule never had a chance against it. Same for
        the boxed `has-bg-image` variant on the container.
   The mock-up separates the header with a shadow; see the `header` rule above.
   -------------------------------------------------------------------------- */
.header__mainnav::after,
header.is-minimal::after {
    content: none;
}

.cl-basket header,
body.has-bg-image.cl-basket > header > .container-xxl {
    border-bottom: 0;
}

/* --------------------------------------------------------------------------
   Header action icons — one size, even spacing (mock-up `.mh-header-icons`).

   o3-theme ships each glyph at its own size and lets the buttons' padding set the spacing, which
   came out as 18px / 20px / 18x23px icons with 23px and 5px gaps. The mock-up has all three at
   24px with a uniform 16px gap and a hairline divider between search and account.

   Height rather than width is normalised: the cart glyph's viewBox is 18x23, so forcing a square
   would stretch it. Equal height is what reads as "same size" anyway.
   -------------------------------------------------------------------------- */
.header__search-toggle svg,
.header__actions-account svg,
.header__actions-cart svg,
.header__actions-noticelist svg {
    height: 24px;
    width: auto;
}

/* Padding off, so the flex gap below is the actual distance between glyphs rather than
   padding-plus-gap. */
.header__search-toggle,
.header__actions > .btn,
.header__actions > a {
    padding: 0;
}

.header__actions {
    align-items: center;
    gap: 1rem;
}

/* Mock-up's `<div class="vr mx-1">` between search and account: 1px, 24px tall, 25% of the
   near-black text colour. Decoration only, so a pseudo-element is the right place for it. */
header:not(.is-minimal) .header__actions::before {
    content: "";
    align-self: center;
    flex: 0 0 auto;
    width: 1px;
    height: 24px;
    background-color: #191c1e;
    opacity: 0.25;
    /* The mock-up's `mx-1` on that divider, so the 16px gaps either side become 20px and the
       divider sits symmetrically between the search and account glyphs. */
    margin-inline: 0.25rem;
}

/* It divides the search icon from the account icon, and stays off below xl — the mock-up's own
   divider is `<div class="vr d-none d-lg-block mx-1">`, i.e. it drops out on a phone, where the
   group is three plain icons. (The geometry would work either way now that the search glyph is on
   their line: this pseudo-element is the actions container's first child, so it falls between the
   two. It was originally off because the search was a field on a row of its own.) */
@media (max-width: 1199.98px) {
    header:not(.is-minimal) .header__actions::before {
        content: none;
    }
}

/* Mock-up badge: 10px, and sitting on the glyph's top-right *corner* — overhanging it rather than
   covering it. o3-theme's `top: 5px; right: 3px` was measured against the button's old padding; with
   the padding removed above, those values dropped the badge straight onto the cart glyph. */
.header__actions .badge-counter {
    font-size: 10px;
    top: -7px;
    right: -8px;
}

@media (min-width: 1200px) {
    /* `ms-lg-4` in the mock-up — 24px between the nav and the icon group, then the group's own
       16px gaps carry the rest.

       The top margin is the nav row's drop (see --bbb-navrow-drop, where the 6px is derived):
       doubled, because these are centred flex items and a centred box moves by half of what you
       add above it. It lives in this shorthand rather than beside the property that defines it —
       a `margin` shorthand later in the file would reset a lone `margin-top` set earlier. */
    header:not(.is-minimal) .header__search {
        margin: calc(var(--bbb-navrow-drop, 0px) * 2) 0 0 1.5rem;
    }

    /* No extra margin: the header row's own 15px flex gap is the lead-in, so with the divider's
       4px margins the spacing lands symmetric (19px | divider | 20px) instead of 31px | 16px. */
    header:not(.is-minimal) .header__actions {
        margin-top: calc(var(--bbb-navrow-drop, 0px) * 2);
        margin-left: 0;
    }

    /* QM: "Gute Idee die Nav Punkte auszublenden. Aber die Icons müssen rechtsbündig sein."
       The checkout pages render no category nav at all — `.header__mainnav` is absent from the
       DOM, not merely hidden — so the element that normally grows into the row's free space and
       carries the icons out to the container's right edge is not there, and search + actions
       ended up parked against the logo with ~750px of empty row after them.

       `:has()` on the nav's absence rather than on `.is-minimal`: the basket page is the odd one
       out — it keeps the full header class but drops the nav, and the basket is what QM shot.
       Second selector is the fallback for a reduced header that has no search either.

       Must stay in THIS block, after the `margin: 0 0 0 1.5rem` above: same specificity, so
       source order decides. */
    header:not(:has(.header__mainnav)) .header__search,
    header:not(:has(.header__mainnav)):not(:has(.header__search)) .header__actions {
        margin-left: auto;
    }
}

/* --------------------------------------------------------------------------
   The header row must never wrap the action icons onto a second line.

   `flex-wrap: wrap` on the header container is required (the navy top bar takes its own row), so if
   the logo row overflows, flexbox wraps it — and because the icons come last in `order`, they land
   at the far left of a new row under the logo. That is what the padding budget above prevents in
   the normal case; these two rules make it structurally impossible in any case:

     - search and actions never shrink and never wrap away,
     - the nav MAY shrink, which is what flexbox prefers over wrapping — but only once
       `min-width: 0` lifts the automatic minimum of its content. With the default `min-width: auto`
       a flex item refuses to go below its content width, which leaves wrapping as the only option.

   So in a genuinely too-narrow moment the nav clips its last label instead of the header
   reorganising itself. Only at xl, the one width range that uses this layout.
   -------------------------------------------------------------------------- */
@media (min-width: 1200px) {
    header:not(.is-minimal) .header__search,
    header:not(.is-minimal) .header__actions {
        flex: 0 0 auto;
    }

    header:not(.is-minimal) .header__mainnav {
        min-width: 0;
        overflow: hidden;
    }
}

/* --------------------------------------------------------------------------
   Header icons — the designer's own glyphs.

   Sizing them was never going to make them match: they are different drawings. The mock-up uses
   Material icons `search` / `account_circle` / `shopping_cart`, o3-theme ships a magnifier, a bare
   person outline (no circle) and a shopping **bag** (no trolley).

   The designer shipped the font — html/fonts/material-icons-outlined-v110-latin-regular.woff2 — so
   it is self-hosted here, never fetched from Google (same rule as Hanken Grotesk: no visitor IPs to
   Google).

   Swapped in CSS rather than by overriding the two widget templates that render those SVGs
   (oxwServiceMenu, oxwMiniBasket): the glyph is presentation, and all three controls already carry
   real accessible names ("Suche", "Anmelden / Registrieren"), which the SVG never provided anyway.
   `content: "…" / ""` gives the pseudo-element explicitly empty alternative text, so the ligature
   name is not exposed to assistive tech.
   -------------------------------------------------------------------------- */
@font-face {
    font-family: "Material Icons Outlined";
    src: url("../font/material-icons-outlined-v110-latin-regular.woff2") format("woff2");
    font-weight: 400;
    font-style: normal;
    font-display: block;
}

.header__search-toggle svg,
.header__actions-account svg,
.header__actions-cart svg {
    display: none;
}

.header__search-toggle::before,
.header__actions-account::before,
.header__actions-cart::before,
.basket__continue::before,
.basket__actions .basket__proceed .btn::after,
main .component__basketarticle-remove::before,
.component__basketquantity-update::before,
.component__productbox-action .btn-primary::before,
.details__wrapper .component__tobasket-submit::before,
.details__wrapper .component__tobasket-minus::before,
.details__wrapper .component__tobasket-plus::before,
.details__picture-thumbnails .splide__arrow::before,
.order__notice::before,
.order .title-small button::before,
.order .title-small .btn::before,
.component__steps-step.passed .listing-point::before,
.order__navi > button[type="submit"]::before,
.payment__payments .form-check-input:checked ~ .form-check-label .desc::before,
.user__navi > a.btn::before,
.user__navi > button[type="submit"]::after,
.payment__navi > a.btn::before,
.payment__navi > button[type="submit"]::after,
.start__newscard a::after,
.checkout__options > form > .btn::after,
.checkout__options > form > button[type="submit"]::after,
.checkout__options .list-benefits li::before,
.cl-user main form > .row > div > .title-small::before {
    font-family: "Material Icons Outlined";
    font-weight: 400;
    font-style: normal;
    font-size: 24px;
    line-height: 1;
    letter-spacing: normal;
    text-transform: none;
    white-space: nowrap;
    direction: ltr;
    /* Both are required: the glyphs are ligatures of their own names, so without liga the literal
       text renders instead. */
    font-feature-settings: "liga";
    -webkit-font-feature-settings: "liga";
    -webkit-font-smoothing: antialiased;
}

.header__search-toggle::before {
    content: "search" / "";
}

.header__actions-account::before {
    content: "account_circle" / "";
}

.header__actions-cart::before {
    content: "shopping_cart" / "";
}

/* The two glyphs that are not 24px. Here rather than beside their `content` further up, because
   the declaration block above sets the size for every selector it lists and comes after them. */
.details__wrapper .component__tobasket-minus::before,
.details__wrapper .component__tobasket-plus::before {
    font-size: 20px;
}

/* fs-3 in the mock-up = 1.75rem. */
.details__picture-thumbnails .splide__arrow::before {
    font-size: 28px;
}

/* ==========================================================================
   Checkout — mock-ups html/Wk1.html … html/Wk4.html
   ========================================================================== */

/* --------------------------------------------------------------------------
   Step indicator (all checkout pages).

   Mock-up `.mh-stepper`: 40px circles in a row, label UNDERNEATH, a 2px track running
   through the circles' centres that is navy up to the step you are on. o3-theme instead
   puts the label beside the number and draws one grey line across the whole row
   (`.component__steps::before`, top 50%, z-index -1) which every step punches through with
   the page background. Both of those have to go.

   The track is drawn per step rather than as one line plus a percentage progress overlay:
   with every step the same width, a segment anchored at `right: 50%` with `width: 100%`
   runs exactly from this circle's centre to the previous circle's centre. So "how far along
   is the bar" needs no arithmetic at all — only the `active` / `passed` classes steps.tpl
   already emits. Equal widths are what makes that exact, which is why the steps are
   `flex: 1 1 0`; the mock-up's fixed 110px + space-between leaves the segment width
   unknowable in CSS.
   -------------------------------------------------------------------------- */
.component__steps {
    justify-content: flex-start;
    margin-bottom: 3rem;
}

/* The parent's single full-width grey line. */
.component__steps::before {
    content: none;
}

.component__steps-step {
    flex: 1 1 0;
    min-width: 0;
    flex-direction: column;
    align-items: center;
    text-align: center;
    gap: 10px;
    padding: 0;
    position: relative;
    /* o3-theme paints each step with --page-bg to punch a hole in its own track line.
       With the track drawn per segment there is nothing to punch through, and an opaque
       block would cover the segment it sits on. */
    background-color: transparent;
}

/* Track segment back to the previous step. */
.component__steps-step::before {
    content: "";
    position: absolute;
    top: 20px; /* centre of the 40px circle */
    right: 50%;
    width: 100%;
    height: 2px;
    background-color: #e1e2e5;
}

.component__steps-step:first-of-type::before {
    content: none;
}

.component__steps-step.active::before,
.component__steps-step.passed::before {
    background-color: var(--bbb-navy);
}

.component__steps .listing-point {
    width: 40px;
    height: 40px;
    border-radius: 50%;
    background-color: #fff;
    border: 2px solid #e1e2e5;
    color: #94a3b8;
    font-size: 1rem;
    font-weight: 700;
    margin-right: 0;
    /* Above its own step's track segment. */
    position: relative;
    z-index: 1;
}

.component__steps-step.active .listing-point {
    border-color: var(--bbb-navy);
    color: var(--bbb-navy);
    box-shadow: 0 0 0 4px rgba(0, 40, 77, 0.1);
}

/* A completed step is a filled navy circle with a CHECK MARK, not its number — see any of
   html/Wk2_1.html … Wk4.html. The digit is real text in the markup, so it is collapsed with
   `font-size: 0` and the glyph drawn as a pseudo-element at its own size (from the theme's
   self-hosted Material font, as everywhere else). The step's own label still names it, so
   nothing is lost when the number goes. */
.component__steps-step.passed .listing-point {
    background-color: var(--bbb-navy);
    border-color: var(--bbb-navy);
    color: #fff;
    font-size: 0;
}

.component__steps-step.passed .listing-point::before {
    content: "check" / "";
    font-size: 20px;
}

/* Labels. `:not(.listing-point-link)` because the parent's markup wraps the circle in an
   <a> of its own on every step that is reachable. */
.component__steps-step > a:not(.listing-point-link),
.component__steps-step > span {
    font-size: 12px;
    line-height: 1.3;
    color: #6c757d;
    text-decoration: none;
}

.component__steps-step.active > a:not(.listing-point-link),
.component__steps-step.active > span {
    font-weight: 700;
    color: var(--bbb-navy);
}

.component__steps-step.passed > a:not(.listing-point-link),
.component__steps-step.passed > span {
    color: var(--bbb-navy);
}

.component__steps-step > a:not(.listing-point-link):hover {
    color: var(--bbb-orange);
}

/* --------------------------------------------------------------------------
   Basket page — the white card, the totals box and the action bars (html/Wk1.html).

   o3-theme lays the basket out as a sidebar: items on the left at 60%, and summary,
   voucher and the next-step button stacked in an ice-blue rail on the right. The mock-up
   is one white card holding the full-width item table with the voucher and the totals side
   by side beneath it, and the two action bars on the page background outside it.

   The rearrangement is done entirely by redeclaring `grid-template-areas`, because the
   parent already places all four children by area name — so nothing has to move in the
   template except the bottom action bar, which had to leave the card (see basket.tpl).
   -------------------------------------------------------------------------- */
.basket {
    background-color: #fff;
    border: 1px solid var(--bbb-border);
    border-radius: 0.5rem;
    box-shadow: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.075);
    padding: 30px;
    /* 5/12 for the totals is the mock-up's `col-md-5 offset-md-1`. */
    grid-template-columns: minmax(0, 1fr) minmax(0, 41.6667%);
    grid-template-rows: auto auto;
    grid-template-areas:
        "basketarticles basketarticles"
        "vouche         summary";
    column-gap: var(--bs-gutter-x);
    row-gap: 3rem;
}

/* The rail's ice-blue panels: on the mock-up both sit on the card's own white. */
.basket__summary,
.basket__voucher {
    background-color: transparent;
    padding: 0;
    margin: 0;
    align-self: start;
    /* The parent makes the summary a sticky rail; inside the card there is nothing to
       stick past. */
    position: static;
    gap: 0;
}

/* The mock-up's totals box carries no heading. It is a plain div in the parent, so no
   heading structure is lost with it. */
.basket__summary-title {
    display: none;
}

/* Bordered box, one hairline per row, the grand total on a grey band — mock-up
   `.cart-summary-box` / `.summary-row` / `.summary-row.total`. */
.basket__summary table {
    background-color: #f8f9fa;
    border: 1px solid #dee2e6;
    border-radius: 0.25rem;
    overflow: hidden;
    /* QM: "Border fehlt" — and the border above was computing to 1px solid #dee2e6 the whole
       time. A collapsed table paints its own border ON the grid line, i.e. half of it outside the
       padding box, so the `overflow: hidden` that clips the grey total row into the rounded
       corners was clipping the frame away with it. Separated borders paint inside the box, so the
       radius, the clipping and the frame can all coexist; `border-spacing: 0` keeps the rows
       flush, and each row's own bottom hairline is unaffected either way. */
    border-collapse: separate;
    border-spacing: 0;
}

/* Matching Bootstrap's own `.table-borderless > :not(caption) > * > *` shape, or it keeps
   winning on specificity and every row stays borderless. */
.basket__summary .table-borderless > :not(caption) > * > * {
    padding: 0.5rem 15px;
    font-size: 0.875rem;
    font-weight: 400;
    border-bottom: 1px solid #dee2e6;
    background-color: transparent;
    color: var(--bbb-text-variant);
}

.basket__summary .table-borderless > :not(caption) > * > th {
    text-align: left;
}

.basket__summary .table-borderless > :not(caption) > .basket__summary-total > * {
    padding: 1rem 15px !important;
    border-bottom: 0;
    background-color: #eee;
    font-size: 1.125rem;
    font-weight: 700;
    color: var(--bbb-navy);
}

/* --- The basket line's quantity control ------------------------------------
   QM asked twice. The first round kept o3-theme's − / + stepper and only gave it the
   mock-up's border, on the reasoning that the stepper submits the change by itself while the
   mock-up's control needs a second click. QM came back with "Design anpassen" on the same
   cell, so the mock-up (html/Wk1.html) wins: a plain 60px field, the unit beside it and a
   circular-arrow update button.

   The stepper rules that used to live here are gone with it — the review step hides the
   − / + buttons anyway (see `.order__articles` below), so nothing else was using them.
   -------------------------------------------------------------------------- */
.component__basketquantity {
    display: inline-flex;
    align-items: center;
    gap: 0.5rem;
}

.component__basketquantity-input {
    /* The mock-up's `.quantity-input`: 60 × 38, centred, hairline box. */
    width: 60px;
    height: 38px;
    padding: 0 0.25rem;
    text-align: center;
    background-color: #fff;
    border: 1px solid var(--bbb-border);
    border-radius: 0.375rem;
}

.component__basketquantity-unit {
    font-size: 0.75rem;
    color: #6c757d;
    white-space: nowrap;
}

.component__basketquantity-update {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    padding: 0.25rem;
    border: 0;
    background: none;
    color: var(--bbb-orange);
}

.component__basketquantity-update:hover,
.component__basketquantity-update:focus {
    color: var(--bbb-orange-dark);
}

/* Discounts read as money coming off — the mock-up's "Rabatt:" row is red. o3-theme gives
   that cell `.text-primary`, which in this theme is navy and looks like any other row. */
.basket__summary .discount-row td,
.basket__summary tr:has(> td.text-primary) td.text-primary {
    color: var(--bs-danger);
}

/* --- Voucher ---------------------------------------------------------------
   The mock-up shows the field itself; o3-theme hides it behind a dashed "Ich habe einen
   Gutscheincode" toggle. Both the toggle and the collapse are switched off in CSS rather
   than by copying basketcontents.tpl — that file is 250 lines of summary rows, of which
   this would be the only change.
   -------------------------------------------------------------------------- */
.basket__voucher-collapse {
    display: none;
}

/* QM: "a) trennen" — the mock-up's pair is `flex gap-2`, two controls with 8px of air and a
   radius each. The `.input-group` class went with it (see basketcontents.tpl): keeping it meant
   Bootstrap flattening the facing corners from a six-class selector, which is a fight this file
   would have to win with `!important` for no gain — nothing else in the group was wanted either.

   `.basket__voucher` stays in front of every selector here for one reason: `display: flex` has to
   out-specify `.collapse:not(.show) { display: none }`, which is (0,2,0). Two classes ties it, and
   bbb.css loads after main.min.css, so the tie goes this way — one class alone would lose and the
   whole row would vanish. */
.basket__voucher .basket__voucher-row {
    display: flex;
    visibility: visible;
    height: auto;
    margin-top: 0;
    max-width: 384px;
    gap: 0.5rem;
}

.basket__voucher .basket__voucher-row .form-control {
    /* `.input-group > .form-control` used to supply this. */
    flex: 1 1 auto;
    min-width: 0;
    border-radius: 0.25rem;
    border-color: #d1d5db;
}

/* The mock-up's redeem button is green — and green appears nowhere else in this shop, in
   neither the brand palette nor o3-theme. Taken from the mock-up as drawn; one line to
   change if it turns out to be Tailwind noise rather than intent (it renders Tailwind's
   `bg-green-600`, while the mock-up's own `--action-green: #7ab51d` is declared and never
   used). Hover is the theme's accent, as everywhere else. */
/* QM: "b) Icon fehlt, c) font weight verkehrt". The mock-up draws a small SOLID play triangle
   before the label and leaves the button at Bootstrap's own 400 — this had it at 600, which next
   to a 400 input reads as the loudest thing in the row.

   The triangle is drawn with borders rather than taken from the theme's icon font: that font is
   Material Icons **Outlined**, so its `play_arrow` is a hollow outline — QM: "the icon need to be
   filled out". A border triangle is four lines, needs no second font family and no inline svg,
   takes the button's colour from `currentColor`, and lands on the mock-up's 10x10 ink. */
.basket__voucher .basket__voucher-row .btn {
    display: inline-flex;
    align-items: center;
    gap: 0.375rem;
    background-color: #16a34a;
    border-color: #16a34a;
    color: #fff;
    font-size: 0.875rem;
    font-weight: 400;
    padding: 0.5rem 1.5rem;
    border-radius: 0.25rem;
    white-space: nowrap;
}

.basket__voucher .basket__voucher-row .btn::before {
    content: "";
    width: 0;
    height: 0;
    border-style: solid;
    border-width: 5px 0 5px 9px;
    border-color: transparent transparent transparent currentColor;
}

.basket__voucher .basket__voucher-row .btn:hover,
.basket__voucher .basket__voucher-row .btn:focus {
    background-color: var(--bbb-orange);
    border-color: var(--bbb-orange);
    color: #fff;
}

/* --- Action bars -----------------------------------------------------------
   "Weiter shoppen" left, the next-step CTA right, above and below the card. The next-step
   form keeps the parent's `.basket__proceed` class (it is the parent's markup), which
   carried the ice-blue rail panel and a full-width 74px button — both have to be undone
   now that it is a button in a row.
   -------------------------------------------------------------------------- */
.basket__actions {
    display: flex;
    justify-content: space-between;
    align-items: center;
    gap: 20px;
    margin-block: 1.5rem;
}

.basket__actions .basket__proceed {
    background-color: transparent;
    padding: 0;
    margin: 0;
    height: auto;
}

.basket__actions .btn,
.basket__actions .basket__proceed .btn {
    display: inline-flex;
    align-items: center;
    gap: 0.375rem;
    width: auto;
    height: auto;
    padding: 10px 25px;
    font-size: 1rem;
    font-weight: 600;
    border-radius: 4px;
}

.basket__actions .basket__proceed .btn:hover,
.basket__actions .basket__proceed .btn:focus {
    background-color: var(--bbb-orange);
    border-color: var(--bbb-orange);
}

.basket__continue {
    border-color: #a1a5ad;
    color: var(--bbb-text);
    background-color: #fff;
}

.basket__continue:hover,
.basket__continue:focus {
    background-color: var(--bbb-orange);
    border-color: var(--bbb-orange);
    color: #fff;
}

/* Chevrons and the remove ✕ come from the theme's self-hosted Material font — see the
   header icons above; the font-face and the shared glyph rule are declared there. */
.basket__continue::before {
    content: "chevron_left" / "";
}

.basket__actions .basket__proceed .btn::after {
    content: "chevron_right" / "";
}

main .component__basketarticle-remove::before {
    content: "close" / "";
    font-size: 14px;
}

/* The mock-up's `bi-arrow-repeat` — two arrows chasing each other round a circle. Material's
   `autorenew` is the same drawing; `refresh` is the single-arrow one. */
.component__basketquantity-update::before {
    content: "autorenew" / "";
    font-size: 20px;
}

/* --- Below lg the table slides sideways -----------------------------------
   QM: "In Vorlage mit Slide gelöst" — the customer signed off on the mock-up's horizontally
   scrolling table, so the basket line keeps its five columns and its header row instead of
   reflowing into a stacked card. Same treatment as the review table on checkout step 4; see the
   note there.

   This replaces a stack that dropped the header row along with the columns, so the unit price,
   quantity and line total ended up under the product name with nothing naming them.

   Below **lg**, not md: the five desktop tracks carry 530px of fixed width, so between 768 and
   991px the title column is squeezed to ~110px and the row reads as one word per line. md is
   also exactly where o3-theme turns `.basket` into a grid, i.e. the worst case is the first
   pixel of the old breakpoint.

   780px leaves ~186px for the product name once the four fixed tracks and the gaps are taken. */
@media (max-width: 991.98px) {
    /* QM (mobile): "hierher" — the voucher belongs above the totals, as it does in the mock-up
       and as it already does on the two-column desktop card. o3-theme only switches `.basket` to
       `display: grid` from md up, so below 768px the areas would be inert and the plain DOM
       order (articles, summary, voucher) would put the voucher *under* the total — hence
       `display: grid` here rather than a second one-column block just for phones.

       `vouche` without the r is the parent's own area name (typo included); the elements are
       placed with it, so this has to match. */
    .basket {
        display: grid;
        padding: 1rem;
        grid-template-columns: minmax(0, 1fr);
        grid-template-areas:
            "basketarticles"
            "vouche"
            "summary";
        row-gap: 2rem;
    }

    .basket__articles {
        overflow-x: auto;
        -webkit-overflow-scrolling: touch;
        /* Same defect, same fix as `.order .basket__articles` on step 4 — see the long note
           there. Measured on the basket page at 390px: the table scrolls inside this box as
           intended, but its layout overflow was also counted into the ancestors, so `body`
           measured 813px against a 390px viewport and the whole page could be dragged
           sideways into white. `contain: paint` states what is already true of a scroll
           container and stops the overflow propagating: body goes 813 -> 390 and the table
           keeps its own sideways scroll. */
        contain: paint;
    }

    .basket__articles .component__basketheader,
    .basket__articles .component__basketarticle {
        min-width: 780px;
    }

    .basket__actions {
        flex-direction: column-reverse;
        align-items: stretch;
    }

    .basket__actions .btn,
    .basket__actions .basket__proceed .btn,
    .basket__actions .basket__proceed {
        width: 100%;
        justify-content: center;
    }
}

/* --------------------------------------------------------------------------
   Checkout step 2a — account chooser (html/Wk2_1.html).

   Three equal cards of the same height, each with its button pinned bottom-right.
   `form/login.tpl` bakes `col-12 col-md-6 col-lg-5` into its own <form> and is shared
   with the account page, so its width is reset here instead of forking that file. A grid
   rather than the parent's `.row`, so the three tracks are equal whatever classes the
   children carry.
   -------------------------------------------------------------------------- */
.checkout__options {
    display: grid;
    grid-template-columns: repeat(3, minmax(0, 1fr));
    gap: 1.5rem;
    align-items: stretch;
    margin: 0;
}

.checkout__options > form {
    /* Beats Bootstrap's .col-* (same specificity, later in the cascade) — no !important. */
    width: 100%;
    max-width: none;
    margin: 0;
    /* …but `form/login.tpl` also carries `mb-4`, and Bootstrap's spacing utilities ARE
       !important, so that one has to be matched in kind. Without it the login card is
       24px shorter than the other two and its button sits off the shared baseline. */
    margin-bottom: 0 !important;
    padding: 2.5rem;
    background-color: #fff;
    border: 1px solid #e1e2e5;
    border-radius: 0.75rem;
    display: flex;
    flex-direction: column;
    transition: box-shadow 0.2s;
}

.checkout__options > form:hover {
    box-shadow: 0 10px 25px -5px rgba(0, 0, 0, 0.05);
}

/* `.is-checkout .title-big:not(.offcanvas-title)` gives these a 50px top margin for use as
   a page heading, which inside a card sat 50px below its own 40px of padding. Matched at
   the same (0,3,0) specificity — a plain `.checkout__options h2` never reached it. */
.is-checkout .checkout__options .title-big {
    font-size: 1.5rem;
    margin-top: 0;
    margin-bottom: 0.5rem;
    color: var(--bbb-navy);
}

/* Mock-up: `mt-auto pt-4 d-flex justify-content-end` — the buttons line up along the
   bottom of the row however tall the cards' copy is. */
.checkout__options > form > .btn,
.checkout__options > form > button[type="submit"] {
    margin-top: auto;
    align-self: flex-end;
    display: inline-flex;
    align-items: center;
    gap: 0.5rem;
    padding: 0.75rem 2rem;
    font-weight: 700;
    border-radius: 0.5rem;
}

/* QM ("Icons fehlen"): every button on this step carries a trailing glyph in the mock-up —
   `arrow_forward` on "Weiter" and "Konto eröffnen", `login` on "Anmelden" — at `fs-6` with the
   card button's own 0.5rem gap. From the self-hosted Material font like the benefit ticks
   above, so there is still no icon markup in the templates. QM 2026-09-04: no long arrows
   anywhere in the theme, chevrons only — so `chevron_right` here too. */
.checkout__options > form > .btn::after,
.checkout__options > form > button[type="submit"]::after {
    content: "chevron_right" / "";
    font-size: 1.25rem;
}

.checkout__options > form > .btn:hover,
.checkout__options > form > button[type="submit"]:hover {
    background-color: var(--bbb-orange);
    border-color: var(--bbb-orange);
}

/* One muted tone for every line of card copy — rgba(33, 37, 41, 0.75), Bootstrap's
   `text-muted`, which is what the mock-up marks all of them with. The shop had three greys
   in these three cards: the parent's own text-muted (rgba(18, 18, 18, .75)) on the guest
   sub-line, body colour on the two intro paragraphs, and --bbb-text-variant on the benefit
   list and the field labels. Headings, buttons, the "Passwort vergessen?" link and the red
   validation messages keep their own colours. */
.checkout__options {
    /* Bootstrap's `.text-muted` is `color: var(--bs-secondary-color) !important`, and o3-theme
       sets that variable to rgba(18, 18, 18, .75). Re-pointing the variable inside the cards
       beats the utility without an `!important` of our own. */
    --bs-secondary-color: var(--bbb-text-muted);
}

.checkout__options > form p,
.checkout__options .list-benefits li,
/* (0,4,0), to reach past `.cl-user main form .form-floating > label` further down, which
   dresses every floating label on this page — including these two. */
.cl-user .checkout__options .form-floating > label {
    color: var(--bbb-text-muted);
}

/* Benefit list in the "Konto eröffnen" card (html/Wk2_1.html). The check marks come from
   the theme's self-hosted Material font via `::before`, so there is no icon markup in the
   template and nothing is fetched from Google — same rule as the header icons.

   Note the mock-up loads Material *Symbols* and uses the FILLED check_circle; the font the
   designer actually shipped is Material *Icons Outlined*, so ours is the outlined drawing
   of the same glyph. */
.checkout__options .list-benefits {
    list-style: none;
    margin: 0 0 0.75rem;
    padding: 0;
}

.checkout__options .list-benefits li {
    display: flex;
    align-items: flex-start;
    gap: 0.75rem;
    margin-bottom: 0.75rem;
    font-size: 0.875rem;
}

/* The mock-up's tick is the FILLED check_circle. QM 2026-09-04 asked for unfilled icons
   throughout, so this is the outlined `check_circle` of the shipped Material Icons Outlined
   font — ring and tick in orange, no disc. */
.checkout__options .list-benefits li::before {
    content: "check_circle" / "";
    flex: 0 0 auto;
    font-size: 1.25rem;
    line-height: 1;
    color: var(--bbb-orange);
}

/* Login card fields: the mock-up's `.form-control-custom` (html/Wk2_1.html) — o3-theme leaves
   them white on a white card, so the two inputs have no body at all. Radius is the one value
   not taken from the mock-up: 0.75rem instead of its 0.5rem, rounder on request. */
.checkout__options .form-floating > .form-control {
    background-color: #f2f4f6;
    border: 1px solid #c3c6cf;
    border-radius: 0.75rem;
}

/* The login card's "Passwort vergessen?" sits directly above its button, so the button's
   `margin-top: auto` must not push it away from the fields. */
.checkout__options .d-flex.justify-content-between {
    margin-bottom: 1rem;
}

@media (max-width: 991.98px) {
    .checkout__options {
        grid-template-columns: minmax(0, 1fr);
    }
}

/* --------------------------------------------------------------------------
   Checkout step 2b — customer / address form (html/Wk2_2.html).

   The mock-up stacks four full-width white cards: Kontoinformationen, Rechnungsadresse,
   Lieferadresse, Ihre Mitteilung an uns. o3-theme instead splits them across two columns
   — section 1 at `col-md-5` on the left and sections 2-4 stacked at `offset-md-7` on the
   right — so the left half of the page below "Kundeninformation" is empty, which on a form
   this long is the single most visible difference from the design.

   Scoped to `.cl-user`, not `.is-checkout`: the payment and review steps have `.row`s of
   their own that must keep Bootstrap's grid.
   -------------------------------------------------------------------------- */
.cl-user main form > .row {
    display: grid;
    grid-template-columns: minmax(0, 1fr);
    gap: 2rem;
    margin: 0;
}

.cl-user main form > .row > [class*="col-"] {
    /* Beats `.col-md-5` and `.offset-md-*` on specificity (0,2,0) vs (0,1,0). `mb-5` is a
       Bootstrap spacing utility and carries !important, so it needs matching in kind. */
    width: 100%;
    max-width: none;
    margin-left: 0;
    margin-bottom: 0 !important;
    padding: 2rem;
    background-color: #fff;
    border: 1px solid var(--bbb-border);
    border-radius: 8px;
    box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05);

    /* Fields sit on a 12-column track so the short ones can share a line, as in the
       mock-up (Anrede | Vorname | Nachname, then Straße | PLZ/Ort, then Land | Telefon).
       o3-theme stacks every field full width, which made this the longest page of the
       checkout by a wide margin.

       Row gap stays 0 — the fields carry Bootstrap's own `mb-3`. */
    display: grid;
    grid-template-columns: repeat(12, minmax(0, 1fr));
    column-gap: 1rem;
    align-content: start;
}

/* Full width is the default; the exceptions are placed below by the field they contain.
   Keying on the input's id rather than on `nth-child` so that a field being switched off in
   the shop configuration (Firma, USt-ID, the honeypot) cannot silently shift the layout. */
.cl-user main form > .row > [class*="col-"] > * {
    grid-column: 1 / -1;
}

/* The delivery address repeats the billing layout. Gridded on `.user__shippingaddress-form`
   — a plain div INSIDE the collapse — rather than on the collapse itself: Bootstrap hides
   that with `.collapse:not(.show){display:none}`, and a `display: grid` at this selector's
   higher specificity would beat it and leave the panel permanently open. */
.cl-user main form .user__shippingaddress-form {
    display: grid;
    grid-template-columns: repeat(12, minmax(0, 1fr));
    column-gap: 1rem;
    align-content: start;
}

.cl-user main form .user__shippingaddress-form > * {
    grid-column: 1 / -1;
}

/* …except that the SELECTED saved address's edit form is the collapse itself
   (`<div class="user__shippingaddress-form collapse">` in user_shipping.tpl), so the grid rule
   above did beat Bootstrap and the form stood open under every chosen address, "Ändern" or not
   (QM 2026-09-08, BBB-90). Closed until "Ändern" opens it. */
.cl-user main form .user__shippingaddress-form.collapse:not(.show) {
    display: none;
}

/* QM 2026-09-09 (BBB-90, "Abstände angleichen"): the tile's label padding is o3-theme's
   `20px 15px 20px 50px` — the 50px clears the radio for the address TEXT. With the edit form open
   that text is collapsed, so the fields stood 50px from the left edge, 15px from the right and
   20px above the bottom. While the form is open, the label wears the same 15px on all three sides
   (the radio, "Ändern" and the trash can are absolutely positioned and stay where they are).
   Same for the "neue Adresse hinzufügen" tile once its radio opens the form. */
.user__shippingaddress-change .form-check-label:has(.user__shippingaddress-form.collapse.show),
.user__shippingaddress-change #address_new:checked + .form-check-label {
    padding-right: 15px;
    padding-bottom: 15px;
    padding-left: 15px;
}

/* QM 2026-09-09 (BBB-90): "neue Adresse hinzufügen" looked nothing like the edit form — one field
   per line. o3-theme opens it with `#address_new:checked + label .user__shippingaddress-form
   { display: block }`, and the id in that selector outranks the grid rule above. Same grid, and
   the last row's fields drop their mb-3 so the form ends 15px above the tile's edge, like "Speichern" does. */
.cl-user main form #address_new:checked + .form-check-label .user__shippingaddress-form {
    display: grid;
}

.cl-user main form #address_new:checked + .form-check-label .form-floating:has(#deladr_oxaddress__oxfon),
.cl-user main form #address_new:checked + .form-check-label .form-floating:has(#deladr_oxaddress__oxfax) {
    /* Both, not `:last-child`: they share the last grid row, and a grid item's margin still
       stretches the track, so the one remaining mb-3 kept the row 16px too tall.
       !important because Bootstrap's spacing utilities are. */
    margin-bottom: 0 !important;
}

/* With the label's left padding down to 15px the caption "neue Adresse hinzufügen" — a bare text
   node on the label's first line — would run under the radio (absolute, left: 35px). text-indent
   moves that first line only; the form below is a block and stays at the 15px edge. */
.user__shippingaddress-change #address_new:checked + .form-check-label {
    text-indent: 35px;
}

/* text-indent inherits — without this the field labels and the typed text would be indented too. */
.user__shippingaddress-change #address_new:checked + .form-check-label .user__shippingaddress-form {
    text-indent: 0;
}

/* The "Speichern" of that edit form: clear of the Telefax field, and the same 4px corners as the
   checkout's other buttons (.user__navi > .btn). */
.cl-user main form .user__shippingaddress-form .user__shippingaddress-save {
    /* QM: "as much as the fields have space between each other" — one field row to the next
       measures about 2rem in the QM shot (field bottom to next label top). */
    margin-top: 2rem;
}

.cl-user main form .user__shippingaddress-form .user__shippingaddress-save > .btn {
    /* o3-theme positions EVERY button inside an address tile absolutely, for "Ändern" and the
       trash can (`.form-check-label button { position: absolute; right: 15px }`). That took
       this one out of the grid and hung it 15px past the fields' right edge (QM shot). Back in
       flow, the `text-end` wrapper lines it up with the Telefax field. */
    position: static;
    padding: 10px 25px;
    font-weight: 600;
    border-radius: 4px;
}

/* QM (qm10.png): "hineinander" — Passwort and Passwort bestätigen share one line, as in
   html/Wk2_2.html, where they are a `col-md-6` pair. Six tracks each of the card's twelve,
   the same mechanism the address fields below use; the markup stays o3-theme's, because a
   Bootstrap column class means nothing to a grid item.

   The requirement list is inside the password wrapper, so it fills the left cell under its
   own field instead of pushing the two inputs apart — which is what QM's arrow was about.
   The mobile query further down collapses the pair back to one column. */
.cl-user main form > .row > [class*="col-"] > div:has(#userPassword),
.cl-user main form > .row > [class*="col-"] > div:has(#userPasswordConfirm) {
    grid-column: span 6;
}

.cl-user main form .form-floating:has(#invadr_oxuser__oxsal),
.cl-user main form .form-floating:has(#invadr_oxuser__oxfname),
.cl-user main form .form-floating:has(#invadr_oxuser__oxlname),
.cl-user main form .form-floating:has(#invadr_oxuser__oxcompany),
.cl-user main form .form-floating:has(#deladr_oxaddress__oxsal),
.cl-user main form .form-floating:has(#deladr_oxaddress__oxfname),
.cl-user main form .form-floating:has(#deladr_oxaddress__oxlname),
.cl-user main form .form-floating:has(#deladr_oxaddress__oxcompany) {
    grid-column: span 4;
}

.cl-user main form .form-floating:has(#deladr_oxaddress__oxaddinfo) {
    grid-column: span 8;
}

.cl-user main form .user__shippingaddress-form > .row:has(#deladr_oxaddress__oxstreet) {
    grid-column: span 8;
}

.cl-user main form .user__shippingaddress-form > .row:has(#deladr_oxaddress__oxzip) {
    grid-column: span 4;
}

.cl-user main form .user__shippingaddress-form > .row:has(#deladr_oxaddress__oxzip) > :first-child {
    flex: 0 0 38%;
    width: 38%;
    max-width: none;
}

.cl-user main form .user__shippingaddress-form > .row:has(#deladr_oxaddress__oxzip) > :last-child {
    flex: 0 0 62%;
    width: 62%;
    max-width: none;
}

/* Land takes the full row now that Bundesland is gone from the delivery forms as well
   (bbb user_shipping.tpl gates it on bInputState like the billing form) — same as billing's Land. */
.cl-user main form .form-floating:has(#deladr_oxaddress__oxfon),
.cl-user main form .form-floating:has(#deladr_oxaddress__oxfax),
/* Matched on the name, not the id — the id contains brackets and would need escaping.
   Paired with the country it belongs to, instead of sitting alone on a full-width row. */
.cl-user main form .form-floating:has(select[name="deladr[oxaddress__oxstateid]"]) {
    grid-column: span 6;
}

.cl-user main form > .row > div > .row:has(#invadr_oxuser__oxstreet) {
    grid-column: span 8;
}

.cl-user main form > .row > div > .row:has(#invadr_oxuser__oxzip) {
    grid-column: span 4;
}

.cl-user main form .form-group:has(#invCountrySelect),
.cl-user main form .form-floating:has(#invadr_oxuser__oxfon) {
    grid-column: span 6;
}

/* PLZ/Ort share a 4/12 track with each other. o3-theme splits them `col-lg-3` / `col-lg-9`,
   a ratio meant for a full-width row — inside a quarter-width track it left the postcode
   input 62px wide next to a 261px city field. */
.cl-user main form > .row > div > .row:has(#invadr_oxuser__oxzip) > :first-child {
    flex: 0 0 38%;
    width: 38%;
    max-width: none;
}

.cl-user main form > .row > div > .row:has(#invadr_oxuser__oxzip) > :last-child {
    flex: 0 0 62%;
    width: 62%;
    max-width: none;
}

/* "Land" is the one label-above field (TroUser's legacy `form-group`) sitting beside a
   floating-label one, so its control started 22px lower than Telefon's and was 4px shorter.
   Matching the height and aligning to the bottom of the row puts both controls on the same
   line — the label simply sits above, as the mock-up has it. */
.cl-user main form .form-group:has(#invCountrySelect),
.cl-user main form .form-floating:has(#invadr_oxuser__oxfon) {
    /* Both sit at the bottom of the row rather than stretching, so the two CONTROLS line up
       and Land's label simply rises above them. Stretching (the grid default) left Telefon's
       input pinned to the top of a box sized by Land's label. */
    align-self: end;
    margin-bottom: 1rem;
}

.cl-user main form .form-group:has(#invCountrySelect) #invCountrySelect {
    height: 50px;
}

/* Below md the grid collapses to one column — three-up name fields are unusable on a
   phone. */
@media (max-width: 767.98px) {
    .cl-user main form > .row > [class*="col-"] > *,
    .cl-user main form .user__shippingaddress-form > * {
        grid-column: 1 / -1 !important;
    }
}

/* Mock-up heads each card with a large navy Material icon beside the title
   (account_circle / description / local_shipping; the message card has none). Selected by a
   field the section owns, for the same reason as above. */
.cl-user main form > .row > div > .title-small::before {
    font-size: 40px;
    color: var(--bbb-navy);
    line-height: 1;
}

/* --- Fields ---------------------------------------------------------------
   Measured against the mock-up rather than eyeballed, because the difference was in the
   controls themselves, not the layout:

   | token            | mock-up            | o3-theme            |
   |------------------|--------------------|---------------------|
   | field background | #fff               | #f7f9fb (page grey) |
   | field border     | 1px #c3c6cf        | 1px #dbdbdb         |
   | field radius     | 4px                | 0                   |
   | label            | 12px/700 #43474e   | 16px/400 #929292    |
   |                  | ABOVE the field    | floating inside it  |

   Scoped to `.cl-user`: these are the shop's only long forms, and the rest of the site
   keeps o3-theme's field style until the designer covers those pages too.
   -------------------------------------------------------------------------- */
.cl-user main form .form-control,
.cl-user main form .form-select {
    background-color: #fff;
    border: 1px solid #c3c6cf;
    border-radius: 4px;
}

/* `.form-floating` puts the label AFTER the input in the DOM and positions it absolutely on
   top. `column-reverse` lifts it above the field without touching a single template. */
.cl-user main form .form-floating {
    display: flex;
    flex-direction: column-reverse;
}

/* Bootstrap moves the label with `.form-floating > .form-control:focus ~ label` and
   `:not(:placeholder-shown) ~ label` — (0,3,1) selectors that a plain
   `.form-floating > label` cannot outrank, so the same shapes have to be matched here or
   the label jumps as soon as a field is filled or focused. */
.cl-user main form .form-floating > label,
.cl-user main form .form-floating > .form-control:focus ~ label,
.cl-user main form .form-floating > .form-control:not(:placeholder-shown) ~ label,
.cl-user main form .form-floating > .form-select ~ label {
    position: static;
    height: auto;
    width: auto;
    padding: 0;
    /* QM: the "Strasse" / "Hausnr." / "PLZ" labels stood 16px right of their own fields, while
       every other label on the page sat flush. o3-theme's `.row > .form-floating label
       { margin-left: 1rem }` is the cause: it compensates the column padding while the label
       still OVERLAYS the input, and those three are the only labels here that live inside a
       Bootstrap row. Once the label is lifted out of overlay mode two declarations up, that
       margin has nothing left to compensate. Zeroed with the rest of the overlay geometry
       rather than in a rule of its own — it belongs to the same reset. */
    margin-left: 0;
    margin-bottom: 4px;
    border: 0;
    font-size: 12px;
    font-weight: 700;
    line-height: 1.5;
    color: var(--bbb-text-variant);
    opacity: 1;
    transform: none;
    pointer-events: auto;
}

/* Same trick on the padding: Bootstrap re-pads a filled floating input to clear the label
   that is no longer overlaying it. */
.cl-user main form .form-floating > .form-control,
.cl-user main form .form-floating > .form-select,
.cl-user main form .form-floating > .form-control:focus,
.cl-user main form .form-floating > .form-control:not(:placeholder-shown) {
    height: 50px;
    padding: 12px 16px;
    line-height: 1.5;
}

/* The placeholder carries the label's own text, so with the label above it every field
   would read its name twice. */
.cl-user main form .form-floating > .form-control::placeholder {
    color: transparent;
}

/* Bootstrap backs the floated label with `label::after` — an absolutely positioned block in
   the page colour that hides the input's top border where the label crosses it. Once the
   label is `position: static`, that pseudo-element resolves against `.form-floating`
   instead and paints a 20px grey band across the top of every field
   (`inset: 14px 8px 38px; background: #f7f9fb`).

   `content: none` so it is never generated, rather than `display: none` which would leave a
   switched-off leftover — same treatment as the header's stray hairlines. */
.cl-user main form .form-floating > label::after {
    content: none;
}

/* Password fields sit in an `.input-group` — `.form-floating` (grown) plus the show/hide eye
   button. The group stretches its children, and `.form-floating` is now label + input tall,
   so the button stretched over BOTH rows: a 72px box starting at the label's top, whose top
   border drew a stray hairline across the field at label height and left the control 22px
   taller than the input beside it.

   Aligning the group to its last row puts the button back on the input's line; the explicit
   height makes the two exactly equal. Its border also has to match the field's — o3-theme
   gives the button #dbdbdb where the input is #c3c6cf. */
.cl-user main form .input-group {
    align-items: flex-end;
}

.cl-user main form .input-group > .btn {
    height: 50px;
    border-color: #c3c6cf;
}

/* The password-rules panel is a plain `div.bg-light`, and o3-theme repaints Bootstrap's
   neutral `.bg-light` utility with the theme's secondary — which in this theme is the ice
   blue used for PRODUCT surfaces. Inside a registration form it read as a stray product
   tile, the third instance of this same mismatch after the payment method labels and the
   "Pflichtfelder" note. Given the theme's own info-box treatment instead.

   Selected by the requirement rows it contains rather than by `.bg-light`, so no other
   utility use in the form is caught. */
.cl-user main form div:has(> .component__password-requirement) {
    /* Bootstrap's background utilities carry !important, so `.bg-light` has to be matched in
       kind — same trap as `.alert-info` on this page. */
    background-color: rgba(211, 227, 253, 0.4) !important;
    border: 1px solid #c3c6cf;
    border-radius: 4px;
    font-size: 0.875rem;
    color: var(--bbb-navy);
}

.cl-user main form > .row > div:has(#userLoginName) > .title-small::before {
    content: "account_circle" / "";
}

.cl-user main form > .row > div:has(#invadr_oxuser__oxfname) > .title-small::before {
    content: "description" / "";
}

.cl-user main form > .row > div:has(#user__shippingaddress-chance) > .title-small::before {
    content: "local_shipping" / "";
}

/* The section headings are `h3.title-small`, not `.title-big` — the page heading class. */
.cl-user main form > .row .title-small {
    display: flex;
    align-items: center;
    gap: 1rem;
    margin-top: 0;
    margin-bottom: 1.5rem;
    font-size: 1.75rem;
    color: var(--bbb-navy);
}

/* "Land" is the one field still in Bootstrap 3's horizontal `form-group` +
   `control-label col-lg-3` / `col-lg-9` shape, so it rendered as a 75%-wide control with
   its label beside it while every other field on the page is a full-width `form-floating`.
   Stacked to match, with the mock-up's own label style. */
.cl-user main form .form-group > .control-label {
    display: block;
    width: 100%;
    margin-bottom: 0.25rem;
    font-size: 12px;
    font-weight: 700;
    color: var(--bbb-text-variant);
}

.cl-user main form .form-group > [class*="col-"] {
    width: 100%;
    max-width: none;
    margin-left: 0;
}

/* Mock-up: `Die mit * gekennzeichneten Felder sind Pflichtfelder.` in a light-blue info
   box with a hairline. o3-theme paints it with the theme's secondary, which here is the
   ice blue used for product surfaces — it read as a product tile inside a form. */
.cl-user main form .alert-info {
    /* o3-theme sets `.alert-info, .alert-warning { background-color: var(--accent-color)
       !important }` — the ice blue used for product surfaces, which inside a form read as
       a stray product tile. Its !important has to be matched in kind. */
    background-color: rgba(211, 227, 253, 0.4) !important;
    border-color: #c3c6cf !important;
    color: var(--bbb-navy) !important;
    border-radius: 0.25rem;
    font-size: 0.875rem;
    padding: 1rem;
    /* QM: "info-Icon: Die mit * gekennzeichneten Felder sind Pflichtfelder." The wording is a
       lang override (COMPLETE_MARKED_FIELDS); the icon is the mock-up's Material Symbols
       "info" glyph, inlined here rather than pulled from fonts.googleapis.com — the shop must
       not leak visitor IPs to Google, which is the same rule the theme's fonts follow. */
    display: flex;
    align-items: center;
    gap: 0.5rem;
}

.cl-user main form .alert-info::before {
    content: "";
    flex: 0 0 auto;
    /* Mock-up's `.material-symbols-outlined` is 24px; QM 2026-09-04 asked for a touch more. */
    width: 26px;
    height: 26px;
    background: currentColor;
    -webkit-mask: var(--bbb-icon-info) center / contain no-repeat;
    mask: var(--bbb-icon-info) center / contain no-repeat;
}

/* Action bar, same treatment as the basket's (html/Wk2_2.html: "Zurück" outline left,
   the CTA navy right). */
.user__navi {
    justify-content: space-between;
    align-items: center;
    gap: 20px;
    margin-top: 2rem;
}

.user__navi > .btn {
    padding: 10px 25px;
    font-size: 1rem;
    font-weight: 600;
    border-radius: 4px;
}

.user__navi > a.btn {
    border: 1px solid #a1a5ad;
    background-color: #fff;
    color: var(--bbb-text);
}

/* QM ("Verkehrtes icon" / "Fehlt") against html/Wk2_2.html, whose action bar is
   `chevron_left  Zurück` and `Weiter zum nächsten Schritt  chevron_right`. o3-theme ships a
   long-arrow SVG on the back link and nothing at all on the CTA. Both glyphs come from the
   self-hosted Material font like every other icon in this theme, so the parent's inline SVG
   is hidden rather than the template forked. */
.user__navi > .btn,
.payment__navi > .btn {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    gap: 0.5rem;
}

/* Every inline arrow in these bars goes, not just the back link's: the logged-in variant of
   the address step (o3-theme form/user_checkout_change.tpl) also ships a long arrow inside the
   "Weiter" button, next to the chevron this theme adds — "Weiter → ›" (QM 2026-09-04). The
   payment step's pair of long arrows is replaced by the same chevrons for consistency. */
.user__navi > .btn > svg,
.payment__navi > .btn > svg {
    display: none;
}

.user__navi > a.btn::before,
.payment__navi > a.btn::before {
    content: "chevron_left" / "";
    font-size: 20px;
}

.user__navi > button[type="submit"]::after,
.payment__navi > button[type="submit"]::after {
    content: "chevron_right" / "";
    font-size: 20px;
}

.user__navi > a.btn:hover,
.user__navi > a.btn:focus,
.user__navi > button:hover,
.user__navi > button:focus {
    background-color: var(--bbb-orange);
    border-color: var(--bbb-orange);
    color: #fff;
}

/* --------------------------------------------------------------------------
   Checkout steps 3 and 4 — payment and review (html/Wk3.html, html/Wk4.html).

   o3-theme lays both pages out as a 60/34 grid with the stepper INSIDE the left column
   (`grid-area: steps` in a `"steps . summary"` row), so the progress bar is squeezed to
   60% of the page while the mock-ups run it full width above everything. Redeclaring the
   area strings so `steps` spans every column is the whole fix — nothing moves in a
   template.
   -------------------------------------------------------------------------- */
/* Checkout step 2 (address) — QM 2026-09-08 (BBB-90): "Das kann weg und dann alles auf volle
   Breite" and "Mach mal den Block unter Rechnungsadresse". o3-theme runs a 28/28/34 grid with the
   two address cards side by side and a totals rail on the right, which left the delivery form
   with ~300px for its fields. One full-width column now: stepper, billing address, delivery
   address, newsletter, message, buttons — the totals come back on the review step. The parent
   only turns the rail on from 768px, so the `display: none` needs no media query of its own. */
.user {
    grid-template-columns: minmax(0, 1fr);
    grid-template-areas:
        "steps"
        "billingaddress"
        "shippingaddress"
        "newsletter"
        "message"
        "navi";
}

.user .basket__summary {
    display: none;
}

/* The mock-up runs this step as ONE full-width column of cards — no totals rail; the
   totals reappear on the review step (html/Wk4.html). o3-theme puts a 34% summary rail
   here, which is why the content column was cramped and the stepper squeezed.

   Worth knowing before reverting this: if a payment method ever carries a surcharge
   (`getPaymentCost()`), it is no longer visible while choosing — it shows on the review
   step instead. Neither method in this shop has one today. Two lines to put back. */
.payment {
    grid-template-columns: minmax(0, 1fr);
    grid-template-areas:
        "steps"
        "delivery"
        "payments"
        "navi";
}

.payment .basket__summary {
    display: none;
}

/* The mock-up runs this step as ONE full-width column: the two address cards side by side,
   then shipping/payment side by side, then AGB, then the basket table full width, then the
   totals card BELOW everything and right-aligned at 5/12, then the CTA. o3-theme instead
   keeps a 34% summary rail down the right, which squeezed the basket table into a ~430px
   column and left the whole page reading as two unequal halves.

   Twelve tracks rather than two, so the summary can sit at the mock-up's 5/12 rather than a
   rounded-off half. */
.order {
    grid-template-columns: repeat(12, minmax(0, 1fr));
    column-gap: 1.5rem;
    grid-template-areas:
        "steps          steps          steps          steps          steps          steps          steps          steps          steps          steps          steps          steps"
        "notice         notice         notice         notice         notice         notice         notice          notice          notice          notice          notice          notice"
        "billingaddress billingaddress billingaddress billingaddress billingaddress billingaddress shippingaddress shippingaddress shippingaddress shippingaddress shippingaddress shippingaddress"
        "delivery       delivery       delivery       delivery       delivery       delivery       payment         payment         payment         payment         payment         payment"
        "agb            agb            agb            agb            agb            agb            agb             agb             agb             agb             agb             agb"
        "articles       articles       articles       articles       articles       articles       articles        articles        articles        articles        articles        articles"
        "message        message        message        message        message        message        message         message         message         message         message         message"
        ".              .              .              .              .              .              .               summary         summary         summary         summary         summary"
        "navi           navi           navi           navi           navi           navi           navi            navi            navi            navi            navi            navi";
}

/* The summary is the mock-up's own white card here, not the bordered/striped box the basket
   page uses (Wk1 and Wk4 draw it differently), and it must not stick to the viewport — the
   parent makes it a sticky rail. */
.order .basket__summary {
    position: static;
    padding: 1.5rem;
    background-color: #fff;
    border: 1px solid var(--bbb-border);
    border-radius: 0.5rem;
    box-shadow: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.075);
}

.order .basket__summary table {
    background-color: transparent;
    border: 0;
    border-radius: 0;
}

.order .basket__summary .table-borderless > :not(caption) > * > * {
    padding: 0.5rem 0;
    border-bottom: 0;
}

/* html/Wk4.html: the gross sum row carries `class="border-top"` + `pt-3` — a hairline between the
   VAT line and "Summe Artikel (brutto)", so the box reads net → VAT | gross → total. QM 2026-09-08
   (BBB-90): "Linie fehlt noch". Both ids: the parent switches to `basketTotalGross` once a
   discount is in the basket. */
.order .basket__summary .table-borderless > :not(caption) > tr:has(> #basketTotalProductsGross) > *,
.order .basket__summary .table-borderless > :not(caption) > tr:has(> #basketTotalGross) > * {
    padding-top: 1rem;
    border-top: 1px solid var(--bbb-border);
}

/* Mock-up marks the grand total with a 2px navy rule above it and sets it larger, instead of
   the basket's grey band. */
.order .basket__summary .table-borderless > :not(caption) > .basket__summary-total > * {
    padding: 1rem 0 0 !important;
    background-color: transparent;
    border-top: 2px solid var(--bbb-navy);
    font-size: 1.25rem;
}

/* The review notice added in order.tpl — the mock-up's small light-blue banner. */
.order__notice {
    /* Named, not auto-placed: without an area of its own it dropped into the first free cell
       after every named one, i.e. below the basket instead of opening the page. */
    grid-area: notice;
    /* Same 1.5rem every card on this page carries, so the notice sits in the page's rhythm.
       Bootstrap's `.alert` brings 1rem, which left it 8px tight; zeroing it (as this did at
       first) left it flush against the address cards. */
    margin-bottom: 1.5rem;
    padding: 0.75rem 1rem;
    background-color: rgba(211, 227, 253, 0.4) !important;
    border: 1px solid #c3c6cf !important;
    border-radius: 0.25rem;
    color: var(--bbb-navy) !important;
    /* QM: "Fontsize + Icon". Both from html/Wk4.html's `.alert-info-custom`: 12px, and an `info`
       glyph in front at the icon font's own 24px, `gap: 0.75rem` between them. (The mock-up also
       asks for `font-weight: 300`; Hanken Grotesk is self-hosted here in 400/600/700/800 only, so
       a 300 would resolve to the 400 that is already set — left out rather than written as a
       declaration that does nothing.) */
    display: flex;
    align-items: center;
    gap: 0.75rem;
    font-size: 12px;
}

.order__notice::before {
    content: "info" / "";
}

/* --- Shipping / payment options ------------------------------------------
   Mock-up `.payment-option-card`: separate white cards, 1px #dee2e6, 8px radius, and the
   chosen one marked by a 2px border. o3-theme renders them as one flush stack (borders
   collapsed between neighbours) and marks the choice by filling the label with
   `--bs-secondary` — which in this theme is the ice blue used for product surfaces, so a
   selected payment method looked like a product tile.
   -------------------------------------------------------------------------- */
/* Each section is one of the mock-up's cards: a `bg-light` header strip with the heading,
   then the options inset in the body. There is no wrapper element around the options in
   o3-theme's template, so the "body" padding is carried by the options' own inline margin
   — `overflow: hidden` makes the card a block formatting context, which stops those
   margins collapsing out through the card edges. */
.payment__delivery,
.payment__payments {
    background-color: #fff;
    border: 1px solid var(--bbb-border);
    border-radius: 0.5rem;
    overflow: hidden;
    padding: 0;
    margin-bottom: 1.5rem;
}

.payment__delivery > .title-small,
.payment__payments > .title-small {
    display: flex;
    align-items: baseline;
    justify-content: space-between;
    gap: 0.75rem;
    margin: 0;
    padding: 1rem 1.5rem;
    background-color: #f8f9fa;
    border-bottom: 1px solid var(--bbb-border);
    font-size: 1.125rem;
    color: var(--bbb-navy);
}

/* QM: the carrier is a dropdown now (see payment.tpl) and the cost sits under it as a small
   hint, the way the mock-up has it — it used to ride along in the heading. */
.payment__delivery-picker {
    padding: 1rem 1.5rem;
}

.payment__delivery-picker .form-select {
    max-width: 420px;
    /* The mock-up's `.form-select` (html/Wk3.html): white, `--border-radius: 0.5rem`, 1px
       #dee2e6. o3-theme fills its selects with the same near-white as the header field and
       squares them off (`--bs-border-radius: 0` shop-wide), so on the white card the control
       had no edge of its own. */
    background-color: #fff;
    border-radius: 0.5rem;
}

.payment__delivery-costs {
    margin: 0.5rem 0 0;
    font-size: 0.875rem;
    color: #6c757d;
}

.payment__delivery .form-check,
.payment__payments .form-check,
.payment__payments .form-check:nth-last-of-type(2) {
    border-width: 1px;
    border-color: var(--bbb-border);
    border-radius: 0.5rem;
    background-color: #fff;
    margin: 1rem 1.5rem;
    transition: background-color 0.2s;
}

.payment__delivery .form-check:hover,
.payment__payments .form-check:hover {
    background-color: var(--bbb-surface-low);
}

/* QM: "Kunde will die Schwarze Umrandung nicht" — the selected option used to be ringed in a
   2px navy border. The state is still unambiguous without it: the radio itself is filled, the
   label goes bold (below) and the method's description only unfolds on the selected one. */
.payment__delivery .form-check:has(.form-check-input:checked),
.payment__payments .form-check:has(.form-check-input:checked) {
    border-width: 1px;
    border-color: var(--bbb-border);
}

.payment__delivery .form-check-input:checked + .form-check-label,
.payment__payments .form-check-input:checked + .form-check-label {
    background-color: transparent;
}

/* Radio and label are a real flex row — the mock-up's `d-flex align-items-start`.

   o3-theme positions the radio `absolute; left: 35px; top: 16px` and reserves room for it
   with a 50px `padding-left` on the label. Those are two independent hand-set numbers, so
   they cannot agree: the radio's centre sat 5px BELOW the text's on every option, and the
   gap between them came out at 6px. Laying them out as a row means the browser aligns
   them and the gap is a value, not arithmetic.

   Bootstrap's `margin-left: -1.5em` (for its own float layout) goes with the absolute
   positioning — it was also what left the radio hanging outside the card's border. */
.payment__delivery .form-check,
.payment__payments .form-check {
    display: flex;
    align-items: flex-start;
    gap: 0.75rem;
    padding: 1.25rem;
}

.payment__delivery .form-check-input,
.payment__payments .form-check-input {
    position: static;
    flex: 0 0 auto;
    /* Centres the 22px control on the 24px first line of the label. */
    margin: 1px 0 0;
}

.payment__delivery .form-check-label,
.payment__payments .form-check-label {
    flex: 1 1 auto;
    padding: 0;
}

.payment__payments .form-check-input:checked ~ .form-check-label > span:first-child {
    font-weight: 700;
    color: var(--bbb-navy);
}

/* Mock-up shows a method's explanation only once it is chosen, as a light-blue info box —
   the same one the address form uses. Unchosen methods stay a single line, which is what
   makes the list scannable; o3-theme prints every description at once, so PayPal's five
   lines dominated the page. */
.payment__payments .form-check-input:not(:checked) ~ .form-check-label .desc {
    display: none;
}

.payment__payments .form-check-input:checked ~ .form-check-label .desc {
    display: flex;
    align-items: center;
    gap: 0.5rem;
    margin-top: 0.75rem;
    padding: 0.75rem 1rem;
    background-color: rgba(211, 227, 253, 0.4);
    border: 1px solid #c3c6cf;
    border-radius: 0.25rem;
    color: var(--bbb-navy);
    /* html/Wk3.html `.info-box { font-size: 12px }` — same size as the review step's notice
       (`.order__notice`), which comes from the same mock-up box. QM 2026-09-08 (BBB-90) marked
       the 14px this had as "Font size" against the mock-up. */
    font-size: 12px;
}

/* QM: "Mitiger + Icon + Font size" on html/Wk3.html's `.info-box`, which is an `info` glyph
   and the sentence on one centred line.

   The icon is the mock-up's, from the self-hosted Material font. The centring needed the
   paragraph too: `oxpayments__oxlongdesc` is admin HTML — "<p>Der Versand der Waren erfolgt
   nach Zahlungseingang.</p>" — and the browser's 16px bottom margin on that `<p>` pushed the
   text off the box's middle whatever the box did. */
.payment__payments .form-check-input:checked ~ .form-check-label .desc > * {
    margin: 0;
}

.payment__payments .form-check-input:checked ~ .form-check-label .desc::before {
    content: "info" / "";
    flex: 0 0 auto;
    font-size: 20px;
}

/* --- Checkout action bars -------------------------------------------------
   Same pair as the basket's: an outline "Zurück" left, the navy CTA right. o3-theme
   renders "Zurück" as a bare text link, which on a page whose only other control is a
   solid button does not read as a control at all.
   -------------------------------------------------------------------------- */
.payment__navi,
.order__navi {
    align-items: center;
    gap: 20px;
    margin-top: 2rem;
}

/* .order__navi is the exception — it carries only the final CTA and aligns it right, see below. */
.payment__navi {
    justify-content: space-between;
}

.user__navi > .btn,
.payment__navi > .btn,
.order__navi > .btn {
    padding: 10px 25px;
    font-size: 1rem;
    font-weight: 600;
    border-radius: 4px;
}

.payment__navi > a.btn,
.order__navi > a.btn {
    border: 1px solid #a1a5ad;
    background-color: #fff;
    color: var(--bbb-text);
}

.payment__navi > a.btn:hover,
.payment__navi > a.btn:focus,
.payment__navi > button:hover,
.payment__navi > button:focus,
.order__navi > a.btn:hover,
.order__navi > a.btn:focus,
.order__navi > button:hover,
.order__navi > button:focus {
    background-color: var(--bbb-orange);
    border-color: var(--bbb-orange);
    color: #fff;
}

/* --- Step 4, review: the summary blocks as cards -------------------------
   Mock-up (html/Wk4.html): every block — both addresses, Versandart, Zahlungsart, the
   basket and the AGB note — is a `card shadow-sm p-4` with its "Ändern" control as an
   orange pencil in the top right. o3-theme renders them as bare text separated by 4rem of
   whitespace, which on a page whose whole job is "check this before you pay" gives the
   reader nothing to check against.
   -------------------------------------------------------------------------- */
.order__billingaddress,
.order__shippingaddress,
.order__delivery,
.order__payment,
.order__message,
.order__articles,
.order__agb {
    background-color: #fff;
    border: 1px solid var(--bbb-border);
    border-radius: 0.5rem;
    box-shadow: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.075);
    padding: 1.5rem;
    /* The parent spaces these with 4rem of margin instead; as cards they only need a gap. */
    margin-bottom: 1.5rem;
}

.order .title-small {
    margin-top: 0;
    margin-bottom: 1rem;
    font-size: 1.125rem;
    color: var(--bbb-navy);
}

/* "Ändern" — the mock-up's orange `.edit-btn`, top right of its card. */
.order .title-small {
    display: flex;
    /* `baseline` was right while the control was a word; an icon-only button has no baseline of
       its own and hangs off the bottom of the row. */
    align-items: center;
    justify-content: space-between;
    gap: 0.5rem;
}

/* QM: "icon statt text" — the mock-up's control is the orange pencil alone, no "Ändern" beside
   it. `font-size: 0` hides the label without touching six buttons in the template, and the glyph
   sets its own size back (the same trick the product cards use); the button keeps its text node
   and its `title`, so its accessible name is still "Ändern" rather than an unlabelled icon. */
.order .title-small button,
.order .title-small .btn {
    padding: 0;
    border: 0;
    background: none;
    color: var(--bbb-orange);
    font-size: 0;
    font-weight: 400;
    line-height: 1;
    /* o3-theme's `.btn-edit` is a dashed-underlined text link. Under an icon the dashes read as a
       stray artefact, and the mock-up's `.edit-btn` has none. */
    text-decoration: none;
}

.order .title-small button::before,
.order .title-small .btn::before {
    content: "edit" / "";
}

/* QM: "font size" on the address block. The mock-up sets those lines `.small`; ours inherited the
   16px body size, which made the two address cards the loudest text on a page of check-this-over
   detail. */
.order address {
    font-size: 0.875rem;
}

.order .title-small button:hover,
.order .title-small .btn:hover {
    color: var(--bbb-orange-dark);
    text-decoration: underline;
}

/* --- The review table (html/Wk4.html) --------------------------------------
   Mock-up columns: ARTIKEL (picture + title) | MENGE | EINZELPREIS | MWST | GESAMTBETRAG.
   Two things differ from the basket's table, so the tracks and placements are declared
   separately instead of reusing them: Menge comes BEFORE Einzelpreis here, and there is an
   extra per-line MwSt column (rendered only when !$editable — see basketcontents_list.tpl).
   -------------------------------------------------------------------------- */
.order__articles,
.order .basket__articles {
    --bbb-basket-cols: 80px minmax(0, 1fr) 90px 120px 70px 130px;
}

/* Mock-up puts the Warenkorb card's title on a `bg-light` strip with a hairline under it,
   the same treatment as the shipping/payment cards on step 3. The address cards keep a plain
   heading, which is what the mock-up does there too. */
.order__articles > .title-small {
    margin: -1.5rem -1.5rem 1.5rem;
    padding: 1rem 1.5rem;
    background-color: #f8f9fa;
    border-bottom: 1px solid var(--bbb-border);
    border-radius: 0.5rem 0.5rem 0 0;
}

/* Mock-up's `thead.bg-light.text-muted.text-uppercase`: 11px, uppercase, tracked out. The
   basket's own header (Wk1) is a plain 14px row and keeps it. */
.component__basketheader--review {
    padding: 0.5rem 0;
    font-size: 11px;
    font-weight: 600;
    text-transform: uppercase;
    letter-spacing: 0.05em;
    color: #6c757d;
}

/* "Artikel" heads the picture and the title together. */
.order__articles .component__basketheader > span:first-child {
    grid-column: span 2;
}

.order__articles .component__basketheader > span:nth-child(2) {
    text-align: center;
}

.order__articles .component__basketheader > span:nth-child(3),
.order__articles .component__basketheader > span:nth-child(4),
.order__articles .component__basketheader > span:nth-child(5) {
    text-align: right;
}

.order__articles .component__basketarticle {
    padding-block: 1rem;
}

.order__articles .component__basketarticle:last-of-type {
    border-bottom: 0;
    padding-bottom: 0;
}

/* Mock-up puts the final CTA bottom right at its own width, not stretched across the
   column, and gives it the filled `check_circle` glyph. */
.order__navi {
    justify-content: flex-end;
}

.order__navi > button[type="submit"] {
    width: auto;
    /* o3-theme pins this button to `height: 74px` (`.order__navi .btn`, for the full-width
       block button it used to be). With the padding overridden but not the height, it stayed
       74px tall against the mock-up's 56 — i.e. 18px of dead space inside. `auto` lets the
       padding define it: 2×16 + a 24px line = the mock-up's 56 exactly. */
    height: auto;
    padding: 1rem 3rem;
    font-weight: 700;
    display: inline-flex;
    align-items: center;
    gap: 0.5rem;
    /* The mock-up's `.btn-primary-brand` is `border: none`; Bootstrap's 1px (invisible, it
       matches the fill) was the last 2px between 58 and the mock-up's 56. */
    border: 0;
}

/* Mock-up sets a check_circle on the final CTA. */
.order__navi > button[type="submit"]::before {
    content: "check_circle" / "";
    font-size: 20px;
}

/* With PayPal chosen, the module swaps that CTA for an empty div the PayPal SDK renders its
   own button into (checkout_order_btn_submit_bottom.tpl). Two things follow:

   - It arrives with `float-right pull-right` and no `.paypal-button-wrapper`, i.e. the
     module's own width rules (out/src/css/paypal.css) never apply and those two Bootstrap-3/4
     classes do nothing in Bootstrap 5 — so as a flex child of `.order__navi` it had no width
     at all.
   - The button itself is PayPal's iframe. Its inside is not ours to restyle (nor should it be
     — it carries PayPal's mark), so what is matched is the box. The 4px corners need nothing:
     the module's default `shape: rect` already draws them.

   The two numbers are our own CTA, measured against a real SDK render (`client-id=test`) with
   this stylesheet: `.order__navi > button[type="submit"]` comes out 338.7 x 56 — 2x48px insets
   around "Zahlungspflichtig bestellen" at 1rem/700 Hanken Grotesk plus the 20px check_circle
   and its 0.5rem gap. So the label is what fixes the width; if SUBMIT_ORDER is ever retranslated,
   re-measure. Height cannot be matched exactly — the SDK caps `style.height` at 55 (see
   page/checkout/order.tpl) — so the 55px button is centred in a 56px box, half a pixel top and
   bottom.

   `display: flex` is load-bearing, not tidiness: the SDK renders `.paypal-buttons` as an
   inline-block, and inside a block container that line box added its 6.5px descender below the
   button (a 61.5px box around a 55px button), which pushed the footer down by that much on
   PayPal orders only. A flex container has no line box to inherit it from. */
.order__navi > .paypal-button-container {
    display: flex;
    align-items: center;
    width: 339px;
    height: 56px;
    max-width: 100%;
}

/* Column placements for the review table's own order (see the tracks above). Rows 1-4 hold
   title / Art-Nr in column 2; everything else spans them and centres. */
.order__articles .component__basketarticle {
    grid-template-rows: auto auto auto auto;
}

.order__articles .component__basketarticle > :first-child {
    grid-row: 1 / 5;
}

.order__articles .component__basketarticle .component__tobasket {
    grid-column: 3;
    grid-row: 1 / 5;
    justify-self: center;
    /* The mock-up shows a plain figure here. o3-theme includes the tobasket widget with
       `disabled=true`, so the +/- buttons and the frame render greyed out but still read as a
       control the customer could use. */
    border: 0;
}

.order__articles .component__basketarticle .component__tobasket-minus,
.order__articles .component__basketarticle .component__tobasket-plus {
    display: none;
}

.order__articles .component__basketarticle .component__tobasket-input {
    background-color: transparent;
    text-align: center;
    width: auto;
    min-width: 0;
}

/* The widget keeps the parent's `flex: 0 0 auto`, so with the buttons gone it still claimed
   its old width and spilled 15px into the columns either side of its track. */
.order__articles .component__basketarticle .component__tobasket {
    width: auto;
    max-width: 100%;
}

.order__articles .component__basketarticle-unitprice {
    grid-column: 4;
    grid-row: 1 / 5;
    text-align: right;
}

.order__articles .component__basketarticle-vat {
    grid-column: 5;
    grid-row: 1 / 5;
    align-self: center;
    text-align: right;
    /* QM 2026-09-04: body text colour like Menge and Einzelpreis, not the muted grey. */
}

.order__articles .component__basketarticle-price {
    grid-column: 6;
    grid-row: 1 / 5;
    align-self: center;
    text-align: right;
    white-space: nowrap;
}

/* `.order__navi` is not a flex row in the parent (unlike `.payment__navi`), so the
   justify-content above had nothing to act on. */
.order__navi {
    display: flex;
}

/* o3-theme ends the AGB form with an `<hr>` — inside a card that is a rule under nothing,
   and it was the empty band at the top of that block. Removed, not hidden. */
.order__agb hr {
    display: none;
}

/* The card's trailing non-EU customs note is small muted italic in the mock-up, and it is bare
   text inside the form — no wrapper of its own to hang the style on — so the card carries it
   and the two elements that are NOT that note take it back off below. */
.order__agb {
    font-size: 0.875rem;
    font-style: italic;
    color: #6c757d;
}

/* QM ("Fehlt + Font style"). Two things, both from html/Wk4.html's AGB card:

   1. Its heading sits on the same `card-header bg-light border-bottom` strip as the Warenkorb
      card above — the AGB card was the one heading still rendered plain.
   2. The consent line is not the customs note: the mock-up sets it as ordinary body copy
      (`form-check-label small`), while the card-level italic above was reaching the heading
      and the checkbox as well, which is what made the whole block read as a footnote. */
.order__agb > .title-small {
    margin: -1.5rem -1.5rem 1.5rem;
    padding: 1rem 1.5rem;
    background-color: #f8f9fa;
    border-bottom: 1px solid var(--bbb-border);
    border-radius: 0.5rem 0.5rem 0 0;
    font-style: normal;
}

.order__agb .form-check {
    font-style: normal;
    color: var(--bbb-text);
}

/* QM ("font color + ohne underline"): the mock-up's AGB link is
   `text-industrial-orange fw-bold text-decoration-none`. The word is a CMS link
   (oxrighttocancellegend) and carries o3-theme's `fontunderline` class, so the underline has
   to be taken off here rather than in the content. */
.order__agb .form-check a {
    color: var(--bbb-orange);
    font-weight: 700;
    text-decoration: none;
}

.order__agb .form-check a:hover {
    color: var(--bbb-orange-dark);
    text-decoration: underline;
}

/* Below md the mock-up's `.button-wrapper` stacks the pair full-width with the primary on
   top (`order-1 order-md-2`) — same as the basket's action bar. Kept side by side, the CTA
   label wraps to two lines on a phone. */
@media (max-width: 767.98px) {
    .user__navi,
    .payment__navi,
    .order__navi {
        flex-direction: column-reverse;
        align-items: stretch;
        gap: 0.75rem;
    }

    .user__navi > .btn,
    .payment__navi > .btn,
    .order__navi > .btn,
    .order__navi > button[type="submit"],
    /* The PayPal button stands in for that CTA, so it stacks full-width with it. Its desktop
       width above is a fixed px value, which would otherwise beat the column's `stretch`. */
    .order__navi > .paypal-button-container {
        width: 100%;
        justify-content: center;
    }
}

/* No mobile area map: o3-theme only makes `.order` a grid from md up, so below that the
   cards simply follow DOM order (… basket, totals, customs note, CTA) — which reads fine and
   is one less thing to keep in sync. What DOES need handling below lg is the line-item table,
   whose six desktop tracks (490px of them fixed) do not fit a phone — nor a tablet: at 768px
   the card is only part of o3-theme's 12-track `.order` grid, so the `1fr` title track
   collapsed to 74px and the product name printed across the Menge column. Same breakpoint as
   the basket's own table. */
@media (max-width: 991.98px) {
    /* QM: "In Vorlage auch hier mit Slide gelöst" — the review table keeps its columns and
       scrolls sideways instead of reflowing.

       This used to stack the six tracks into one column, which lost the header row with them:
       the unit price, quantity, VAT rate and line total ended up under the product name as four
       bare numbers ("5,80 €", "1", "19%", "5,80 €") with nothing saying which was which. On a
       page whose whole job is checking the order before paying, that is the one place the labels
       have to survive — so the table stays a table and the visitor swipes it.

       760px keeps ~190px for the product name once the five fixed tracks (490px) and the gaps are
       taken; below that the title wraps to four lines and the row grows taller than the screen. */
    .order__articles,
    .order .basket__articles {
        overflow-x: auto;
        -webkit-overflow-scrolling: touch;
        /* QM: sideways scroll on cl=order into ~410px of white. The table scrolls inside this
           box as intended — but its 760px of layout overflow was ALSO being counted into the
           ancestors' scrollable area: `main` and `body` measured 801px against a 390px
           viewport, even though nothing outside this box is wider than the screen (walked
           every element under main — no other offender). `body { overflow-x: clip }` hides
           that in Chromium, which is why the page looks fine here and scrolls on a phone: the
           clip is the only thing holding it, and a browser that does not apply it scrolls the
           801px.

           `contain: paint` states what is already true of a scroll container — its descendants
           do not paint outside it — and that stops the overflow propagating. Measured: body
           and main go 801 -> 390 and the table keeps its own sideways scroll (scrollWidth 808
           in a 358px box), which is the "In Vorlage auch hier mit Slide gelöst" behaviour.
           `overflow: hidden` here, or a clip on `.order`, do NOT fix it — only containment
           does; a clip on `main` also works but reaches far wider than this one table. */
        contain: paint;
    }

    .order__articles .component__basketheader,
    .order__articles .component__basketarticle,
    .order .basket__articles .component__basketheader,
    .order .basket__articles .component__basketarticle {
        min-width: 760px;
    }

    /* The header row is what makes the columns legible, and it is exactly what o3-theme hides on
       small screens. It is the point of the slide, so it comes back. */
    .order__articles .component__basketheader,
    .order .basket__articles .component__basketheader {
        display: grid;
    }

    /* Pinned so the card keeps its title while the table underneath is swiped. Negative margins
       have to match the card's own 1.5rem padding, or the strip stops short of both edges.

       QM: "Da verschiebt sich was mit", arrow on this strip. `left: 0` was the bug. A sticky box
       is offset against the scrollport but *constrained to its containing block*, which here is
       the card's content box — so the clamp cancelled the -1.5rem left margin and put the strip
       back at the content edge while it kept the width the margins gave it. Measured at 414px:
       the band ran x=40..407 against a visible right edge of 384, i.e. its right end and the
       air around "Ändern" sat 23px outside the scrollport, and the offset changed as the table
       was swiped.

       `left: -1.5rem` cancels exactly that clamp — same value as the margin and as the card's
       padding, so it is one number, not a fudge. Measured after: x=16..383, precisely the card's
       padding box, band still bleeding to both edges, "Ändern" ending at 359 well inside, and
       the strip holding still while the rows scroll under it. */
    .order__articles > .title-small {
        position: sticky;
        left: -1.5rem;
        margin: -1.5rem -1.5rem 1.5rem;
    }
}

/* ==========================================================================
   Imported CMS pages — the Bootstrap grid the content is authored with
   ========================================================================== */

/* QM on /Wir-ueber-uns/: "Anordnung wie auf Live: Bild rechts links Text. Nur Mobil
   untereinander." — the team blocks were stacking image over text at every width.

   Not a content bug: the imported markup is a faithful conversion of live's
   `<div class="row"><div class="col-sm-2">img</div><div class="col-sm-10">text</div></div>`
   into Bootstrap 5's `col-md-*`.

   o3-theme's **minified** build is what a productive shop loads, and PurgeCSS strips from it
   every class the theme's own templates never mention. Checked class by class against
   out/o3-theme/src/css/main.min.css:

       col-md-3  col-md-4  col-md-6  col-md-8  col-md-12  col-3  col-6   -> present
       col-md-2  col-md-10                                               -> DROPPED

   (They are all in the unminified main.css, which is why this looks fine with
   `oxproductive` off.) And the two that were dropped are precisely the portrait-plus-text pair,
   which is why this one page broke while every other imported page came out right. `.row`
   survives — templates do use it — and is still `display: flex` with `.row > * { width: 100% }`,
   so both columns claimed the full width and wrapped. Same trap as the `d-xl-*` utilities noted
   at the header burger.

   So exactly those two come back, and nothing else: re-declaring the classes that main.min.css
   already carries would be two stylesheets fighting over one value.

   Scoped to `.cmsContent`, which is the article the CMS page renders into: this is here to
   serve pasted editor markup, and the theme's own templates must keep getting their widths
   from o3-theme.

   The media query is the mobile answer too — below md the columns stay full width and stack,
   which is the "Nur Mobil untereinander" half of the note. */
@media (min-width: 768px) {
    .cmsContent .col-md-2 {
        width: 16.66666667%;
    }

    .cmsContent .col-md-10 {
        width: 83.33333333%;
    }
}

/* QM, same note: "Abstände beachten! Bild Kontakt ist ein Block -> abstand -> nächster Block."
   With the columns working, consecutive member blocks touched — measured 0px between one row
   and the next.

   Live has no row margin either; what separates its blocks is that its text column happens to
   outgrow the photo by ~30px on every person. That is content luck, and it does not survive
   here: measured on this page the space below a photo runs 26px for Andreas, Norbert and Manuel
   but 2px for Martina, whose text is one line shorter. So the gap becomes a real margin instead
   of a side effect of how much someone has to say.

   Matched to the pattern rather than to the page: a row holding a `col-md-2` beside a
   `col-md-10` is the portrait-plus-text block, which is also what meh_opressearchiv is built
   from (46 of them). The spacer rows the content carries for the big section breaks
   (`<div style="height:50px">` inside a `col-md-12`) are untouched, so the deliberate spacing in
   the content still reads as authored. */
.cmsContent > .row:has(> .col-md-2):has(> .col-md-10) {
    margin-bottom: 2rem;
}

/* ==========================================================================
   Add-to-cart confirmation on a phone (QM qms10.png)
   ========================================================================== */

/* QM: "Warenkorb Popup für mobil optimieren." with an allfirewalls.de popup beside it —
   "Box größe und Lage wie hier bei allfirewalls. Inhalte von Druckweiterverarbeitung."

   Measured on an iPhone XR (414x896), which is what QM tested: `#offcanvas__basket` came up as
   Bootstrap's full-height right-hand drawer, **317 x 896** — 77% of the screen width and the
   entire height for one line of confirmation, with a large empty band in the middle. The
   reference is a compact card near the top, sized to its content.

   Nothing in the markup needed changing, and the content stays ours as QM asked: the panel
   already holds the title with the item count, the "Neuer Artikel wurde in den Warenkorb gelegt"
   confirmation, the product line, the gross total and — already side by side, from o3-theme's
   `.offcanvas__basket-navigation` — "Warenkorb zeigen" / "Zur Kasse". The drawer geometry was the
   whole problem.

   Below md only. On a tablet the drawer is a reasonable width and the mock-ups keep it; this is
   the phone case QM photographed.
   -------------------------------------------------------------------------- */
@media (max-width: 767.98px) {
    #offcanvas__basket.offcanvas {
        /* Anchored top with a gutter each side instead of pinned to the right edge full-height.
           `inset` beats Bootstrap's `top/right/bottom` longhands on the direction class without
           !important because it is later in the cascade and equally specific. */
        inset: 0.75rem 0.75rem auto 0.75rem;
        width: auto;
        height: auto;
        /* o3-theme caps the drawer at `calc(100% - 82px)` so a strip of the page stays visible
           beside it. That cap is why the card came out 317px wide on a 399px viewport instead of
           filling the gutters — the insets were already right. A top-anchored card does not need
           the peek; the backdrop already shows it is a layer over the page. */
        max-width: none;
        /* Sized to content, but a basket with many lines must not run off the screen — the
           article list is the part that scrolls (o3-theme already gives it `overflow-y: auto`). */
        max-height: calc(100dvh - 1.5rem);
        border: 0;
        border-radius: 0.75rem;
        box-shadow: 0 12px 32px -8px rgba(0, 0, 0, 0.28);
    }

    /* The drawer slides in from the right; a card at the top comes from the top. `.hiding` as
       well as `:not(.show)`, because Bootstrap re-applies the direction class's transform for
       the closing transition and would otherwise fly it out sideways. */
    #offcanvas__basket.offcanvas:not(.show),
    #offcanvas__basket.offcanvas.hiding {
        transform: translateY(-100%);
    }

    /* This is what produced the empty band: o3-theme lets the article list grow into whatever
       height the drawer has. In a content-sized card it must not grow. */
    #offcanvas__basket .offcanvas__basket-articles {
        flex-grow: 0;
    }

    /* `mb-5` (3rem) under the confirmation is drawer spacing — half the reference card's total
        height would have been that one margin. Utilities are !important, so this has to be too. */
    #offcanvas__basket .offcanvas-body > .alert {
        margin-bottom: 1rem !important;
    }

    /* The single biggest thing making the card tall: o3-theme pads this strip with
       `calc(var(--bs-offcanvas-padding-x) * 2)` on all four sides — 40px around one line of
       text, 170px of the old 607px height for the total alone. */
    #offcanvas__basket .offcanvas__basket-costs {
        padding: 0.75rem 1.25rem;
        margin-inline: -1.25rem;
    }

    #offcanvas__basket .offcanvas__basket-costs > * {
        font-size: 1rem;
    }

    #offcanvas__basket .offcanvas__basket-costs > *:nth-child(2) {
        font-size: 1rem;
    }

    /* Both buttons on one line, as in the reference, whatever the label lengths: o3-theme's
       btn-lg padding at this width pushed "Warenkorb zeigen" onto two lines. */
    #offcanvas__basket .offcanvas__basket-navigation {
        margin-block: 1rem 0;
    }

    #offcanvas__basket .offcanvas__basket-navigation .btn {
        padding-inline: 0.75rem;
        font-size: 0.9375rem;
    }
}

/* --- The basket page's header on a phone (QM qms11.png) --------------------
   QM, on /warenkorb/ at 414px: "Suche entfernen" on the search field, and "Icons Höhe Logo"
   with the empty space beside the logo boxed — the account and cart icons belong on the logo's
   line, not on a row of their own.

   Measured before: logo at y=0, actions wrapped to y=75, search to y=116, header 207px tall —
   three rows to show a logo and two icons.

   Why it wrapped: o3-theme gives `.header__actions` `flex: 1 1 calc(50% - 10px)` below md, and
   this theme's logo is `max-width: 196px` against a 369px container. 196 + 180 does not fit, so
   the actions took a line of their own and pushed the search onto a third. Letting the actions
   size to their content and pushing them over with `margin-left: auto` puts them back on the
   logo's line, right-aligned — which is also what the same QM round asked for on desktop.

   `:not(:has(.header__mainnav))` is the basket page: it is the one page that keeps the full
   header class while rendering no category nav, which is exactly why the mobile collapse block
   above skips it (see the note there) and why its search and actions are left in the flow to
   wrap. The inverse guard here is the other half of that same distinction.

   Search hidden below md only. On a desktop basket it is one inline icon costing nothing, and
   QM's note is about the phone, where it claims a whole 44px row inside a checkout step.
   -------------------------------------------------------------------------- */
@media (max-width: 767.98px) {
    header:not(.is-minimal):not(:has(.header__mainnav)) .header__search {
        display: none;
    }

    header:not(.is-minimal):not(:has(.header__mainnav)) .header__actions {
        flex: 0 0 auto;
        width: auto;
        margin-left: auto;
    }
}


/* --------------------------------------------------------------------------
   My account — QM went through /mein-konto/ (three points, all on this page).
   -------------------------------------------------------------------------- */

/* QM: "Orange und ohne Underline". Was `.card-header .btn { color: var(--bbb-orange) }`, which
   got the colour right but left the underline: o3-theme's `.btn-edit` is a dashed-underlined
   text link (`text-decoration: underline; text-decoration-style: dashed`), so the dashes have
   to be switched off explicitly. Narrowed to `.btn-edit` inside the dashboard as well — the
   same `.btn-edit` class is the orange pencil on checkout step 4, which has its own rules
   above, and `.card-header .btn` would also have caught any future control in a card header.

   `.user__billingaddress .btn-edit` is the same control on "Rechnungs- und Lieferadressen"
   (and on step 2 for a logged-in customer); it was a second rule of its own
   (`.user__billingaddress button`) with the same underline left behind, so it joins this one. */
body.cl-account .card-header .btn-edit,
.user__billingaddress .btn-edit,
.user__shippingaddress-change .btn-edit {
    color: var(--bbb-orange);
    text-decoration: none;
}

body.cl-account .card-header .btn-edit:hover,
body.cl-account .card-header .btn-edit:focus,
.user__billingaddress .btn-edit:hover,
.user__billingaddress .btn-edit:focus,
.user__shippingaddress-change .btn-edit:hover,
.user__shippingaddress-change .btn-edit:focus {
    color: var(--bbb-orange-dark);
    text-decoration: underline;
}

/* QM 2026-09-08 (BBB-90): the saved delivery address's "Ändern" is orange like the billing one
   (rule above). The trash can was in the tile's bottom-right corner (o3-theme's
   `button:last-of-type { right: 5px; bottom: 10px }`); QM wanted it up beside "Ändern", then
   BEHIND it, with room, and visibly a separate control — "they look like they link together".
   So: "Ändern" first, a hairline divider, the trash can at the tile's right edge in the muted
   grey the other secondary controls use, orange only on hover. Both stay absolutely positioned
   siblings (the trash button is a Bootstrap `collapse` toggled by "Ändern"), so this is offsets
   only: trash at 15px, "Ändern" one glyph + divider + gaps further left. */
.user__shippingaddress-change .form-check-label .btn-edit {
    right: calc(15px + 18px + 0.75rem + 1px + 1rem);
}

.user__shippingaddress-change .form-check-label .btn-delete:last-of-type {
    top: 20px;
    right: 15px;
    bottom: auto;
    padding: 0 0 0 0.75rem;
    line-height: 17px;
    border-left: 1px solid var(--bbb-border);
    border-radius: 0;
}

.user__shippingaddress-change .form-check-label .btn-delete svg path {
    fill: #a1a5ad;
}

.user__shippingaddress-change .form-check-label .btn-delete:hover svg path,
.user__shippingaddress-change .form-check-label .btn-delete:focus svg path {
    fill: var(--bbb-orange);
}

/* QM: "Ecken abrunden". o3-theme sets `--bs-border-radius: 0` shop-wide, which leaves the
   dashboard cards and the sidebar nav as the only square boxes on the page; 0.5rem is the
   radius the rest of this theme's cards carry.

   `--bs-border-radius`, not `border-radius`: Bootstrap derives `--bs-card-inner-border-radius`
   from it, and that is what rounds the header strip along with the card — setting the outer
   radius alone leaves the grey `card-header` squaring off the top two corners. */
body.cl-account .card {
    --bs-border-radius: 0.5rem;
}

/* The nav is on every account page, not only the dashboard, so it is not scoped to cl-account.
   `overflow: hidden` because the active item's fill runs the full width of the box and would
   otherwise square the corner off again. */
.account__menu {
    border-radius: 0.5rem;
    overflow: hidden;
}

/* QM (qm18): "Ecken abrunden", second round — the two buttons on these pages. o3-theme's
   `--bs-border-radius: 0` squares off `.account__menu-logout` ("Abmelden") and the address
   form's "Speichern" as well; 0.375rem is the radius this theme's other form buttons carry
   (the listing cards' pair, the checkout action bars).

   `#accUserSaveTop` is the id o3-theme gives that submit — used rather than a bare
   `.btn-primary`, which on this page would also be nothing else, but on the account pages as a
   group would start catching controls this note is not about. */
.account__menu-logout,
#accUserSaveTop {
    border-radius: 0.375rem;
}

/* QM ("this text goes outside the box"): on Bestellhistorie the article line runs past the
   card's right edge, taking the price with it.

   o3-theme builds that line as `display: flex; gap: 20px` — thumbnail, title, price — and
   gives the price `flex-grow-1 text-nowrap`. A flex item will not shrink below its own
   min-content, and the title's min-content is its longest word
   ("1-Spindelpapierbohrmaschine,"), so once thumbnail + longest word + price no longer fit
   the half-width column, the row cannot compress and the price is pushed out of the card
   instead. It is width-dependent, which is why it shows on some orders and not others.

   The title gets `min-width: 0` (so it may shrink past min-content) plus `overflow-wrap`
   (so the long word breaks rather than spilling), and the price stops flexing — it keeps
   its intrinsic width and stays on the right, which is all `flex-grow-1` was there for. */
.account_order__details-article > div:first-of-type {
    flex: 1 1 auto;
    min-width: 0;
    /* `break-word`, not `anywhere`: it breaks a word only when it would otherwise overflow,
       so a title that fits keeps its natural wrap points. */
    overflow-wrap: break-word;
}

.account_order__details-article > div:last-child {
    /* `!important` because Bootstrap's `.flex-grow-1` utility (on the price in o3-theme's
       markup) is itself `!important`. While the price took the row's free space the title
       stayed at its narrowest and broke mid-word even on a wide screen; with the growth on
       the title instead, the price keeps its own width and still sits right. */
    flex-grow: 0 !important;
    flex-shrink: 0;
}

/* QM: "Box können wir breiter machen". From lg up o3-theme gives the nav `col-lg-3` (25%) and
   the panel `col-lg-8`, with `offset-md-1` as the gutter between them — and at 25% the longest
   item breaks mid-word ("Newslettereinstellun-gen"). The nav takes that 8.33% offset; the panel
   keeps its own width, so nothing else on the page moves.

   Keyed on the nav itself rather than on a body class: every account subpage includes the same
   sidebar, and they do not share one class. Only lg and up — at md the nav is already 33%. */
@media (min-width: 992px) {
    main .row > div:has(> .account__menu) {
        width: 33.333333%;
    }

    main .row > div:has(> .account__menu) + div {
        width: 66.666667%;
        margin-left: 0;
    }
}

/* QM: "orange" on the "Mehr" toggle in the order history. o3-theme paints it
   `var(--bs-primary)`, which this theme maps to the brand navy — so the one interactive control
   in that row came out the same colour as the order number beside it. Orange is this theme's
   interaction colour everywhere else (nav labels, mega menu, footer links, the pencil on the
   review step), and it hovers a step darker like the rest of them. */
.account_order .btn {
    color: var(--bbb-orange);
}

.account_order .btn:hover,
.account_order .btn:focus-visible {
    color: var(--bbb-orange-dark);
}
