/* ===========================================================================
   components.css — the skin.

   Ported from the approved design draft (the shop overview mockup) and applied
   to the ONE page this repository serves: the products inside one category.
   Everything visual lives here; everything structural stays in base.css, so
   removing this file leaves a plain, readable, correctly ordered document.

   NOT A HEX VALUE IN HERE, and not a colour name either. Every colour resolves
   from tokens.css through var(--*) — that is what makes the accent changeable in
   one place, and gate G2 proves it by swapping the token for a sentinel and
   finding nothing of the brand left on the rendered page.

   What is deliberately absent, because each is its own slice: the layout shell
   (logo topbar, trust strip, footer with links), the navigation, and the product
   detail page. The shop chrome in the mockup — search field, account, cart — is
   absent for a stronger reason: the launch is a catalogue, not a shop, so those
   controls have no function behind them and are not ours to imply.
   =========================================================================== */

/* --- The document ------------------------------------------------------- */

body {
    background: var(--color-surface);
    color: var(--color-text);
}

/* Usage rule: a link is normal-size text, so it takes step 700 or darker. Step 600
   measures 3.26 : 1 on the muted surface the footer uses and is a surface,
   border and large-headline step — not a text one. */
a {
    color: var(--color-accent-hover);
}

a:hover {
    color: var(--color-accent-active);
}

/* --- The minimal shell (trust strip and footer links still open, R5) ----- */

.site-header {
    background: var(--color-surface);
    border-bottom: 1px solid var(--color-border);
}

.site-footer {
    background: var(--color-surface-muted);
    border-top: 1px solid var(--color-border);
}

.skip-link:focus {
    background: var(--color-anchor);
    color: var(--color-surface);
}

/* --- The breadcrumb (SWEB-133 — one implementation, site-wide) ----------
   Rendered once by layout.php, from partials/breadcrumb.php, for every page
   but the homepage. It replaces THREE page-local implementations: `.crumbbar`
   (product page), `.inhalt-breadcrumb` (company pages and an Aktuelles item)
   and a third, hard-coded twice in the Kontakt pages under its own aria-label
   and never in this stylesheet's own map until now (see that section's own
   note for its old name — not spelled out here: a later slice's own fence
   scans everything above its banner for its own prefix). One separator, one
   rule: `.breadcrumb-sep`. */

.breadcrumb {
    padding: 14px 0;
    background: var(--color-surface-tint);
    border-bottom: 1px solid var(--color-border);
    font-size: var(--text-small);
}

.breadcrumb-list {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 7px;
    margin: 0;
    padding: 0;
    list-style: none;
}

.breadcrumb-item {
    display: flex;
    align-items: center;
    gap: 7px;
}

/* Usage rule: a breadcrumb separator is normal-size text, so it takes step 700 or
   darker — the same rule that moved .crumb-sep off --color-text-subtle
   (3.36 : 1, a recorded defect) rather than reproducing it here. */
.breadcrumb-sep {
    color: var(--color-text-muted);
}

.breadcrumb-link {
    color: var(--color-accent-hover);
    text-decoration: underline;
    text-underline-offset: 2px;
}

.breadcrumb-link:hover,
.breadcrumb-link:focus-visible {
    color: var(--color-accent-active);
}

.breadcrumb-current {
    color: var(--color-text);
    font-weight: 600;
}

/* --- The page head ------------------------------------------------------ */

.pagehead {
    background: linear-gradient(180deg, var(--color-surface-tint) 0%, var(--color-surface) 100%);
    border-bottom: 1px solid var(--color-border);
}

/* NO RULE WHERE A HERO BAND FOLLOWS -- Tom, 2026-08-29, looking at
   /unternehmen. The line and the band are both saying "the head ends here",
   and two separators one above the other read as a stray rule rather than as
   structure. The band wins because it carries the claim; the line keeps its
   job on every page that has no band. */
.pagehead:has(+ .inhalt-hero) {
    border-bottom: 0;
}

.pagehead h1 {
    margin: 0;
    font-size: var(--text-title-fluid);
    font-weight: 700;
    line-height: 1.1;
    letter-spacing: -0.02em;
    color: var(--color-anchor);
    overflow-wrap: break-word;
}

/* The warning eyebrow inside a heading — SWEB-444.

   `display: block` IS THE LINE BREAK. The specification is "„Achtung:", then a
   line break, then the rest": a `<br>` in the template would be markup carrying
   a layout decision, and the two parts would then be unable to sit on one line
   at any width even if a later design wanted that. A block-level span inside
   the heading breaks the line, keeps the heading one element, and is one
   property to change if it ever should not.

   It inherits the h1's size, weight and leading and states only what differs —
   the colour — so it tracks the fluid title scale automatically and cannot
   drift out of step with the heading it belongs to. The same rule serves the
   feed tile (.aktuelles-title), which is smaller for the same reason and by the
   same inheritance.

   Nothing about the heading changes when no eyebrow is stored: the span is not
   rendered at all. */
.title-eyebrow {
    display: block;
    color: var(--color-warning-eyebrow);
}

/* The count line doubles as the section label of the mockup: accent text,
   uppercase, with the small accent marker the mockup puts before a list entry. */
/* Usage rule: 11px uppercase is normal-size text — step 700, not 600 (3.37 : 1 on the tint). */
.pagehead-count {
    margin: 14px 0 0;
    display: inline-flex;
    align-items: center;
    gap: 8px;
    font-size: var(--text-mini);
    font-weight: 700;
    letter-spacing: 0.1em;
    text-transform: uppercase;
    color: var(--color-accent-hover);
}

.pagehead-count::before {
    content: "";
    width: 8px;
    height: 8px;
    flex: 0 0 auto;
    border-radius: 2px;
    background: var(--color-accent);
}

.empty-note {
    margin: 0;
    padding: 14px 16px;
    background: var(--color-accent-surface);
    border: 1px solid var(--color-accent-badge-border);
    border-radius: 5px;
    color: var(--color-accent-active);
    font-size: var(--text-body);
}

/* --- The product grid --------------------------------------------------- */

.product-list {
    display: grid;
    grid-template-columns: repeat(4, 1fr);
    gap: 16px;
}

@media (max-width: 900px) {
    .product-list {
        grid-template-columns: repeat(3, 1fr);
    }
}

@media (max-width: 640px) {
    .product-list {
        grid-template-columns: repeat(2, 1fr);
    }
}

@media (max-width: 380px) {
    .product-list {
        grid-template-columns: 1fr;
    }
}

/* SWEB-234 — THE GROUPED LISTING RUNS AT THREE COLUMNS.
   Tom's ruling on the rendered page, 29.08: at four columns the article
   numbers inside a card's disclosure wrap onto a second line. A grouped card
   carries a list of Art.-Nr. that an ungrouped card does not, so it needs more
   width per card than the flat listing does — three columns give each card
   333px of the 1080px wrap instead of 246px.

   IT IS A MODIFIER, AND `.product-list` ITSELF IS NOT TOUCHED. Both templates
   render that class, so changing it would silently re-column the default
   listing as well — the one thing this whole route exists to avoid.

   WRAPPED IN `min-width: 901px` RATHER THAN PLACED BEFORE THE BREAKPOINTS.
   `.product-list--gruppiert` and `.product-list` are both a single class, so
   they have equal specificity and source order alone decides between them: a
   plain rule here would out-rank the max-width rules above and force three
   columns onto a 380px phone. Bounding it to the range it is about makes the
   rule true wherever anybody later moves it. Below 900px the grouped listing
   inherits the flat one's 3 / 2 / 1 ladder, which is already right. */
@media (min-width: 901px) {
    .product-list--gruppiert {
        grid-template-columns: repeat(3, 1fr);
    }
}

/* SWEB-266 — „Das könnte Sie auch interessieren" runs at three columns.
   Three cards, three columns, one row: a suggestion row that wraps onto a
   second line reads as a listing rather than as a suggestion.

   BOUNDED TO `min-width: 901px` FOR THE SAME REASON `--gruppiert` IS, and the
   comment above carries the argument: modifier and base class have equal
   specificity, so an unbounded rule here would out-rank the max-width rules
   above it and force three columns onto a 380px phone. Below 901px the row
   inherits the listing's own 3 / 2 / 1 ladder, which is already the clean
   wrap AC7 asks for — the cards are the listing's cards, so they want the
   listing's breakpoints. */
@media (min-width: 901px) {
    .product-list--related {
        grid-template-columns: repeat(3, 1fr);
    }
}

/* The section itself, not the cards in it. A rule and a tinted band mark it as
   a different KIND of thing from the product above — the same two devices
   `.allcategories` uses on /kategorien to separate the band from the tiles.
   No padding or spacing of its own: `.section` (base.css) already owns the
   vertical rhythm every band on the site shares. */
.related {
    background: var(--color-surface-tint);
    border-top: 1px solid var(--color-border);
}

.product-list-item {
    display: flex;
    flex-direction: column;
    overflow: hidden;
    background: var(--color-surface);
    border: 1px solid var(--color-border);
    border-radius: 6px;
    box-shadow: 0 1px 4px rgba(0, 0, 0, 0.05);
    transition: border-color 0.15s, box-shadow 0.15s;
}

/* They ARE links now — the product detail page is this slice — so the card may
   lift. The comment above used to say the opposite and had to change with the
   behaviour: a stale comment about a deliberate restraint is worse than none,
   because the next person removes the restraint believing it was never
   considered. */
.product-list-item:hover {
    border-color: var(--color-accent);
    box-shadow: 0 6px 18px rgba(0, 0, 0, 0.10);
    transform: translateY(-2px);
}

/* The whole name is the target, not the few pixels of the text itself. */
.product-name a {
    color: inherit;
    text-decoration: none;
}

/* Usage rule: a product link is normal-size text — step 700 (600 measures 3.55 : 1). */
.product-name a:hover,
.product-name a:focus-visible {
    color: var(--color-accent-hover);
    text-decoration: underline;
}

/* --- The image slot ----------------------------------------------------- */

/* SWEB-259 — square, not 4:3. A placeholder used to be shorter than a
   photograph because the two aspect ratios disagreed; one ratio, square, means
   the slot is the same box whichever branch below draws into it, so the row
   this card sits in never depends on which one that was. Same value `.pd-media`
   already uses on the product page — this card brings the listing tiles to
   match it rather than inventing a second square.

   MEASURED 2026-08-30, Playwright/Chromium, 1280×900 viewport, on the running
   local app:
     /kategorie/roehrchen (32 cards, 3-col grouped grid) — every
     `.product-thumb` bounding box is 331.3×331.3px, photograph or placeholder
     alike; the 32 cards fall into 11 row-tops (ten rows of 3, one of 2) and
     every card sharing a row-top shares the same y exactly.
     /kategorien (12 tiles) — every `.category-thumb` is 331.3×331.3px, 4 rows
     of 3, same per-row y match.
     /produkt/einbettform-mega-ca-33x24x12mm-11660 (no photo) and
     /produkt/bionet-einbettkassette-hellgruen-10505 (photo) — both
     `.pd-media` boxes measure 416.7×416.7px, identical. */
.product-thumb {
    position: relative;
    display: flex;
    align-items: center;
    justify-content: center;
    aspect-ratio: 1 / 1;
    background: var(--color-surface);
    border-bottom: 1px solid var(--color-border-subtle);
}

.product-thumb-tag {
    position: absolute;
    top: 9px;
    left: 9px;
    padding: 2px 7px;
    background: var(--color-surface);
    border: 1px solid var(--color-border);
    border-radius: 3px;
    font-size: var(--text-mini);
    letter-spacing: 0.05em;
    text-transform: uppercase;
    color: var(--color-text-subtle);
}

.product-thumb-figure {
    width: 58%;
    height: 70%;
}

/* SWEB-203 — the real photograph, in the same slot the placeholder occupies.
   `max-*` rather than `width`/`height`: `object-fit: contain` scales UP as
   readily as it scales down, and Süsse's photographs are a mixed bag of sizes —
   a small one blown up to fill the card is blurry, where the same one left at
   its own size in the middle of the slot is not. The 8% inset keeps a
   freigestellt product off the slot's own bottom border. `.product-thumb` is
   already a centring flex container with a fixed 4:3 box, so nothing here sets
   a position and the card's height does not depend on which branch rendered. */
.product-thumb-image {
    max-width: 92%;
    max-height: 92%;
    object-fit: contain;
}

/* --- The card body ------------------------------------------------------ */

.product-body {
    display: flex;
    flex: 1;
    flex-direction: column;
    align-items: flex-start;
    gap: 9px;
    padding: 14px 15px 16px;
}

/* SWEB-475 — WIDTH: 100%, SO THE ITEM ACTUALLY REACHES THE EDGE `overflow-wrap`
   NEEDS. `.product-body`'s `align-items: flex-start` (SWEB-234, see the comment
   on `.product-body` — kept for the disclosure marker below) makes a flex
   child with no width of its own shrink-to-fit its content instead of
   stretching to the card. `.product-name` has no width, so at 2 columns an
   unbroken German compound word (measured: "Rückstellmusterbeutel",
   "Vernichtungsbeutel…", Fred's screenshot, 11.09) sized wider than the shrunk
   box, past the card's edge, where `.product-list-item`'s `overflow: hidden`
   cut it — no wrap, no ellipsis, letters missing. `overflow-wrap: break-word`
   was already here and did nothing, because it never got a box narrow enough
   to need breaking inside. `width: 100%` gives it one. */
.product-name {
    margin: 0;
    width: 100%;
    font-size: var(--text-body);
    font-weight: 700;
    line-height: 1.25;
    color: var(--color-anchor);
    overflow-wrap: break-word;
}

/* The Art.-Nr. is the identifying datum on a laboratory catalogue card, so it
   gets the accent tint the ramp publishes for a badge: step 100 as the fill,
   200 as the frame, 800 as text on an accent tint. */
/* Shrink-wrapped so the fill ends with the text: the <p> is a block box and
   would otherwise paint the badge across the whole row. `width: fit-content`
   rather than `display: inline-block` on purpose — it keeps the block box, so
   no line box and no strut from the 1.5 body line-height are introduced and the
   row height is unchanged. Inside .product-body (flex, align-items: flex-start)
   the item already shrink-wraps, so nothing moves there either. */
.product-sku {
    margin: 0;
    width: fit-content;
    max-width: 100%;
    padding: 2px 8px;
    background: var(--color-accent-badge);
    border: 1px solid var(--color-accent-badge-border);
    border-radius: 3px;
    font-size: var(--text-mini);
    font-weight: 600;
    letter-spacing: 0.02em;
    color: var(--color-accent-active);
}

/* SWEB-157 — the pack size, so two cards with the identical name (e.g. Xylol /
   UN 1307 in several sizes) read apart at a glance. Same label/value pattern
   the product detail page used to keep in its own `.pd-ve` — since SWEB-264
   that page folds Verpackungseinheit into `.pd-specs` instead, so this card's
   own pair is now the only place the pattern still stands apart. Sized for
   the card rather than the wider detail column; absent for a product with no
   pack size, so there is no rule here for an empty state to leave visible. */
.product-ve {
    margin: 0;
    font-size: var(--text-small);
    color: var(--color-text);
}

.product-ve-label {
    font-weight: 600;
    color: var(--color-anchor);
}

/* SWEB-230 — THE PACK-SIZE DISCLOSURE ON A GROUPED CARD.
   Drawn by the category listing, `/kategorie/{slug}`, and only on a card the
   rule grouped. An ungrouped or declined card, and every card on `/suche`, has
   no markup matching any rule below, so none of it reaches them.

   IT IS <details>/<summary>, THE BROWSER'S OWN, AND THAT IS THE POINT. This
   site carries no .js file and no behaviour of its own written in script — the
   one exception being the consent-gated measurement snippet — so a control that
   swaps a card's contents would have introduced both the first functional
   script and the first thing here that behaves differently with scripting off.
   A disclosure opens with no script, works by keyboard, and its contents are
   ordinary anchors a crawler follows.

   The marker is left as the browser draws it rather than replaced with our own
   glyph: it is the affordance a visitor already recognises, it rotates on open
   without anything from us, and it inherits `color` so the ramp still decides
   the ink. Nothing here sets a colour literal — every value is a token.

   SWEB-234 (review) — THE MARKER IS `inside`, AND THAT IS WHAT PUTS IT ON THE
   CARD'S OWN TEXT INSET. Tom, on the rendered card: the triangle sat hard
   against the card's left wall while the product name above it sat at the text
   inset. `list-style-position: outside` — the browser default, and what this
   rule used to say — renders the marker OUTSIDE the summary's content box, so
   it hung left into `.product-body`'s padding and `.product-list-item`'s
   `overflow: hidden` clipped what spilled past the border.

   `inside` makes the marker the first inline box INSIDE that content box, so
   its left edge IS the summary's left edge, which is `.product-body`'s padding
   edge — the very edge `.product-name` starts at. The alignment is therefore
   inherited rather than restated: nothing here names a pixel value, so the day
   `.product-body`'s padding changes, the triangle follows it instead of
   silently drifting away from the name. That mattered here because the inset is
   a literal in `.product-body` and there is no spacing token to point both at. */
.product-variants {
    width: 100%;
    margin-top: 2px;
    font-size: var(--text-small);
}

.product-variants-summary {
    cursor: pointer;
    width: fit-content;
    padding: 2px 0;
    font-weight: 600;
    color: var(--color-anchor);
    list-style-position: inside;
}

.product-variants-summary:hover {
    color: var(--color-anchor-hover);
}

/* The focus ring is the summary's own, not the list item's: the <summary> is
   what takes keyboard focus, and without this the only visible focus state
   would be the row hover, which a keyboard user never triggers. */
.product-variants-summary:focus-visible {
    outline: 2px solid var(--color-anchor);
    outline-offset: 2px;
}

/* SWEB-343 — ONE GRID FOR THE WHOLE LIST, NOT ONE PER ROW.
   Before this card each `.product-variants-link` was its own two-column grid
   (a name track and a fixed Art.-Nr. track), sized independently per row —
   which is why no column edge lined up between rows and a long Art.-Nr. got
   squeezed into a few px of its own row's leftover space.

   SWEB-343 also takes the Art.-Nr. off this row entirely (it stays on the
   product page and on `/suche`), so there is only one thing left to place:
   the product's name. The grid moves here, onto the list, as a single
   `minmax(0, …)` track — `minmax(0, 1fr)` rather than `1fr` alone, so a long
   name WRAPS inside that track instead of pushing it wider than the card. */
.product-variants-list {
    display: grid;
    grid-template-columns: minmax(0, 1fr);
    margin: 6px 0 0;
    padding: 0;
    border-top: 1px solid var(--color-border-subtle);
    list-style: none;
}

/* SWEB-245 — WHAT VARIES INSIDE A FAMILY, LISTED ABOVE THE MEMBERS.
   Only drawn when TWO OR MORE axes vary. A family with one axis needs no
   summary of its axes: the member list already reads as that one axis, and a
   heading saying `Farbe:` over eleven colours is furniture with no function.

   These are not controls. A colour on its own does not identify an article in a
   family that also varies by size and material, so making them links would
   promise a filter this site cannot build without script. They say what the
   family spans; the member list below says which combinations exist. */
.product-axes {
    margin: 6px 0 0;
    padding: 0;
    list-style: none;
    font-size: var(--text-mini);
    color: var(--color-text-muted);
}

.product-axis {
    margin: 0 0 2px;
    overflow-wrap: anywhere;
}

.product-axis-label {
    font-weight: 600;
    color: var(--color-anchor);
}

/* The same idea on the product page: the axis names, where more than one
   varies, under the heading that would otherwise have named a single axis. */
.pd-sizes-axes {
    margin: 0 0 7px;
    font-size: var(--text-small);
    color: var(--color-text-muted);
}

/* A grid item of `.product-variants-list` above, one per row; the border
   gives each row its own line without needing a track of its own. */
.product-variants-item {
    min-width: 0;
    border-bottom: 1px solid var(--color-border-subtle);
}

.product-variants-link {
    display: block;
    padding: 5px 4px;
    color: var(--color-text);
    text-decoration: none;
}

.product-variants-link:hover,
.product-variants-link:focus-visible {
    background: var(--color-accent-surface);
    color: var(--color-accent-active);
}

.product-variants-size {
    display: block;
    font-weight: 600;
    color: var(--color-anchor);
    overflow-wrap: anywhere;
}

.product-variants-link:hover .product-variants-size,
.product-variants-link:focus-visible .product-variants-size {
    color: var(--color-accent-active);
}

/* A quiet note under a section heading — smaller and greyer than body copy,
   spaced to sit with the heading above it rather than with the content below.

   SWEB-237 — this was `.listing-switch`, the line offering the per-article view
   beside the grouped one. That view is gone and the line with it, so rather than
   leave an orphaned rule and add a near-identical one for `/suche`'s note about
   article numbers, the rule is renamed to what it always actually was: one
   sentence of context under a heading. Same declarations, same tokens. */
.section-note {
    margin: 0 0 18px;
    font-size: var(--text-small);
    color: var(--color-text-muted);
}

.product-image {
    margin: auto 0 0;
    padding-top: 4px;
    font-size: var(--text-mini);
    color: var(--color-text-subtle);
    overflow-wrap: anywhere;
}

.product-image--placeholder {
    font-style: italic;
}


/* --- Produktdetailseite (SWEB-40) --- */

/* ===========================================================================
   ---------------------------------------------------------------------------
   Appended, never interleaved: four other slices are being written against this
   file at the same time, and an edit inside an existing block is a conflict git
   cannot see. Nothing above this line was changed except the three selectors
   this slice was assigned.

   Ported from /entwurf-produkt, whose commerce is deliberately absent — no cart
   button, no compare, no wishlist. Not one colour literal: every value is a
   token, which is the only reason a CI change reaches this page at all.
   =========================================================================== */

/* --- Breadcrumb: MOVED (SWEB-133) ---------------------------------------
   `.crumbbar`/`.crumb`/`.crumb-sep`/`.crumb-current` used to live here — this
   page's own copy of what is now the ONE shared breadcrumb, styled once near
   the top of this file (".breadcrumb…") and rendered by layout.php for every
   page. Nothing page-specific is lost: the product page's trail is still
   decided in ProduktPage.php, it just no longer carries its own skin. */

/* --- The two-column body ------------------------------------------------ */

.pd {
    padding: 32px 0 40px;
}

/* SWEB-153 rework: the two-column pair moved one level deeper, into its own
   `.pd-grid`, so `.pd .wrap` could go back to holding exactly the two things
   a page's own wrap always holds here — the filter row (partials/category-
   filter.php), then this grid — with no third grid item to auto-place. */
.pd-grid {
    display: grid;
    grid-template-columns: minmax(0, 5fr) minmax(0, 7fr);
    gap: 32px;
    align-items: start;
}

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

/* --- The image slot, still a placeholder -------------------------------- */

/* SWEB-259 — this slot was already square; `.product-thumb` and
   `.category-thumb` were brought to the same 1:1 ratio to match it, rather
   than the other way round. */
.pd-media {
    position: relative;
    display: flex;
    align-items: center;
    justify-content: center;
    aspect-ratio: 1 / 1;
    background: var(--color-surface);
    border: 1px solid var(--color-border);
    border-radius: 8px;
}

.pd-tag {
    position: absolute;
    top: 12px;
    left: 12px;
    padding: 3px 9px;
    background: var(--color-surface);
    border: 1px solid var(--color-border);
    border-radius: 3px;
    font-size: var(--text-mini);
    letter-spacing: 0.02em;
    color: var(--color-text-muted);
}

.pd-imagename {
    position: absolute;
    bottom: 12px;
    left: 12px;
    margin: 0;
    font-size: var(--text-mini);
    color: var(--color-text-subtle);
}

.pd-figure {
    width: 62%;
    height: 62%;
}

/* SWEB-203 — the real photograph on the detail page, same slot, same reasoning
   as `.product-thumb-image`: `max-*` so a small file is not upscaled into a
   blurry one, `contain` so a wide one is never cropped — a laboratory article
   cut off at the edge is a different product to a reader. `.pd-media` centres
   it and keeps its 1:1 box, so the two-column grid does not move between a
   product with a photograph and one without. */
.pd-image {
    max-width: 92%;
    max-height: 92%;
    object-fit: contain;
}

/* --- The information column --------------------------------------------- */

.pd-brand {
    margin: 0 0 6px;
    font-size: var(--text-small);
    letter-spacing: 0.08em;
    text-transform: uppercase;
    color: var(--color-text-subtle);
}

.pd-info h1 {
    margin: 0 0 10px;
    font-size: var(--text-title);
    line-height: 1.22;
    color: var(--color-anchor);
    overflow-wrap: break-word;
}

.pd-sku {
    display: inline-block;
    margin: 0 0 16px;
    padding: 3px 10px;
    background: var(--color-accent-badge);
    border: 1px solid var(--color-accent-badge-border);
    border-radius: 3px;
    font-size: var(--text-small);
    font-weight: 600;
    color: var(--color-anchor);
}

.pd-lead {
    margin: 0 0 18px;
    font-size: var(--text-body);
    line-height: 1.55;
    color: var(--color-text);
}

/* --- The telephone-order notice, which stays ---------------------------- */

.pd-notice {
    display: flex;
    gap: 12px;
    margin: 0 0 20px;
    padding: 14px 16px;
    background: var(--color-accent-surface);
    border: 1px solid var(--color-accent-divider);
    border-radius: 6px;
}

.pd-notice-icon {
    flex: 0 0 auto;
    width: 20px;
    height: 20px;
    margin-top: 2px;
}

.pd-notice-text {
    margin: 0;
    font-size: var(--text-small);
    line-height: 1.5;
    color: var(--color-text);
}

/* Usage rule: step 700 on the accent surface (600 measures 3.25 : 1). */
.pd-notice-text a {
    color: var(--color-accent-hover);
}

/* --- Packaging unit and the specification list -------------------------- */

/* SWEB-264 — `.pd-ve` / `.pd-ve-label` are gone. Verpackungseinheit is now the
   first row of `.pd-specs` below, on the same `dt`/`dd` rule as every other
   detail row, rather than a paragraph styled apart from it. Grep confirms
   neither class is used anywhere any more — `.pd-ve` was still NAMED in two
   other comments (the `.product-ve` rule above, and `.pd-desc` below), both
   corrected in this same change; `.product-ve` itself is a different,
   unrelated rule for the category card and is not touched. */

/* SWEB-233 — THE PACK-SIZE SELECTOR ON A PRODUCT PAGE.
   Only a product page with siblings has this markup; every other page has
   nothing for these rules to reach.

   IT IS A LIST OF LINKS, NOT A <select>. A <select> cannot change a page
   without JavaScript, and this site carries none of its own. Chips rather than
   a stacked list because the values are short — "250 Milliliter" — and a
   visitor is comparing them against each other, which is a scan across, not a
   read down.

   THE CURRENT ONE IS A <span> and is filled rather than outlined: it must be
   distinguishable from its neighbours without relying on hue alone, so it
   differs in fill, in border and in weight at once. It is also the only entry
   with no hover and no pointer, because it does nothing. */
.pd-sizes {
    margin: 0 0 18px;
}

.pd-sizes-label {
    margin: 0 0 7px;
    font-size: var(--text-small);
    font-weight: 600;
    color: var(--color-anchor);
}

.pd-sizes-list {
    display: flex;
    flex-wrap: wrap;
    gap: 7px;
    margin: 0;
    padding: 0;
    list-style: none;
}

.pd-sizes-item {
    margin: 0;
}

.pd-size {
    display: inline-block;
    padding: 5px 11px;
    border: 1px solid var(--color-accent-badge-border);
    border-radius: 3px;
    background: var(--color-surface);
    font-size: var(--text-small);
    color: var(--color-text);
    text-decoration: none;
    white-space: nowrap;
}

a.pd-size:hover,
a.pd-size:focus-visible {
    border-color: var(--color-accent);
    background: var(--color-accent-surface);
    color: var(--color-accent-active);
}

.pd-size--current {
    border-color: var(--color-accent);
    background: var(--color-accent-badge);
    font-weight: 600;
    color: var(--color-accent-active);
    cursor: default;
}

.pd-specs {
    display: grid;
    grid-template-columns: minmax(0, 2fr) minmax(0, 3fr);
    gap: 0;
    margin: 0;
    border-top: 1px solid var(--color-border);
}

.pd-specs dt,
.pd-specs dd {
    margin: 0;
    padding: 9px 4px;
    border-bottom: 1px solid var(--color-border);
    font-size: var(--text-small);
    line-height: 1.45;
    overflow-wrap: break-word;
}

.pd-specs dt {
    font-weight: 600;
    color: var(--color-text-muted);
}

.pd-specs dd {
    color: var(--color-text);
}

/* --- The long description ----------------------------------------------- */

/* SWEB-154 rework: top padding added so Beschreibung reads as its own
   section instead of sitting butted against Detailinformationen above it.
   44px, matching this same rule's existing bottom padding — the section
   already had a bottom gap of that size, so the top gap now mirrors it
   rather than borrowing the smaller row padding `.pd-specs dt`/`dd` (SWEB-264
   folded Verpackungseinheit into, where `.pd-ve` used to sit) use further up
   the column. */
.pd-desc {
    padding: 44px 0 44px;
}

.pd-desc h2 {
    margin: 0 0 10px;
    font-size: var(--text-lead);
    color: var(--color-anchor);
}

.pd-desc p {
    max-width: 68ch;
    margin: 0;
    font-size: var(--text-body);
    line-height: 1.6;
    color: var(--color-text);
}

/* --- Inhaltsseite (SWEB-50) --- */

/* ONE section, appended at the end of the file, every selector prefixed
   `inhalt-`. Five slices are appending here on the same afternoon: an existing
   rule changed in place is a conflict git does not show anybody, and an
   unprefixed `.breadcrumb` is the same accident one level up.

   THERE WAS NO MOCKUP FOR THIS PAGE and none was borrowed. The three
   that exist are Webshop drafts from before the no-cart decision. What is below
   is the design language SWEB-38 already established — the .pagehead band, the
   measure, the type scale, the accent usage — applied to a page of text.

   Colour usage follows the site's contrast usage rule, and it is a rule
   rather than a taste: running text and links take accent step 700 or
   darker. Step 600 is the anchor and clears 4.5:1 against nothing, so it
   stays on surfaces, borders and illustration strokes, all of which it does
   below. */

/* --- The breadcrumb: MOVED (SWEB-133) ------------------------------------
   `.inhalt-breadcrumb`/`.inhalt-breadcrumb-trenner`/`.inhalt-breadcrumb-hier`
   used to live here. This page's trail is now supplied as data (InhaltPage,
   AktuellesItemPage) and rendered once, site-wide, by layout.php — see the
   shared `.breadcrumb…` rules near the top of this file. */

/* --- The lead ------------------------------------------------------------ */

.inhalt-lead {
    margin: 16px 0 0;
    font-size: var(--text-lead);
    line-height: 1.55;
    color: var(--color-text-muted);
    overflow-wrap: break-word;
}

/* --- The placeholder notice ---------------------------------------------- */

/* It is meant to be seen. The accent bar on the left is the loudest thing on
   the page on purpose: nobody should be able to read this page and come away
   thinking the words are final. */
.inhalt-hinweis {
    margin: 0 0 28px;
    padding: 12px 16px;
    background: var(--color-accent-surface);
    border: 1px solid var(--color-accent-badge-border);
    border-left: 4px solid var(--color-accent);
    border-radius: 5px;
    font-size: var(--text-small);
    color: var(--color-accent-active);
}

/* --- The running text ---------------------------------------------------- */

/* SWEB-154: "wherever is a carriage return we should add a bit more space in
   between the lines" (Tom, live review). Each of the description's own
   blocks — a lead line, `Wesentliche Bestandteile:`, each `•` bullet — is its
   OWN <p>, so more room between paragraphs is what the ask actually needs,
   not a line-height change inside one. Raised ONCE, here, in the shared
   text style every one of the three surfaces this card names already
   renders through: the product description and an Aktuelles article both
   nest their body markup in THIS SAME `.inhalt-text` (produkt.php reuses it
   verbatim, and an Aktuelles item renders through `pages/inhalt.php`,
   SWEB-66/SWEB-68), so one rule reaching all three is a fact about the
   existing renderer, not a new seam. Font size, colour, measure and every
   other margin below are untouched (AC4). */
.inhalt-text p {
    margin: 0 0 1.5em;
    font-size: var(--text-body);
    line-height: 1.65;
    color: var(--color-text);
    overflow-wrap: break-word;
}

.inhalt-text p:last-child {
    margin-bottom: 0;
}

.inhalt-text a {
    color: var(--color-accent-hover);
    text-decoration: underline;
    text-underline-offset: 2px;
}

.inhalt-text a:hover {
    color: var(--color-accent-active);
}

/* --- The image area ------------------------------------------------------ */

.inhalt-bild {
    margin: 32px 0 0;
}

.inhalt-bild-flaeche {
    position: relative;
    display: flex;
    align-items: center;
    justify-content: center;
    aspect-ratio: 16 / 9;
    background: var(--color-surface-raised);
    border: 1px solid var(--color-border);
    border-radius: 6px;
}

.inhalt-bild-marke {
    position: absolute;
    top: 10px;
    left: 10px;
    padding: 2px 8px;
    background: var(--color-surface);
    border: 1px solid var(--color-border);
    border-radius: 3px;
    font-size: var(--text-mini);
    letter-spacing: 0.05em;
    text-transform: uppercase;
    color: var(--color-text-muted);
}

.inhalt-bild-figur {
    width: 46%;
    height: 62%;
}

.inhalt-bild-text {
    margin: 9px 0 0;
    font-size: var(--text-small);
    color: var(--color-text-muted);
    overflow-wrap: anywhere;
}

/* --- The contact block is gone (SWEB-300) ---------------------------------

   `.inhalt-kontakt`, `.inhalt-kontakt-titel` and `.inhalt-kontakt-text` dressed
   an `<aside>` headed „Ansprechpartner", printed from one `varchar` on the
   article. It was NULL on every row in every environment, so nothing on any
   page changes shape; it went with the people for the same reason. */

/* --- Kontakt und Formular (SWEB) --- */

/* Appended as one block at the end of the file, and it CHANGES NOTHING ABOVE
   this line: five slices are being skinned against this stylesheet at the same
   time, and an edited selector is the kind of conflict git does not report.

   There is no mockup for the contact page, so nothing here is ported. The
   vocabulary is the one SWEB-38 already established for the category page: the
   same page head, the same small accent marker before a section label, the same
   surface and badge tints, the same border radii. Not a hex value and not a new
   token — every colour resolves from tokens.css.

   CONTRAST IS A USAGE RULE AND IT DECIDED TWO THINGS HERE:

     - links inside this page are set to step 700. The site-wide `a` rule near
       the top of this file uses step 600, which measures ~3.6:1 on white and
       does not clear 4.5:1 for normal-size text. That rule is not mine to
       change in this slice — it is an existing rule and the fence is
       append-only — so the page scopes its own links instead, and the site-wide
       one is reported as a finding;
     - the submit button takes step 700 as its fill rather than the anchor 600.
       White on 600 measures ~3.55:1 and dark text on 600 is worse, so a 600
       button cannot carry an accessible label in EITHER direction; white on 700
       measures ~5.3:1. */

.kontakt-seite a {
    color: var(--color-accent-hover);
    text-decoration: underline;
}

.kontakt-seite a:hover,
.kontakt-seite a:focus-visible {
    color: var(--color-accent-active);
}

.kontakt-seite :focus-visible {
    outline: 2px solid var(--color-accent-hover);
    outline-offset: 2px;
}

/* `.kontakt-breadcrumb` — MOVED (SWEB-133). It was a THIRD page-local
   breadcrumb implementation, hard-coded twice (this page and its
   confirmation), under its own aria-label ("Brotkrumennavigation" rather than
   "Sie sind hier") — found only by reading the templates, not by reading the
   story's own table. Both pages' trails are now supplied as data
   (KontaktPage, KontaktSendenPage, KontaktBestaetigungPage) and rendered
   once, site-wide, by layout.php. */

/* The section label of the approved design language: the small accent square,
   uppercase, wide tracking. This is where the anchor accent (step 600) is used
   on this page — as a surface, never as text. */
.kontakt-abschnitt {
    display: flex;
    align-items: center;
    gap: 8px;
    margin: 0 0 14px;
    font-size: var(--text-small);
    font-weight: 700;
    letter-spacing: 0.1em;
    text-transform: uppercase;
    color: var(--color-anchor);
}

.kontakt-abschnitt::before {
    content: "";
    width: 8px;
    height: 8px;
    flex: 0 0 auto;
    border-radius: 2px;
    background: var(--color-accent);
}

/* Every placeholder text on these two pages wears this. It is meant to be
   visible — a placeholder that looks like finished copy is a placeholder that
   ships. */
.platzhalter {
    margin: 0 0 24px;
    padding: 12px 14px;
    background: var(--color-accent-surface);
    border: 1px solid var(--color-accent-badge-border);
    border-radius: 5px;
    color: var(--color-accent-active);
    font-size: var(--text-small);
}

.platzhalter--warnung {
    border-left: 4px solid var(--color-accent);
}

.kontakt-grid {
    display: grid;
    grid-template-columns: minmax(0, 1fr) minmax(0, 1.1fr);
    gap: 32px;
    align-items: start;
}

@media (max-width: 760px) {
    .kontakt-grid {
        grid-template-columns: minmax(0, 1fr);
        gap: 28px;
    }
}

.kontakt-daten {
    margin: 0 0 28px;
    padding: 16px 18px;
    background: var(--color-surface-raised);
    border: 1px solid var(--color-border);
    border-radius: 6px;
}

.kontakt-daten dt {
    margin: 0;
    font-size: var(--text-mini);
    font-weight: 700;
    letter-spacing: 0.06em;
    text-transform: uppercase;
    color: var(--color-text-muted);
}

.kontakt-daten dd {
    margin: 2px 0 14px;
    color: var(--color-text);
    overflow-wrap: anywhere;
}

.kontakt-daten dd:last-child {
    margin-bottom: 0;
}

.kontakt-eingabe {
    font-weight: 700;
}

/* --- The form --------------------------------------------------------- */

.kontakt-form {
    display: flex;
    flex-direction: column;
    gap: 16px;
    padding: 20px 20px 24px;
    background: var(--color-surface);
    border: 1px solid var(--color-border);
    border-top: 3px solid var(--color-accent);
    border-radius: 6px;
}

.kontakt-form-hinweis {
    margin: 0;
    font-size: var(--text-small);
    color: var(--color-text-muted);
}

.kontakt-feld {
    display: flex;
    flex-direction: column;
    gap: 6px;
}

.kontakt-feld label {
    font-size: var(--text-small);
    font-weight: 700;
    color: var(--color-text);
}

.pflicht {
    color: var(--color-accent-hover);
}

.kontakt-feld input:not([type="radio"]),
.kontakt-feld textarea {
    width: 100%;
    max-width: 100%;
    padding: 9px 11px;
    font: inherit;
    font-size: var(--text-body);
    color: var(--color-text);
    background: var(--color-surface);
    border: 1px solid var(--color-border);
    border-radius: 5px;
}

/* The salutation is a radio group rather than a dropdown — see the note in
   the template. A fieldset carries the group's name for a screen reader; the
   browser's own border and padding come off because the visual grouping is
   done by the spacing of the form. */
.kontakt-gruppe {
    margin: 0;
    padding: 0;
    border: 0;
}

.kontakt-gruppe legend {
    padding: 0;
    font-size: var(--text-small);
    font-weight: 700;
    color: var(--color-text);
}

.kontakt-auswahl {
    display: flex;
    flex-wrap: wrap;
    gap: 6px 18px;
}

.kontakt-option {
    display: inline-flex;
    align-items: center;
    gap: 6px;
    font-size: var(--text-body);
    color: var(--color-text);
}

/* An option is not a field label. Without this it inherits the bold weight of
   the rule above and four options shout as loudly as the group's own label. */
.kontakt-option label {
    font-weight: 400;
}

.kontakt-option input {
    accent-color: var(--color-accent-hover);
}

.kontakt-feld textarea {
    resize: vertical;
    min-height: 8rem;
}

.kontakt-feld input:not([type="radio"]):focus,
.kontakt-feld textarea:focus {
    border-color: var(--color-accent-hover);
}

.kontakt-submit {
    align-self: flex-start;
    padding: 11px 22px;
    font: inherit;
    font-size: var(--text-body);
    font-weight: 700;
    color: var(--color-surface);
    background: var(--color-accent-hover);
    border: 1px solid var(--color-accent-hover);
    border-radius: 5px;
    cursor: pointer;
}

.kontakt-submit:hover {
    background: var(--color-accent-active);
    border-color: var(--color-accent-active);
}

.kontakt-hinweis {
    margin: 0 0 12px;
    color: var(--color-text-muted);
    font-size: var(--text-small);
}

.kontakt-rechtliches {
    margin: 32px 0 0;
    padding-top: 16px;
    border-top: 1px solid var(--color-border);
    color: var(--color-text-muted);
    font-size: var(--text-small);
}

/* SWEB-91: validation and send-failure messages. --color-alert is a
   FUNCTIONAL colour (tokens.css: "status, never decoration") — the first use
   of it in this stylesheet, for exactly the case it exists for. */
.kontakt-fehler {
    margin: 6px 0 0;
    font-size: var(--text-small);
    font-weight: 600;
    color: var(--color-alert);
}

.kontakt-fehler--allgemein {
    margin: 0 0 20px;
    padding: 14px 16px;
    background: var(--color-surface-raised);
    border: 1px solid var(--color-alert);
    border-radius: 5px;
    font-size: var(--text-body);
}

/* --- Kategorieübersicht (SWEB-22) --- */

/* Appended as one block at the end of the file, and nothing above this line is
   touched. Four other slices are editing this stylesheet at the same time; a
   changed selector higher up is a conflict git does not report.

   COLOUR STEPS ARE CHOSEN, NOT PICKED BY EYE. The ramp publishes a purpose per
   step and the contrast gate computes the ratios, so normal-size text takes
   step 700 or darker and the lighter steps stay on borders, fills and
   illustrations. The approved mockup puts step 600 on the small "ansehen" line;
   that measures about 3,6:1 on white and does not pass, so the same element
   here takes 700. That is the palette working as designed rather than a
   deviation from the draft. */

.section-title {
    margin: 0 0 22px;
    font-size: var(--text-heading-fluid);
    font-weight: 700;
    letter-spacing: -0.015em;
    color: var(--color-anchor);
}

.pagehead-lead {
    margin: 12px 0 0;
    max-width: 60ch;
    font-size: var(--text-body);
    color: var(--color-text-muted);
}

/* --- The category tiles ------------------------------------------------- */

/* THREE ACROSS, NOT FOUR — Tom's ruling 29.08, the same call he made on the
   category listing.

   At four the tile was too narrow for the content it carries: „Abstrichsysteme
   / Watteträger" broke onto a second line against the affordance arrow, and the
   subcategory list inside the tile wrapped mid-phrase. This tile is not a
   product card — it holds a name, a count AND a list of child categories — so
   it needs more width per tile than a card does, at the same wrap.

   `.category-list` is rendered by `pages/kategorien.php` and by nothing else.
   It used to be read alongside the homepage's own tile grid `.start-einstiege`,
   which was also three across; SWEB-293 removed that grid, so this is the only
   three-column tile component left and the paragraph above is about it alone.

   THE 900 px STEP IS GONE BECAUSE IT BECAME A NO-OP. It existed to step four
   down to three; the base is three now, so it set the value it already had.
   Nothing about the rendered result changes — 900 px still yields three — and
   the ladder below it (two at 640, one at 480) is untouched. */
.category-list {
    display: grid;
    grid-template-columns: repeat(3, 1fr);
    gap: 16px;
    margin: 0;
    padding: 0;
    list-style: none;
}

@media (max-width: 640px) {
    .category-list {
        grid-template-columns: repeat(2, 1fr);
    }
}

/* One column from 480 px down, and not from 380 px as the mockup has it.
   The mockup's breakpoint leaves a 390 px phone — the width the acceptance
   criterion names, and the commonest one there is — with two columns about
   110 px wide inside their padding. German category names do not fit that:
   "Einbettkassetten" on its own is wider, so every name broke mid-word and the
   affordance arrow was pushed onto a line of its own. Only the width at which
   the grid stops being two-up changed; the tile itself is the mockup's. */
@media (max-width: 480px) {
    .category-list {
        grid-template-columns: 1fr;
    }
}

.category-list-item {
    display: flex;
    flex-direction: column;
    overflow: hidden;
    background: var(--color-surface);
    border: 1px solid var(--color-border);
    border-radius: 6px;
    box-shadow: 0 1px 4px rgba(0, 0, 0, 0.05);
    transition: border-color 0.15s, box-shadow 0.15s;
}

/* Highlight on hover, and no lift: the card is not itself a link — a tile with
   subcategories carries several — so it must not behave like one. */
.category-list-item:hover {
    border-color: var(--color-accent);
    box-shadow: 0 4px 14px rgba(0, 0, 0, 0.08);
}

/* SWEB-327 (AC 3) — `.category-thumb`, `.category-thumb-tag` and
   `.category-thumb-figure` removed with the placeholder slot they styled;
   `/kategorien` no longer renders a thumbnail box at all. */
.category-body {
    display: flex;
    flex: 1;
    flex-direction: column;
    align-items: flex-start;
    gap: 8px;
    padding: 14px 15px 16px;
}

.category-name {
    margin: 0;
    font-size: var(--text-body);
    font-weight: 700;
    line-height: 1.25;
}

.category-link {
    color: var(--color-anchor);
    overflow-wrap: anywhere;
}

.category-link:hover {
    color: var(--color-accent-hover);
}

/* The arrow is the mockup's affordance and it is decoration, so it is generated
   here rather than typed into the markup — screen readers announce the link
   text, not the glyph. The space in front of it is non-breaking: at 390 px a
   normal space let the arrow wrap onto a line of its own under a long category
   name, which reads as a stray character rather than as an affordance. */
.category-link::after {
    content: "\00a0\2192";
}

.category-children {
    margin: 2px 0 0;
    padding: 0;
    list-style: none;
    display: flex;
    flex-direction: column;
    gap: 3px;
}

.category-children a {
    font-size: var(--text-small);
    color: var(--color-text-muted);
}

.category-children a:hover {
    color: var(--color-accent-hover);
}

/* --- The complete band -------------------------------------------------- */

.allcategories {
    background: var(--color-surface-tint);
    border-top: 1px solid var(--color-border);
}

.allcategories-list {
    display: grid;
    grid-template-columns: repeat(3, 1fr);
    gap: 10px 18px;
    margin: 0;
    padding: 0;
    list-style: none;
}

@media (max-width: 720px) {
    .allcategories-list {
        grid-template-columns: repeat(2, 1fr);
    }
}

@media (max-width: 420px) {
    .allcategories-list {
        grid-template-columns: 1fr;
    }
}

/* THE CHILD COMBINATOR IS LOAD-BEARING, not tidiness. `.allcategories-item a`
   is a DESCENDANT selector, and an opened entry's `<li>` carries this same
   class — so every sub-category link and the „Alle Produkte in …" link inside
   it were wearing the closed row's whole box: the surface, the border, the
   14px padding and the bullet square. The open band came out as a grid of
   little cards, and the name-rule-count row two blocks below this one, which
   the sub-list is built to be, never appeared. Only the entry's OWN link is
   the row; `>` says so. */
.allcategories-item > a {
    display: flex;
    align-items: center;
    gap: 10px;
    padding: 12px 14px;
    background: var(--color-surface);
    border: 1px solid var(--color-border);
    border-radius: 5px;
    font-size: var(--text-small);
    font-weight: 500;
    color: var(--color-text);
    transition: border-color 0.12s, box-shadow 0.12s;
}

.allcategories-item > a:hover {
    border-color: var(--color-accent);
    color: var(--color-anchor);
    box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
}

.allcategories-item > a::before {
    content: "";
    width: 8px;
    height: 8px;
    flex: 0 0 auto;
    border-radius: 2px;
    background: var(--color-accent);
}

/* AN ENTRY THAT GOES SOMEWHERE SAYS SO, AND SAYS IT DIFFERENTLY FROM ONE THAT
   OPENS. The two kinds of row were indistinguishable — eighteen bullets and
   eighteen names — so the arrow here and the chevron below are a pair: an
   arrow leaves the page, a chevron opens in place. The arrow is the same glyph
   and the same reasoning as `.category-link::after` further up this file.

   No non-breaking space is needed here, unlike there: `::after` inside a flex
   row is a flex ITEM, so it cannot wrap onto a line of its own, and
   `margin-left: auto` puts it hard right whatever the name's length. */
.allcategories-item > a::after {
    content: "\2192";
    flex: 0 0 auto;
    margin-left: auto;
    color: var(--color-accent-hover);
}

/* --- An entry that opens (SWEB-293) --------------------------------------

   `<details>` / `<summary>`. The open/closed state is the element's own, so
   there is no script behind any of this and nothing here has to be kept in
   step with an `aria-expanded` we set ourselves.

   An OPEN entry leaves the grid and takes the full width. A three-column grid
   whose middle cell suddenly grows to twelve sub-categories pushes its two
   neighbours' boxes down with it and leaves a ragged hole; `grid-column: 1/-1`
   makes the opened entry a row of its own, which is the shape the content
   actually is. `:has()` does the same job with no class to toggle and no
   script — where a browser does not support it the entry simply grows inside
   its own cell, which is untidy and not broken. */
.allcategories-item--auf:has(> details[open]) {
    grid-column: 1 / -1;
}

/* The summary wears the closed entry's own box, so an entry that opens and one
   that does not are the same object in the same row until one is opened. The
   marker square is repeated here rather than inherited, because
   `.allcategories-item > a` is an anchor selector and a summary is not an
   anchor.

   `display: flex` HERE IS WHAT DELETED THE DISCLOSURE TRIANGLE — see the
   chevron block below. */
.allcategories-summary {
    display: flex;
    align-items: center;
    gap: 10px;
    padding: 12px 14px;
    background: var(--color-surface);
    border: 1px solid var(--color-border);
    border-radius: 5px;
    font-size: var(--text-small);
    font-weight: 500;
    color: var(--color-text);
    cursor: pointer;
    transition: border-color 0.12s, box-shadow 0.12s;
}

.allcategories-summary:hover,
.allcategories-details[open] > .allcategories-summary {
    border-color: var(--color-accent);
    color: var(--color-anchor);
}

.allcategories-summary::before {
    content: "";
    width: 8px;
    height: 8px;
    flex: 0 0 auto;
    border-radius: 2px;
    background: var(--color-accent);
}

/* --- THE CHEVRON, AND WHY IT IS OURS AFTER ALL --------------------------

   This block used to be two `::marker` selectors setting a colour, on the
   stated reasoning that the browser's own triangle was being kept and merely
   recoloured. IT NEVER RENDERED. A `<summary>` is a `display: list-item` by
   default and that is the only thing that gives it a marker box; the rule
   above sets `display: flex`, which takes the marker away entirely. So all
   eighteen closed rows came out identical — a bullet and a name — with
   nothing saying that eleven of them open. Measured on the served start page
   at 1280px, 2026-09-02.

   The two ways out were to give the summary its marker back
   (`display: list-item` plus a nested flex wrapper, which needs a new element
   in the markup) or to draw the chevron here. Drawing it costs four
   declarations and no markup, and this file already draws the bullet square
   above and the arrow glyph in `.category-link::after`, so a shape in CSS is
   the house convention rather than an exception to it. The COLOUR is a token,
   which is the rule the earlier note was actually protecting.

   `content: ""` and not a triangle character: an empty pseudo-element is
   announced by nothing. `<details>` already tells a screen reader that the
   row is collapsed or expanded, so a glyph here would be a second, worse copy
   of that — which is also why the element itself is untouched and there is
   still no script behind any of this.

   A 7px box with two borders, rotated: -45deg points it right (closed),
   +45deg points it down (open). The chevron's visual mass sits off its own
   box centre, so each state carries the 1-2px nudge that puts the apex on the
   row's optical middle. */
.allcategories-summary::after {
    content: "";
    flex: 0 0 auto;
    margin-left: auto;
    width: 7px;
    height: 7px;
    border-right: 2px solid var(--color-accent-hover);
    border-bottom: 2px solid var(--color-accent-hover);
    transform: translateY(-1px) rotate(-45deg);
    transition: transform 0.12s;
}

.allcategories-details[open] > .allcategories-summary::after {
    transform: translateY(-2px) rotate(45deg);
}

.allcategories-inhalt {
    padding: 12px 14px 4px;
}

.allcategories-alle {
    margin: 0 0 10px;
    font-size: var(--text-small);
    font-weight: 600;
}

.allcategories-alle a {
    color: var(--color-accent-hover);
}

.allcategories-unter {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(15rem, 1fr));
    gap: 2px 18px;
    margin: 0;
    padding: 0;
    list-style: none;
}

/* Name on the left, count hard right, with a rule between them so the eye can
   travel from one to the other across a wide row. Two children of a flex row
   and a border on the item, which is one element rather than a leader made of
   pseudo-elements. */
.allcategories-unter-item {
    display: flex;
    align-items: baseline;
    justify-content: space-between;
    gap: 12px;
    padding: 7px 0;
    border-bottom: 1px solid var(--color-border-subtle);
    font-size: var(--text-small);
}

.allcategories-unter-item a {
    min-width: 0;
    color: var(--color-anchor);
}

/* The search box sits directly above the band on the start page and needs the
   air the catalogue pages get from the heading above it. */
.allcategories .search-box {
    margin-bottom: 24px;
}

/* --- Startseite (SWEB-44) --- */

/* THE BANNER ABOVE IS ONE LINE ON PURPOSE. It is the shape slice_section_end()
   in tests/harness.php matches, and this section's banner was a multi-line
   block first — which that helper does not recognise. A slice bounding its own
   section by "the next banner" would have run straight through this one and
   started measuring rules it does not own. Nothing broke today only because the
   section immediately above happens not to use the helper; that is luck, and
   the next slice appended after this one would have inherited the trap. The
   prose lives below the banner instead of inside it.

   APPENDED, AND NOTHING ABOVE THE BANNER WAS TOUCHED. Five page slices were
   built at once; a changed selector further up is a conflict git does not
   report, so this slice only adds.

   Every class here is prefixed `start-` for the same reason: the approved draft
   names its blocks `hero`, `about`, `contact` and `cat-card`, and five agents
   porting five drafts into one stylesheet with those names would collide in
   silence — two rules, one selector, last one wins, nothing to merge.

   CONTRAST IS DECIDED PER RULE HERE, NOT BY TASTE. The ratios on white,
   named by ramp step rather than by value — this file may not contain a hex
   literal, and a comment quoting one makes the stylesheet report itself to G1
   and then survive the G2 sentinel swap as a hardcoded brand colour. That is
   not hypothetical: it happened while this section was being written, and
   gates went red on the comment above, which is precisely what they are for.

     step 600  3.55 : 1   NOT a text colour — surfaces, borders, illustrations,
                          and LARGE display type only
     step 700  5.29 : 1   the smallest step that carries normal text
     step 800  7.99 : 1   text on an accent tint
     the anchor blue  8.10 : 1   headings

   So the small uppercase labels and the "more" links take 700, not the 600 the
   draft uses, and the primary button is a 700 FILL rather than a 600 one:
   white on 600 is 3.55 : 1, which fails in the other direction. The draft's
   button is a 600 fill with a white label, and that is the defect. The gate in
   tests/home_page_test.php computes each of these ratios itself rather than
   trusting this comment.

   No hex, no colour name, no inline style: everything resolves from tokens.css,
   which is what lets the sentinel swap prove the page carries no hardcoded
   brand pixel.
   -------------------------------------------------------------------------- */

/* --- The trust line ----------------------------------------------------- */

.start-vertrauen {
    background: var(--color-surface-muted);
    border-bottom: 1px solid var(--color-border);
}

.start-vertrauen .wrap {
    padding-top: 10px;
    padding-bottom: 10px;
}

.start-vertrauen-liste {
    display: flex;
    flex-wrap: wrap;
    justify-content: center;
    gap: 4px 0;
    margin: 0;
    padding: 0;
    list-style: none;
    font-size: var(--text-small);
    color: var(--color-text-muted);
}

.start-vertrauen-liste li {
    padding: 3px 20px;
}

.start-vertrauen-liste li + li {
    border-left: 1px solid var(--color-border);
}

@media (max-width: 640px) {
    .start-vertrauen-liste {
        flex-direction: column;
        align-items: center;
    }

    .start-vertrauen-liste li {
        padding: 2px 0;
    }

    .start-vertrauen-liste li + li {
        border-left: 0;
    }
}

/* --- The small uppercase label above a heading -------------------------- */

/* SWEB-293: `.start-kicker` was dropped from this selector and its own margin
   rule below removed with it. The tile kicker („Produktkategorie") was its only
   user and the tiles are gone. `.start-eyebrow` still has four. */
.start-eyebrow {
    margin: 0 0 8px;
    font-size: var(--text-mini);
    font-weight: 700;
    letter-spacing: 0.1em;
    text-transform: uppercase;
    color: var(--color-accent-hover);
}

/* --- Buttons ------------------------------------------------------------ */

.start-btn {
    display: inline-flex;
    align-items: center;
    gap: 8px;
    padding: 13px 26px;
    border: 1.5px solid transparent;
    border-radius: 4px;
    font-size: var(--text-body);
    font-weight: 600;
    line-height: 1;
    transition: background 0.14s, color 0.14s, border-color 0.14s;
}

/* SWEB-177: the homepage's main call to action wears the client's own blue,
   through --color-anchor. No new value enters the file — the token is the one
   already published for the anchor colour, and the hover state below stays in
   the same family rather than falling back to the accent ramp it left. */
.start-btn--primaer {
    background: var(--color-anchor);
    border-color: var(--color-anchor);
    color: var(--color-surface);
}

/* SWEB-183: the hover is now the site's one hover blue, the same value the
   navbar links, `Suchen`, `Zurücksetzen` and `Anfrage senden` use. It replaces
   the pale-wash inversion this state used to do — a hovered button on this
   site changes its blue, it does not change into a different kind of button. */
.start-btn--primaer:hover {
    background: var(--color-anchor-hover);
    border-color: var(--color-anchor-hover);
    color: var(--color-surface);
}

.start-btn--umriss {
    background: var(--color-surface);
    border-color: var(--color-anchor);
    color: var(--color-anchor);
}

/* The outlined twin fills on hover — same hover blue as its filled partner, so
   the pair hovers to one colour rather than to two shades of white. */
.start-btn--umriss:hover {
    background: var(--color-anchor-hover);
    border-color: var(--color-anchor-hover);
    color: var(--color-surface);
}

.start-cta-reihe {
    display: flex;
    flex-wrap: wrap;
    gap: 12px;
    margin: 32px 0 0;
}

@media (max-width: 640px) {
    .start-cta-reihe {
        flex-direction: column;
        align-items: stretch;
    }

    .start-cta-reihe .start-btn {
        justify-content: center;
    }
}

/* --- The hero ----------------------------------------------------------- */

/* SWEB-182: a real photograph behind the hero.

   THE PHOTO IS A STAND-IN. Süsse may still supply an abstract motif of their
   own, and Tom's ruling is that swapping it must be a FILE REPLACEMENT, not a
   code change. Two things make that true, and both are deliberate:

     - the path appears exactly once in this stylesheet, as
       `--start-hero-bild` on the rule below. Every other place that needs the
       picture — the narrow-viewport veil further down included — reads the
       variable.
     - the file is named for its ROLE, `hero-start.jpg`, not for the campaign
       it was borrowed from (`campaign-summer.jpg` on the Sommer page). The
       replacement picture is dropped in under that same name and no line here
       is edited.

   It is served from this repository, `public/assets/img/`. The path is
   relative to this stylesheet, so it resolves under any BASE_URL, and the page
   makes no request to a host that is not ours.

   READABILITY IS THE CAMPAIGNS PAGE'S TREATMENT, PORTED. A white gradient is
   painted left-to-right OVER the photo in the same background shorthand — the
   four stops are the ones the Sommer hero ships with. It is opaque where the
   eyebrow, headline, sub-line and buttons sit and clears towards the right,
   where the picture carries the band on its own. NO COLOUR OF THE TYPE OR OF
   THE BUTTONS CHANGES: the blues stay exactly what SWEB-177 and SWEB-183 set,
   and stay in tokens.css.

   The veil is the one place in this file that carries a literal colour, and it
   is not a brand one: it is plain white at four alpha values, a transparency
   ramp over a photograph. The token ramp holds opaque values only and cannot
   express "fades to nothing", which is the whole treatment. The tint underneath
   is a token, and is what the band falls back to if the file is ever missing.

   The copy shares the band with a second grid column that is now EMPTY, and
   that is deliberate — see the rule below. */
.start-hero {
    --start-hero-bild: url("../img/hero-start.jpg");

    background-color: var(--color-surface-tint);
    background-image:
        linear-gradient(
            to right,
            rgba(255, 255, 255, 1) 0%,
            rgba(255, 255, 255, 0.95) 40%,
            rgba(255, 255, 255, 0.55) 58%,
            rgba(255, 255, 255, 0) 85%
        ),
        var(--start-hero-bild);
    background-repeat: no-repeat;
    background-size: cover;
    background-position: center;
    border-bottom: 1px solid var(--color-border);
}

/* THE SECOND COLUMN IS EMPTY ON PURPOSE. SWEB-182 deleted the placeholder motif
   that used to sit in it and took this grid with it, which let `.start-hero-text`
   run the full width of the wrap — the copy read wider than it ever had, and that
   was never in the card's scope. The grid is back exactly as it was: the empty
   track is the space the PHOTOGRAPH shows through, which is what the column was
   always for. The copy keeps the geometry it had before the picture arrived. */
.start-hero .wrap {
    display: grid;
    grid-template-columns: minmax(0, 1.15fr) minmax(0, 0.85fr);
    gap: 40px;
    align-items: center;
    padding-top: 66px;
    padding-bottom: 72px;
}

/* No `max-width` here, on purpose — the headline had none before SWEB-182 and
   was bounded by its column, which is again the case. The 26ch cap the card
   added was only ever load-bearing while the copy ran the full wrap: measured
   against the restored column it never binds (26ch is 604px at the narrowest
   grid viewport and 842px at the widest, against a column that peaks at 570px),
   so it is a rule that cannot fire, and rules that cannot fire come out. The
   column's right edge lands near 54% of the band at every desktop width, where
   the veil still holds about 0.63 white and no line reaches the clear end. */
.start-hero h1 {
    margin: 0;
    font-size: var(--text-hero-fluid);
    font-weight: 700;
    letter-spacing: -0.025em;
    color: var(--color-anchor);
}

/* Step 600 on display type only. At 28px and up this is large text, where the
   threshold is 3 : 1 and 3.55 clears it — the same value would fail on the body
   copy two rules down. */
.start-hero-akzent {
    color: var(--color-accent);
}

.start-hero-sub {
    max-width: 52ch;
    margin: 20px 0 0;
    font-size: var(--text-body);
    line-height: 1.65;
    color: var(--color-text-muted);
}

.start-hero-note {
    margin: 16px 0 0;
    font-size: var(--text-small);
    color: var(--color-text-muted);
}

/* SWEB-182 took `.start-hero-motiv` off this rule and deleted its two
   companions — the hero's placeholder motif no longer exists. The "Über Süsse"
   band still carries one and is untouched. */
.start-ueber-motiv {
    position: relative;
    display: flex;
    align-items: center;
    justify-content: center;
    overflow: hidden;
    background: linear-gradient(160deg, var(--color-surface-tint) 0%, var(--color-accent-surface) 100%);
    border: 1px solid var(--color-border);
    border-radius: 8px;
    aspect-ratio: 4 / 3;
}

.start-motiv {
    display: block;
    width: 78%;
    height: 78%;
}

.start-motiv-cap {
    position: absolute;
    bottom: 10px;
    left: 12px;
    font-size: var(--text-mini);
    letter-spacing: 0.06em;
    text-transform: uppercase;
    color: var(--color-text-muted);
}

@media (max-width: 860px) {
    .start-hero .wrap {
        grid-template-columns: minmax(0, 1fr);
        gap: 28px;
        padding-top: 40px;
        padding-bottom: 44px;
    }

    /* Below this width the copy runs the full width of the band, so a veil that
       fades left-to-right would leave the end of every line sitting on bare
       photograph. The same white is turned to run top-to-bottom instead: near
       opaque where the text is, clearing at the foot where the picture shows.
       Same file — the variable is read, not repeated. */
    .start-hero {
        background-image:
            linear-gradient(
                to bottom,
                rgba(255, 255, 255, 0.97) 0%,
                rgba(255, 255, 255, 0.93) 60%,
                rgba(255, 255, 255, 0.55) 100%
            ),
            var(--start-hero-bild);
    }
}

/* --- Section heads ------------------------------------------------------ */

.start-abschnittskopf {
    margin-bottom: 32px;
    text-align: center;
}

.start-abschnittskopf h2,
.start-ueber h2,
.start-kontakt h2 {
    margin: 0;
    font-size: var(--text-title-fluid);
    font-weight: 700;
    letter-spacing: -0.015em;
    color: var(--color-anchor);
}

.start-abschnittskopf-sub {
    max-width: 60ch;
    margin: 8px auto 0;
    font-size: var(--text-small);
    color: var(--color-text-muted);
}

/* --- The entry blocks are gone (SWEB-293) --------------------------------

   `.start-einstiege`, `.start-einstieg`, `.start-einstieg-motiv`,
   `.start-motiv-klein`, `.start-einstieg-text`, `.start-einstieg-sub` and
   `.start-mehr--zentriert` went with the illustrated tiles the start page used
   to carry above the band. `.start-platzhalter` went with them: it marked the
   empty state of THAT grid and nothing else rendered it.

   `.start-mehr` below stays. It is not the tiles' — „Mehr über Süsse" under the
   Kennzahlen still wears it. */

.start-mehr {
    margin: 14px 0 0;
    font-size: var(--text-small);
    font-weight: 600;
}

.start-mehr a {
    color: var(--color-accent-hover);
}

/* --- The band whose content is still an open decision ------------------- */

.start-band {
    background: var(--color-accent-surface);
    border-top: 1px dashed var(--color-accent-badge-border);
    border-bottom: 1px dashed var(--color-accent-badge-border);
    text-align: center;
}

.start-band h2 {
    margin: 0;
    font-size: var(--text-lead);
    font-weight: 700;
    letter-spacing: 0.02em;
    color: var(--color-accent-active);
}

.start-band-sub {
    max-width: 60ch;
    margin: 10px auto 0;
    font-size: var(--text-small);
    font-style: italic;
    color: var(--color-accent-active);
}

/* --- Über Süsse --------------------------------------------------------- */

.start-ueber .wrap,
.start-kontakt .wrap {
    display: grid;
    gap: 40px;
    align-items: center;
}

.start-ueber .wrap {
    grid-template-columns: minmax(0, 0.85fr) minmax(0, 1.15fr);
}

.start-kontakt .wrap {
    grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
}

.start-ueber-text,
.start-kontakt-text {
    min-width: 0;
}

.start-ueber-sub,
.start-kontakt-sub {
    margin: 14px 0 0;
    font-size: var(--text-body);
    line-height: 1.7;
    color: var(--color-text-muted);
}

/* SWEB-291 — THE HEADLINE DOES NOT LEAVE A WORD BEHIND.

   `text-wrap: balance` evens the two lines out instead of filling the first
   one and dropping the remainder onto the second, which is what left „Jahren"
   alone under a full line. It is the cosmetic half.

   The half that actually HOLDS is in the markup, not here: „125&nbsp;Jahren"
   in pages/home.php. A number and its unit do not take a line break (DIN 5008),
   so „Jahren" cannot be rendered alone at ANY width, in any browser, whether or
   not it supports `text-wrap` — the browser has nowhere to break. A CSS
   property that a browser might not implement is not where a criterion about
   what a visitor sees should rest. */
.start-ueber-text h2 {
    text-wrap: balance;
}

/* SWEB-291 — THREE OF A KIND, AND THE ROW HAS TO READ AS THREE OF A KIND.

   It was a flex row whose items took their content's width, so „Jahre
   Erfahrung" sat in a third of the space „Generation im Familienbesitz"
   needed and the row read as three unrelated things. Equal fractions make the
   three blocks the same width by construction rather than by the labels
   happening to be a similar length — which they are not, and which the next
   copy change would break again.

   `minmax(0, 1fr)` and not `1fr`: a grid track's default minimum is its
   content, so a long unbroken label would push its own column wider than the
   other two and quietly undo the equality this rule exists for. */
.start-kennzahlen {
    display: grid;
    grid-template-columns: repeat(3, minmax(0, 1fr));
    gap: 22px 20px;
    margin: 24px 0 0;
    padding: 0;
    list-style: none;
}

/* Under about 480px three columns are ~85px each, which breaks
   „Ansprechpartner" across three lines to buy an alignment nobody can see.
   One full-width column is still three blocks of the same width. */
@media (max-width: 480px) {
    .start-kennzahlen {
        grid-template-columns: minmax(0, 1fr);
    }
}

.start-kennzahlen li {
    display: flex;
    flex-direction: column;
    min-width: 0;
}

/* Display type again: 30px bold, so step 600 is measured against the 3 : 1
   threshold and clears it. */
.start-kennzahl {
    font-size: var(--text-title);
    font-weight: 700;
    line-height: 1;
    color: var(--color-accent);
}

.start-kennzahl-text {
    margin-top: 5px;
    font-size: var(--text-small);
    color: var(--color-text-muted);
}

@media (max-width: 860px) {
    .start-ueber .wrap,
    .start-kontakt .wrap {
        grid-template-columns: minmax(0, 1fr);
        gap: 26px;
    }

    .start-ueber-motiv {
        order: -1;
        max-width: 420px;
    }

    .start-kontakt-text {
        order: -1;
    }
}

/* --- Kontakt ------------------------------------------------------------ */

.start-kontakt {
    background: var(--color-surface-muted);
    border-top: 1px solid var(--color-border);
}

.start-kontakt-karte {
    min-width: 0;
    padding: 26px 28px;
    background: var(--color-surface);
    border: 1px solid var(--color-border);
    border-radius: 8px;
    box-shadow: 0 1px 4px rgba(0, 0, 0, 0.05);
}

.start-kontakt-name {
    margin: 0;
    font-size: var(--text-body);
    font-weight: 700;
    color: var(--color-anchor);
}

.start-kontakt-rolle {
    margin: 2px 0 0;
    font-size: var(--text-small);
    color: var(--color-text-muted);
}

/* SWEB-295: `.start-kontakt-zeilen` and its three descendant rules went with
   the Telefon / E-Mail / Zeiten list they dressed. The footer's own hotline
   column is the one place the start page shows those values now. The card the
   list sat in stays -- it carries the named contact and the route to /kontakt. */

/* The button moved into the card, where the list used to be. `.start-cta-reihe`
   sets a 32px top margin for the hero, which is a gap between two paragraphs
   and a button row; inside a 26px-padded card, under a two-line name, it is a
   hole. This is the only override and it changes nothing about the hero's. */
.start-kontakt-karte .start-cta-reihe {
    margin-top: 18px;
}


/* --- Inhaltsseite: Textauszeichnung (SWEB-66) --- */

/* WHAT THIS SECTION IS FOR. A page body may now carry a small, fixed set of
   elements — h2, h3, ul, li, strong, em, a, br — instead of only paragraphs.
   Seven of those had no rule anywhere in this stylesheet, so they would have
   rendered at browser defaults: a heading twice the size of the page's own h1
   and a list indented by a value nobody chose. This is the type scale of the
   running text extended to them, and nothing else.

   NO MOCKUP EXISTS FOR THIS PAGE AND NONE WAS BORROWED. The three
   drafts that exist are Webshop-Entwürfe from before the no-cart decision. What
   is below comes from the measure, the scale and the spacing the .inhalt-text
   block already established one section above.

   NOT ONE COLOUR DECLARATION HERE, AND THAT IS DELIBERATE. Every element below
   inherits the running text's colour, already set on `.inhalt-text p`. A
   `color:` here would be a second place the same decision is made, which is
   how two rules drift apart. Nothing is gained by declaring what is already
   correct by inheritance.

   `a` and `p` inside .inhalt-text are styled in the SWEB-50 section above. They
   are not repeated here. */

.inhalt-text h2 {
    margin: 1.9em 0 0.55em;
    font-size: var(--text-heading);
    font-weight: 700;
    letter-spacing: -0.01em;
    overflow-wrap: break-word;
}

.inhalt-text h3 {
    margin: 1.5em 0 0.45em;
    font-size: var(--text-lead);
    font-weight: 700;
    overflow-wrap: break-word;
}

/* The first heading in the block does not push away from the notice above it —
   that gap is already set by .inhalt-hinweis, and two margins meeting is the
   accidental double space nobody can attribute. */
.inhalt-text h2:first-child,
.inhalt-text h3:first-child {
    margin-top: 0;
}

.inhalt-text ul {
    margin: 0 0 1.05em;
    padding-left: 1.35em;
    list-style: disc;
}

/* A list inside a list item: two allowed elements, no rule of their own beyond
   the tighter spacing that keeps a nested level from reading as a new block. */
.inhalt-text li ul {
    margin: 0.35em 0 0;
}

.inhalt-text li {
    margin: 0 0 0.4em;
    font-size: var(--text-body);
    line-height: 1.65;
    overflow-wrap: break-word;
}

.inhalt-text li:last-child {
    margin-bottom: 0;
}

.inhalt-text strong {
    font-weight: 700;
}

.inhalt-text em {
    font-style: italic;
}

/* --- Aktuelles (SWEB-67) --- */

/* APPENDED AS ONE BLOCK AT THE END OF THE FILE, and nothing above this line is
   touched — the same discipline the Kategorieübersicht section states above.

   NO MOCKUP EXISTS FOR THIS PAGE AND NONE WAS BORROWED. None of the
   three approved drafts ever drew an Aktuelles list.

   EVERY COLOUR BELOW REUSES A RECIPE ALREADY PROVEN SAFE ELSEWHERE IN THIS
   FILE, rather than a new, unmeasured pairing:
     - the filter badge and the "current filter" state reuse .product-sku's own
       recipe (accent-badge fill, accent-badge-border frame, accent-active
       text) — step 100 / 200 / 800, exactly as the ramp publishes it for a
       badge on a tinted fill;
     - --color-text-muted for secondary text, the same step .pagehead-lead and
       .category-children already use on this same white surface;
     - --color-anchor for the item title, the same step .category-link already
       uses at a comparable size.
   None of the seven low-contrast --color-text-subtle pairings recorded during
   the 2026-08-02 contrast review is repeated here — a design decision about
   the token's role, not an accident — and an eighth is not added on purpose. */

.aktuelles-filter-list {
    display: flex;
    flex-wrap: wrap;
    gap: 8px;
    margin: 0 0 28px;
    padding: 0;
    list-style: none;
}

.aktuelles-filter-link {
    display: inline-block;
    padding: 7px 14px;
    background: var(--color-surface);
    border: 1px solid var(--color-border);
    border-radius: 999px;
    font-size: var(--text-small);
    font-weight: 600;
    color: var(--color-text-muted);
}

.aktuelles-filter-link:hover {
    border-color: var(--color-accent);
    color: var(--color-accent-hover);
}

/* The active filter — the same fill / frame / text triple .product-sku uses
   for a badge on an accent tint, so "which filter is this" reads the same way
   "which Art.-Nr. is this" already does. */
.aktuelles-filter-link[aria-current="page"] {
    background: var(--color-accent-badge);
    border-color: var(--color-accent-badge-border);
    color: var(--color-accent-active);
}

.aktuelles-list {
    display: flex;
    flex-direction: column;
    gap: 14px;
    margin: 0;
    padding: 0;
    list-style: none;
}

.aktuelles-item {
    padding: 16px 18px;
    background: var(--color-surface);
    border: 1px solid var(--color-border);
    border-radius: 6px;
}

.aktuelles-badge {
    display: inline-block;
    margin: 0 8px 6px 0;
    padding: 3px 10px;
    background: var(--color-accent-badge);
    border: 1px solid var(--color-accent-badge-border);
    border-radius: 999px;
    font-size: var(--text-mini);
    font-weight: 700;
    letter-spacing: 0.06em;
    text-transform: uppercase;
    color: var(--color-accent-active);
}

.aktuelles-date {
    font-size: var(--text-small);
    font-weight: 600;
    color: var(--color-text-muted);
}

.aktuelles-title {
    margin: 6px 0 0;
    font-size: var(--text-body);
    font-weight: 700;
    color: var(--color-anchor);
    overflow-wrap: break-word;
}

/* SWEB-132: the title becomes a link for every kind except download (a
   download has no article page to open — AktuellesItemPage refuses it).
   .aktuelles-title itself still carries --color-anchor, above, for the one
   case that stays plain text; this is the same value stated again on the
   element that is actually clickable, the same split .category-name (no
   colour) / .category-link (the colour) already uses one section up. */
.aktuelles-title-link {
    color: var(--color-anchor);
}

.aktuelles-title-link:hover {
    color: var(--color-accent-hover);
}

.aktuelles-lead {
    margin: 6px 0 0;
    font-size: var(--text-body);
    color: var(--color-text-muted);
}

/* The same metadata role .inhalt-bild-text plays for an image: a stored file
   name shown as plain text until the file itself is served from the host.
   --color-text-muted, not --color-text-subtle — see the note above the
   section banner for why the subtle step is not used here. */
.aktuelles-file {
    margin: 8px 0 0;
    font-size: var(--text-small);
    color: var(--color-text-muted);
}

/* SWEB-69: the stored file name becomes the link to the file itself once the
   file is on the server. The same anchor colour pair .aktuelles-title-link uses
   two rules up — a link on this white surface reads the same way everywhere on
   the page — with an underline, because unlike the title this one sits inside a
   line of running metadata and has no size or weight of its own to mark it. */
.aktuelles-file-link {
    color: var(--color-anchor);
    text-decoration: underline;
}

.aktuelles-file-link:hover {
    color: var(--color-accent-hover);
}

/* The measured size, beside the link. Deliberately no colour of its own: it
   inherits .aktuelles-file's --color-text-muted, because it is the same kind of
   secondary metadata as the file name it follows and a second step here would
   be a new pairing nobody measured. */
.aktuelles-file-groesse {
    white-space: nowrap;
}

/* A row whose file is not on the server (SWEB-69 criterion 2). It renders no
   link at all, so the only thing this rule has to do is stop the line reading
   as an ordinary, complete one — italic, at the same muted step, exactly the
   weight .inhalt-bild-text carries for the image placeholder it mirrors. Not a
   warning colour: an absent file is a normal state on the way to launch, not an
   error the visitor caused. */
.aktuelles-file-fehlt {
    font-style: italic;
}

/* --- Site navigation (SWEB-125) --- */

/* Appended, and nothing above this line is touched — the same fence every
   other slice in this file keeps. SWEB-67 is building /aktuelles and
   /downloads in a sibling worktree off this same base commit at the same
   time, and may also be appending to this file.

   THE SHAPE IS THE MOCKUP'S, THE MENU IS NOT. entwurf.php draws a
   bar with the logo on the left at a 40px height and something on the right,
   stacking below 640px — that geometry ported. The draft's right-hand slot
   holds a phone number and opening hours, which are OUT OF SCOPE for this
   card (SWEB-125's own acceptance criteria say nothing about them); the four
   nav links take that slot rather than sitting beside it. The MENU ITSELF has
   no mockup at all — no draft ever drew a site-wide nav — so its look is the
   type scale and accent usage tokens.css and the components already built
   already established, not a page-specific invention.

   NOT ONE HEX VALUE, NOT A NEW TOKEN: every colour resolves through
   tokens.css, same as every other section. */

.site-header-bar {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 20px;
}

.site-logo {
    display: block;
    flex: 0 0 auto;
    line-height: 0;
}

.site-logo img {
    display: block;
    height: 40px;
    width: auto;
}

.site-nav-list {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 4px 26px;
    margin: 0;
    padding: 0;
    list-style: none;
}

/* SWEB-183 — the navigation links wear the site's one link colour, the anchor
   blue, and hover to the single site-wide hover blue. Colour only: the size,
   the weight and the 2px underline slot are the ones this bar already had. */
.site-nav-link {
    display: inline-block;
    padding: 4px 0;
    font-size: var(--text-body);
    font-weight: 600;
    color: var(--color-anchor);
    border-bottom: 2px solid transparent;
}

.site-nav-link:hover,
.site-nav-link:focus-visible {
    color: var(--color-anchor-hover);
}

/* The entry for the page being viewed (AC 3). The same colour as the hover
   state, so the state reads as "arrived" rather than as a second colour to
   measure; the underline is decoration, not a second contrast rule. */
.site-nav-link[aria-current="page"] {
    color: var(--color-anchor-hover);
    border-bottom-color: var(--color-anchor-hover);
}

/* ===========================================================================
   SWEB-476 — THE MOBILE MENU: A HAMBURGER TOGGLE, ONE COLLAPSED PANEL.

   Below 640px there was no mobile nav mechanism at all — every entry in the
   bar simply stacked and stayed permanently visible, wrapping. That is the
   root cause behind Fred's report that the language dropdown "did not render
   correctly": it was never broken in isolation, it was one of five things
   fighting for the same cramped stack.

   `.site-mobile-panel` WRAPS THE THREE THINGS THAT COLLAPSE — the nav, the
   Anfrageliste link and the language switcher — and is `display: contents`
   by default, so above 640px it contributes no box of its own and its
   children sit as direct flex items of `.site-header-bar`, in the same
   order they always have. That is what makes AC1's "above 640px, today's
   inline layout is unchanged" a property of this rule rather than of two
   templates staying in sync.

   `.site-menu-toggle` is hidden by default for the same reason `.site-
   mobile-panel` needs no hiding rule above 640px: neither is display'd
   differently until the media query below says so.

   SAME INTERACTION SHAPE AS veyond.info's BULMA NAVBAR (`navbar-burger` /
   `navbar-menu`) — reference for the UX only. No dependency, no class from
   that library: every colour resolves through tokens.css exactly like every
   other component in this file, and the state is driven by `aria-expanded`,
   set by `assets/js/nav.js`. See that file's own header for the no-script
   fallback: an attribute that is simply ABSENT (script blocked or failed)
   matches neither rule below, so the panel stays visible and the toggle
   stays hidden — degraded to the pre-card behaviour, never broken. */

.site-menu-toggle {
    display: none;
    align-items: center;
    justify-content: center;
    width: 44px;
    height: 44px;
    padding: 0;
    margin: 0;
    font: inherit;
    background: transparent;
    border: 0;
    border-radius: 4px;
    cursor: pointer;
}

.site-menu-toggle:hover .site-menu-toggle-icon rect,
.site-menu-toggle[aria-expanded="true"] .site-menu-toggle-icon rect {
    fill: var(--color-anchor-hover);
}

.site-menu-toggle:focus-visible {
    outline: 2px solid var(--color-anchor);
    outline-offset: 2px;
}

.site-menu-toggle-icon {
    width: 24px;
    height: 24px;
}

.site-mobile-panel {
    display: contents;
}

@media (max-width: 640px) {
    .site-header-bar {
        flex-wrap: wrap;
        gap: 10px 20px;
    }

    /* Unscoped to any :has() state on purpose — it is the fallback gap for
       the no-script case too (SWEB-476), where `.site-mobile-panel` stays
       `display: contents` and `.site-nav-list` is still the wrapped row it
       always was, just narrower. The column layout below overrides it once
       the panel is actually open. */
    .site-nav-list {
        gap: 6px 18px;
    }

    /* Visible only once assets/js/nav.js has set the attribute — see the
       block comment above this section for why absence, not "false", is
       the no-script state. */
    .site-menu-toggle[aria-expanded] {
        display: flex;
    }

    /* Closed is the state components.css must be able to express on its
       own once the attribute exists; `:has()` is already used in this file
       (.pagehead:has(+ .inhalt-hero)) for the identical reason — reading a
       descendant's state from an ancestor with no class the markup has to
       maintain. */
    .site-header-bar:has(.site-menu-toggle[aria-expanded="false"]) .site-mobile-panel {
        display: none;
    }

    /* Open: the panel drops to its own full-width row below the logo and
       the toggle (`flex-basis: 100%` inside a wrapped flex row is what forces
       the break), and stacks its own three children vertically. */
    .site-header-bar:has(.site-menu-toggle[aria-expanded="true"]) .site-mobile-panel {
        display: flex;
        flex-direction: column;
        align-items: flex-start;
        flex-basis: 100%;
        gap: 12px;
    }

    .site-header-bar:has(.site-menu-toggle[aria-expanded="true"]) .site-nav-list {
        flex-direction: column;
        align-items: flex-start;
        gap: 10px;
    }

    /* THIS IS FRED'S ORIGINAL REPORT, AC3. `.site-language-list` is `right:
       0` (desktop only value, further up this file) because on desktop the
       globe sits at the far right of the bar and the panel opening leftward
       from its own right edge stays on screen. Inside the collapsed mobile
       panel the globe sits at the LEFT (`align-items: flex-start` above), so
       the same `right: 0` opened the panel off the LEFT edge of the
       viewport, clipped — screenshotted and confirmed before this rule
       existed. Anchoring from the icon's own left edge instead keeps the
       whole box on screen at every width this breakpoint covers. */
    .site-language-list {
        right: auto;
        left: 0;
    }
}

/* ===========================================================================
   SWEB-90 — the Anfrageliste: the persistent header control, the product-page
   action, and the list page itself.

   The header control and the product-page button reuse the site-nav's own
   colour pairing (step 700 default, step 800 on hover/focus, both on white) —
   already measured clear by the sweep below, and it is what makes "arrived"
   and "already marked" read as the same family of state across the site.
   The list page's item rows reuse .product-name and .product-sku outright —
   the same classes the category page uses, already gated — so only the
   unavailable state and the remove control are new roles here.
   =========================================================================== */

.site-anfrageliste {
    display: inline-block;
    padding: 4px 0;
    font-size: var(--text-body);
    font-weight: 600;
    color: var(--color-accent-hover);
}

.site-anfrageliste:hover,
.site-anfrageliste:focus-visible {
    color: var(--color-accent-active);
}

/* SWEB-298 folded the layout in here rather than adding a second
   `.pd-anfrage-form` rule further down this file: the add arm now carries a
   labelled quantity field beside the button, and one selector declared twice
   in one stylesheet is the shape that drifts. */
.pd-anfrage-form {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 14px;
    margin: 0 0 20px;
}

.pd-anfrage-btn {
    display: inline-block;
    padding: 11px 22px;
    font: inherit;
    font-size: var(--text-body);
    font-weight: 700;
    color: var(--color-surface);
    background: var(--color-accent-hover);
    border: 1px solid var(--color-accent-hover);
    border-radius: 5px;
    cursor: pointer;
}

.pd-anfrage-btn:hover,
.pd-anfrage-btn:focus-visible {
    background: var(--color-accent-active);
    border-color: var(--color-accent-active);
}

/* The toggle state, once the product is already marked: outline rather than
   filled, so "click again to remove" reads as a lighter action than the
   first, filled "add" — never a second colour, the same accent family. */
.pd-anfrage-btn--marked {
    color: var(--color-accent-hover);
    background: var(--color-surface);
}

.pd-anfrage-btn--marked:hover,
.pd-anfrage-btn--marked:focus-visible {
    color: var(--color-accent-active);
    background: var(--color-surface);
    border-color: var(--color-accent-active);
}

/* SWEB-183 — `Anfrage senden` on /anfrageliste. It is the same BOX as the
   product page's own action above (which keeps the accent), and now the same
   blue button as `Suchen`: the anchor blue filled, white label, the one
   site-wide hover blue. A modifier rather than a change to `.pd-anfrage-btn`
   itself, because only the list page's send control is in this card's scope —
   the product page's add/remove toggle is not. Colour only. */
.pd-anfrage-btn--senden {
    color: var(--color-surface);
    background: var(--color-anchor);
    border-color: var(--color-anchor);
}

.pd-anfrage-btn--senden:hover,
.pd-anfrage-btn--senden:focus-visible {
    color: var(--color-surface);
    background: var(--color-anchor-hover);
    border-color: var(--color-anchor-hover);
}

.anfrage-list {
    display: flex;
    flex-direction: column;
    margin: 0;
    padding: 0;
    list-style: none;
}

.anfrage-item {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 20px;
    padding: 16px 0;
    border-bottom: 1px solid var(--color-border-subtle);
}

.anfrage-item-info {
    min-width: 0;
}

/* Usage rule: normal-size text — step 600 on white measures 3.55 : 1, so the muted
   state uses the neutral text-muted token instead (7.46 : 1), never the
   accent ramp. */
.anfrage-item-name--unavailable {
    color: var(--color-text-muted);
}

/* SWEB-298 — one form per row now, carrying the quantity field and both
   controls. `.anfrage-item-remove-form` became `.anfrage-item-form`; the rule
   is the same and the name follows what the element does. */
.anfrage-item-form {
    display: flex;
    flex: 0 0 auto;
    align-items: center;
    gap: 14px;
}

/* Label and field on one line: it is two words next to a small box, and
   stacking it would make the row taller than the product name beside it. */
.anfrage-item-menge,
.pd-menge {
    display: inline-flex;
    align-items: center;
    gap: 8px;
}

.anfrage-item-menge-label,
.pd-menge-label {
    font-size: var(--text-small);
    font-weight: 600;
    color: var(--color-text);
}

/* 5ch plus the padding: room for four digits and the browser's own spinner,
   and no wider — a field sized for a hundred thousand invites one. `field-sizing`
   is not used; it is not in every browser yet and a fixed width here is not a
   compromise, it is the right width. */
.anfrage-item-menge-feld,
.pd-menge-feld {
    width: 5.5rem;
    padding: 8px 10px;
    font: inherit;
    font-size: var(--text-small);
    color: var(--color-text);
    background: var(--color-surface);
    border: 1px solid var(--color-border);
    border-radius: 4px;
}

.anfrage-item-menge-feld:focus-visible,
.pd-menge-feld:focus-visible {
    border-color: var(--color-accent);
    outline: 2px solid var(--color-accent);
    outline-offset: 1px;
}

/* The same quiet text button `.anfrage-item-remove` is, because the two sit
   side by side and neither is the primary act on this page — that is
   „Anfrage senden" at the foot. Two filled buttons per row, times fifty rows,
   would drown it. */
.anfrage-item-menge-btn {
    font: inherit;
    font-size: var(--text-small);
    font-weight: 600;
    color: var(--color-accent-hover);
    background: none;
    border: none;
    padding: 4px 0;
    cursor: pointer;
    text-decoration: underline;
}

.anfrage-item-menge-btn:hover,
.anfrage-item-menge-btn:focus-visible {
    color: var(--color-accent-active);
}

/* SWEB-298 - the READ-ONLY quantity, on /kontakt and the confirmation. It is
   metadata under a product name, the same weight as the Art.-Nr. line beside
   it, and it is not a control. Usage rule: normal-size text, so the neutral
   muted token. */
.anfrage-item-menge-text {
    margin: 2px 0 0;
    font-size: var(--text-small);
    color: var(--color-text-muted);
}

/* The product page's own refusal line. Usage rule: normal-size text, so the
   neutral muted token rather than a step off the accent ramp. */
.pd-menge-fehler {
    margin: 8px 0 0;
    font-size: var(--text-small);
    font-weight: 600;
    color: var(--color-text-muted);
}

/* Under about 480px a row cannot hold a name, a labelled field and two
   controls side by side, so the form drops under the name and lines up left
   instead of squeezing. */
@media (max-width: 480px) {
    .anfrage-item {
        flex-direction: column;
        align-items: flex-start;
    }

    .anfrage-item-form {
        flex-wrap: wrap;
    }
}

.anfrage-item-remove {
    font: inherit;
    font-size: var(--text-small);
    font-weight: 600;
    color: var(--color-accent-hover);
    background: none;
    border: none;
    padding: 4px 0;
    cursor: pointer;
    text-decoration: underline;
}

.anfrage-item-remove:hover,
.anfrage-item-remove:focus-visible {
    color: var(--color-accent-active);
}

/* SWEB-92 — the Kontakt honeypot (layer 1). Off-screen, not display:none or
   visibility:hidden: some bots skip an element they can detect is invisible
   by computed style, so this keeps the field genuinely rendered — just
   nowhere a human will ever scroll to or tab into (tabindex="-1" in the
   markup handles the tab order; aria-hidden handles the accessibility tree;
   this rule handles sight). No colour literal — layout only. */
.kontakt-honeypot {
    position: absolute;
    left: -9999px;
    width: 1px;
    height: 1px;
    overflow: hidden;
}

/* SWEB-70's `.site-footer-links` and `.site-footer-links-list` used to sit
   here — the footer as one flat row of four links. SWEB-185 replaced the
   markup with four columns, so both class names are gone from the templates
   and their rules are gone from this file rather than left behind to rot. The
   replacement is the SWEB-185 section at the foot of this stylesheet, which
   owns the whole component in one place beside partials/footer.php. */

/* --- Suche (SWEB-84) --- */

/* Appended, and nothing above this line is touched. On the catalogue pages
   (/kategorien, a category page, a product page) and on /suche itself —
   Tom's placement ruling, 2026-08-06 — never in .site-header, which SWEB-158
   owns this sprint.

   No `color:` declaration anywhere in this section, deliberately: this box
   sets none and inherits the ambient text colour `body` already declares.
   `border`/`background` below reference tokens for the FRAME and FILL only,
   never for text, which is what a plain substring check for the literal
   `color:` confirms this section is free of. */

.search-box {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 8px 12px;
    margin: 0 0 24px;
}

.search-box-label {
    font-size: var(--text-small);
    font-weight: 600;
}

.search-box-input {
    flex: 1 1 240px;
    min-width: 0;
    padding: 9px 12px;
    font: inherit;
    font-size: var(--text-body);
    background: var(--color-surface);
    border: 1px solid var(--color-border);
    border-radius: 5px;
}

/* SWEB-183 — `Suchen` is the site's filled blue button: the anchor blue as the
   fill, a white label, and the one site-wide hover blue. Colour only; the box
   is the one SWEB-164 already set. */
.search-box-submit {
    flex: 0 0 auto;
    padding: 9px 18px;
    font: inherit;
    font-size: var(--text-body);
    font-weight: 600;
    color: var(--color-surface);
    background: var(--color-anchor);
    border: 1px solid var(--color-anchor);
    border-radius: 5px;
    cursor: pointer;
}

.search-box-submit:hover,
.search-box-submit:focus-visible {
    background: var(--color-anchor-hover);
    border-color: var(--color-anchor-hover);
}

/* SWEB-164 — `Zurücksetzen`, the way back to the whole catalogue. It sits
   beside `Suchen` and is deliberately the QUIETER of the two: same height and
   the same box, so the primary action still reads as the primary one.
   SWEB-183 — it is now a button rather than a bare link: the same two blues as
   `Suchen`, outlined instead of filled, and the underline goes with the link
   look. The two are still told apart by their own words and now by fill vs
   outline, never by colour alone — both wear the same blue. The 1px border
   slot was already reserved as transparent, so giving it a colour moves
   nothing. */
.search-box-reset {
    flex: 0 0 auto;
    padding: 9px 12px;
    font-size: var(--text-body);
    font-weight: 600;
    color: var(--color-anchor);
    background: var(--color-surface);
    border: 1px solid var(--color-anchor);
    border-radius: 5px;
}

.search-box-reset:hover,
.search-box-reset:focus-visible {
    color: var(--color-surface);
    background: var(--color-anchor-hover);
    border-color: var(--color-anchor-hover);
}

/* ===========================================================================
   SWEB-107 — the consent banner and its "change your choice" twin on
   /datenschutz. The minimum this needs: a band, two buttons.

   ---------------------------------------------------------------------------
   SWEB-165 — the redesign. Presentation only; the mechanism above is
   untouched, and the markup in partials/consent-banner.php is untouched too.
   ---------------------------------------------------------------------------

   1. `.consent-banner` is now `position: fixed` at the BOTTOM of the
      viewport. It never moves in the DOCUMENT — still the second focusable
      stop after the skip-link, still before the header (layout.php,
      unchanged) — so gate G4's "reachable in reading order" keeps holding
      unread. Only where it is PAINTED changes, which is the whole point AC 2
      names: visual position and document order are two different facts.

   2. It now reads as a decision being asked for: a border on the edge that
      actually separates it from the page — the TOP edge, now that it sits at
      the bottom — plus a shadow, replacing the flat, borderless "promo bar"
      look Tom's finding (2026-08-06) named.

   3. `.consent-btn` took the site's own FILLED, CI-coloured button look
      (compare `.start-btn--primaer`, home.php) instead of a thin grey
      rectangle — SWEB-165 AC 4's "the site's own button styles and CI
      colours" — and originally both buttons shared that one rule, with no
      modifier for either action, so that nothing but the label text told
      them apart.

   ---------------------------------------------------------------------------
   2026-08-08 — TOM'S RULING SUPERSEDES THAT ONE-RULE-FOR-BOTH DESIGN. NOT A
   BUG, NOT A DRIFT — A NAMED DECISION.
   ---------------------------------------------------------------------------
   Akzeptieren is now filled `--color-ok`, white label. Ablehnen is white with
   a `--color-ok` border and a `--color-ok` label. `.consent-btn` still carries
   every property that stays identical between the two — height (padding),
   font size and weight, border WIDTH, border-radius, cursor — so the only
   thing `.consent-btn--annehmen` / `.consent-btn--ablehnen` set is the fill:
   background, border-colour, text colour. Both remain plain
   `<button type="submit">` elements in document order; nothing here adds a
   tabindex or removes either from the keyboard, and both still read with the
   stylesheet removed, exactly as before.

   `--color-ok` measures 3,41 : 1 against white — below the site's usual
   4,5 : 1 floor for normal-size text. Tom ruled the colours anyway, told
   this exact number. This is recorded as a DATED, NAMED DECISION for these
   two selectors only, not a general change to the 4,5 : 1 floor this project
   otherwise holds to. See also tokens.css's own `--color-ok` comment,
   updated the same day for the same reason.
   =========================================================================== */

.consent-banner {
    position: fixed;
    left: 0;
    right: 0;
    bottom: 0;
    z-index: 200;
    background: var(--color-surface-tint);
    border-top: 1px solid var(--color-border);
    box-shadow: 0 -6px 18px rgba(0, 0, 0, 0.10);
}

.consent-banner-inner {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    justify-content: space-between;
    gap: 14px 24px;
    padding: 16px 0;
}

.consent-banner-text {
    flex: 1 1 320px;
}

.consent-banner-heading {
    margin: 0 0 4px;
    font-size: var(--text-body);
    font-weight: 700;
}

.consent-banner-text p:last-child {
    margin: 0;
    font-size: var(--text-small);
    line-height: 1.5;
}

.consent-banner-actions {
    display: flex;
    flex-wrap: wrap;
    gap: 10px;
    flex: 0 0 auto;
}

.consent-banner-form {
    margin: 0;
}

.consent-btn {
    padding: 12px 24px;
    font: inherit;
    font-size: var(--text-body);
    font-weight: 600;
    border-width: 1px;
    border-style: solid;
    border-radius: 4px;
    cursor: pointer;
}

/* Akzeptieren — filled --color-ok, white label. Tom's ruling, 2026-08-08;
   see this section's header comment and tokens.css's --color-ok comment. */
.consent-btn--annehmen {
    color: var(--color-surface);
    background: var(--color-ok);
    border-color: var(--color-ok);
}

.consent-btn--annehmen:hover,
.consent-btn--annehmen:focus-visible {
    /* Darkens the fill without a second green token — no `color:` here, so
       this state sets no text colour, same convention the rest of this file
       already uses for a hover state. */
    box-shadow: inset 0 0 0 999px rgba(0, 0, 0, 0.08);
}

/* Ablehnen — white background, 1px --color-ok border, --color-ok label.
   Same Tom ruling. The only difference from Akzeptieren is the fill. */
.consent-btn--ablehnen {
    color: var(--color-ok);
    background: var(--color-surface);
    border-color: var(--color-ok);
}

.consent-btn--ablehnen:hover,
.consent-btn--ablehnen:focus-visible {
    background: var(--color-surface-tint);
}

/* --- The privacy-page twin (pages/inhalt.php, /datenschutz only) --------- */

.inhalt-consent {
    margin: 32px 0 0;
    padding: 16px 18px;
    background: var(--color-surface-tint);
    border: 1px solid var(--color-border);
    border-radius: 6px;
}

.inhalt-consent-titel {
    margin: 0 0 7px;
    font-size: var(--text-body);
    font-weight: 700;
    letter-spacing: -0.01em;
}

.inhalt-consent-status {
    font-weight: 600;
}


/* ===========================================================================
   THE FOOTER (SWEB-185) — four columns. The slim bar under them was removed
   by SWEB-292; see the note where its rules used to sit, at the end of this
   section.

   Markup: src/View/templates/partials/footer.php, included once by layout.php.
   The one rule that survives from before this card is `.site-footer`, near the
   top of this file; everything else the footer wears is here.

   NOT A HEX VALUE IN THIS SECTION — the file-level rule, restated because a
   footer is exactly the place a "just this once" grey gets typed. The links
   take the anchor blue SWEB-183 made the site's one link colour, which is why
   this section carries a colour rule at all: the file's plain `a` rule resolves
   to the accent, and the footer is not an exception to SWEB-183, it is the
   last part of the site to join it.
   =========================================================================== */

/* The column band. The hotline column is deliberately the wide one: this site
   shows no price and has no cart, so the telephone is how a visitor buys. */
.site-footer-spalten {
    display: grid;
    grid-template-columns: 1.5fr 1fr 1fr 1fr;
    gap: 30px 40px;
    padding-top: 42px;
    padding-bottom: 34px;
}

/* Criterion 6 — reflow, no horizontal scrollbar. A grid track's default
   `min-width: auto` refuses to shrink below its longest unbreakable word, and
   that is the one way this band could push the page wider than the window:
   `+49 5603 502-0` at hotline size. Both properties together are the fix —
   the track may shrink, and the word may break rather than overflow it. */
.site-footer-spalten > * {
    min-width: 0;
    overflow-wrap: break-word;
}

@media (max-width: 860px) {
    .site-footer-spalten {
        grid-template-columns: 1fr 1fr;
        gap: 26px 32px;
    }
}

@media (max-width: 520px) {
    .site-footer-spalten {
        grid-template-columns: 1fr;
        padding-top: 32px;
        padding-bottom: 26px;
    }
}

/* One rule for all four column headings, hotline included — they are the same
   thing and must not drift apart in size or weight. */
.site-footer-titel {
    margin: 0 0 12px;
    font-size: var(--text-small);
    font-weight: 700;
    letter-spacing: 0.04em;
    text-transform: uppercase;
    color: var(--color-text);
}

/* --- The hotline column ------------------------------------------------- */

.site-footer-tel {
    margin: 0 0 6px;
    font-size: var(--text-heading);
    font-weight: 700;
    line-height: 1.2;
}

.site-footer-zeiten {
    margin: 0 0 10px;
    font-size: var(--text-small);
    color: var(--color-text-muted);
}

/* Two spans, two lines — set here rather than as two <p>s so the pair keeps
   one margin between it and the mail address below. THREE spans since
   SWEB-296, the third being what happens outside those hours; it needs no
   rule of its own beyond the small gap below. */
.site-footer-zeiten span {
    display: block;
}

/* SWEB-296 — a little air so the qualification is not read as a third opening
   time. No colour of its own: it inherits `.site-footer-zeiten`'s muted token,
   which is the one the usage rule already allows at this size. */
.site-footer-zeiten-ki {
    margin-top: 6px;
}

/* Same on /kontakt, inside the Erreichbarkeit row. */
.kontakt-daten-ki {
    display: inline-block;
    margin-top: 6px;
    font-size: var(--text-small);
    color: var(--color-text-muted);
}

.site-footer-mail {
    margin: 0 0 12px;
    font-size: var(--text-body);
}

.site-footer-hotline-note {
    margin: 0;
    max-width: 30ch;
    font-size: var(--text-small);
    line-height: 1.5;
    color: var(--color-text-muted);
}

/* --- The three link columns --------------------------------------------- */

.site-footer-liste {
    margin: 0;
    padding: 0;
    list-style: none;
}

.site-footer-liste li + li {
    margin-top: 7px;
}

.site-footer-liste a {
    font-size: var(--text-body);
}

/* --- The one colour rule, for every link in the footer ------------------- */

/* SWEB-183's anchor blue, the site's one link and button colour. Written once,
   for the whole footer, rather than per column: the hotline number, the mail
   address and the three link columns are all links on the same surface and
   there is no case for two blues down here. */
.site-footer a {
    color: var(--color-anchor);
}

.site-footer a:hover,
.site-footer a:focus-visible {
    color: var(--color-anchor-hover);
    text-decoration: underline;
}

/* --- The slim bar is gone (SWEB-292) -------------------------------------

   `.site-footer-bar`, `.site-footer-bar-inhalt` and `.site-footer-note` all
   went with the markup. `.site-footer-note` was the last of them: it was a
   survivor from before SWEB-185 and lived in two files (here and base.css),
   and after the band came out no template used the class at all. */

/* ===========================================================================
   THE HERO BAND AND THE INLINE PICTURE SLOTS — SWEB-226
   ---------------------------------------------------------------------------
   Both are the same idea at two sizes: a picture that is not on the server yet,
   drawn at the size and shape it will occupy, so the page can be judged as a
   layout rather than imagined as one. Neither is an <img>; a missing file would
   render a broken icon, which reads as a fault instead of as an unfinished
   layer.

   THE GREY IS THE NEUTRAL SURFACE, NOT A NEW COLOUR. --color-surface-muted and
   --color-border are the tokens the site already carries; there is no hex here
   and there is nothing brand-coloured about a slot that is going to be covered
   by a photograph. The one accented thing on the band is the claim, which is
   type rather than decoration.
   =========================================================================== */

.inhalt-hero {
    background: var(--color-surface);
    padding: 28px 0 0;
}

/* The size IS the design decision: a hero is a band, so it is stated as a
   minimum height rather than an aspect ratio, which on a wide screen would
   grow a placeholder to the height of a room. */
.inhalt-hero-flaeche {
    position: relative;
    display: flex;
    flex-direction: column;
    align-items: flex-start;
    justify-content: center;
    gap: 14px;
    min-height: 300px;
    padding: 34px 32px;
    background: var(--color-surface-muted);
    border: 1px solid var(--color-border);
    border-radius: 8px;
}

/* WITH A REAL PICTURE the band stops being a grey slab and becomes the picture.
   The claim still sits ON it, so the image is a layer underneath rather than a
   sibling: absolutely positioned, cropped by the band's own rounded corner, and
   the text lifted above it. The min-height above still governs -- a band, never
   a room -- which is why the image is `cover` and not `contain`. */
.inhalt-hero-flaeche--bild {
    overflow: hidden;
    background: var(--color-surface-tint);
    border-color: transparent;
}

.inhalt-hero-bild {
    position: absolute;
    inset: 0;
    width: 100%;
    height: 100%;
    object-fit: cover;
    object-position: center right;
}

/* The claim has to stay readable over the picture, and it has to stay ON ONE
   LINE -- Tom, 2026-08-29: a claim that breaks after "Probe" leaves "an."
   stranded on a line of its own, which reads as a fault rather than as a line
   break. So the width is not a percentage of the band any more: the text sets
   its own width and simply refuses to wrap, and the picture is what moves.
   `object-position` keeps the busy right-hand side of the image out from under
   it. On a narrow screen wrapping comes back deliberately -- one long line off
   the side of a phone is worse than two. */
.inhalt-hero-flaeche--bild .inhalt-hero-claim {
    position: relative;
    z-index: 1;
    max-width: none;
    white-space: nowrap;
}

@media (max-width: 900px) {
    .inhalt-hero-flaeche--bild .inhalt-hero-claim {
        white-space: normal;
        max-width: 100%;
    }
}

@media (max-width: 640px) {
    .inhalt-hero-flaeche {
        min-height: 220px;
        padding: 22px 20px;
    }
}

/* Same marker, same words and same corner as the image slot below, on purpose:
   two placeholders that look like two different mechanisms is how a reader
   starts wondering whether one of them is broken. */
.inhalt-hero-marke,
.inhalt-platzhalter-marke {
    position: absolute;
    top: 10px;
    left: 10px;
    padding: 2px 8px;
    background: var(--color-surface);
    border: 1px solid var(--color-border);
    border-radius: 3px;
    font-size: var(--text-mini);
    letter-spacing: 0.05em;
    text-transform: uppercase;
    color: var(--color-text-muted);
}

/* The claim carries the band. Anchor blue rather than the accent: it is a
   heading in everything but element name, and headings on this site are blue. */
.inhalt-hero-claim {
    margin: 0;
    max-width: 26ch;
    font-size: var(--text-title-fluid);
    font-weight: 700;
    line-height: 1.15;
    letter-spacing: -0.02em;
    color: var(--color-anchor);
}

/* The caption is a BRIEF for whoever supplies the picture, not a subtitle. It
   was accent-coloured on the first pass and read as page copy under the claim —
   which is the one thing it must not do, because it disappears the day the
   photograph arrives. Neutral, small, and clearly a production note. */
.inhalt-hero-text {
    margin: 0;
    max-width: 60ch;
    font-size: var(--text-small);
    line-height: 1.5;
    color: var(--color-text-muted);
}

/* --- The inline picture slot, printed by BodyMarkup ---------------------- */

/* It sits BETWEEN two paragraphs, so its margins are symmetrical — unlike
   .inhalt-bild, which is the last thing on the page and only needs room above. */
.inhalt-platzhalter {
    margin: 34px 0;
}

/* WIDTH-CONSTRAINED, NOT HEIGHT-CAPPED. At the full 1080px measure a 16:9 box
   is 608px tall and reads as a hole in the page rather than as a picture that
   is coming. Capping the HEIGHT instead would have kept the width and quietly
   drawn a letterbox — a placeholder that lies about the shape of the image it
   stands for. So the box is narrowed and centred, and the ratio stays true. */
.inhalt-platzhalter-flaeche {
    position: relative;
    width: 100%;
    max-width: 640px;
    margin: 0 auto;
    aspect-ratio: 16 / 9;
    background: var(--color-surface-muted);
    border: 1px solid var(--color-border);
    border-radius: 6px;
}

.inhalt-platzhalter-text {
    width: 100%;
    max-width: 640px;
    margin: 9px auto 0;
    font-size: var(--text-small);
    line-height: 1.5;
    color: var(--color-text-muted);
    overflow-wrap: break-word;
}

/* --- The marker resolved to a real file (SWEB-454, renamed SWEB-444) ------ */

/* WAS `.inhalt-zertifikat` / `.inhalt-zertifikat-bild`. The one file this had
   ever printed was Süsse's ISO certificate, so the name described the picture
   rather than the slot; the Maxibrief post (SWEB-444) sends two landscape
   graphics through the identical printer. Renamed at both ends in one change —
   `BodyMarkup::printBodyImage()` is the only thing that emits either class, and
   nothing else in this stylesheet referenced them. */

/* Same figure margin as .inhalt-platzhalter above: it sits between the same
   two kinds of paragraph, real image or not. */
.inhalt-inhaltsbild {
    margin: 34px 0;
}

/* NOT CROPPED, NOT LETTERBOXED, AND NOT SHRUNK TO A THUMBNAIL — SWEB-454 AC 6,
   which still binds and now has to hold for two shapes at once.

   SWEB-454 capped the WIDTH at 420px, which was right while the only file was
   a full-page portrait scan (1190×1684). It is wrong for a landscape band: the
   Maxibrief graphic is 1191×742 and at 420px wide its own headline is 35 % of
   the size it was drawn at, which is the "shrunk to a thumbnail" failure the
   criterion names, arrived at from the other direction.

   So the cap is on the HEIGHT and the width simply does not exceed the measure.
   `height: auto` keeps the ratio true either way — nothing is cropped and
   nothing is letterboxed, which is what the original rule was protecting:

     portrait  1190×1684  ->  424 × 600   (was 420 × 594)
     landscape 1191× 742  ->  963 × 600 capped by the measure on a narrow screen

   The portrait case therefore moves by four pixels, which is the whole of the
   effect on /qualitaet. */
.inhalt-inhaltsbild-datei {
    display: block;
    width: auto;
    max-width: 100%;
    height: auto;
    max-height: 600px;
    margin: 0 auto;
    border: 1px solid var(--color-border);
    border-radius: 6px;
}

/* ===========================================================================
   THE PEOPLE — SWEB-231, REMOVED BY SWEB-300
   ---------------------------------------------------------------------------
   `.personen`, `.person`, `.person-kopf`, `.person-kopf-text`, `.person-foto`,
   `.person-monogramm`, `.person-name`, `.person-rolle`, `.person-daten`,
   `.person-zeile` and `.person-label` all went with the block
   `pages/inhalt.php` no longer renders. Süsse do not publish their staff's
   names and direct numbers.

   `public/assets/img/personen/` still holds two photographs (the two dogs).
   They are not referenced by anything now; they are not ours to delete and
   they are the only employee-adjacent pictures Süsse have supplied, so they
   stay on disk and are reported rather than removed.
   =========================================================================== */

/* ===========================================================================
   THE CONSENT BOXES ON THE ENQUIRY FORM — SWEB-236
   ---------------------------------------------------------------------------
   Two checkboxes with sentences beside them, and the whole layout problem is
   the second line: a wrapped label must align under the first word of its own
   text, not under the box. Grid rather than flex, so the text column is a real
   column and the hanging indent is a property of the layout instead of a
   padding somebody guessed.

   `color-scheme` is set on the root elsewhere or it is not; these boxes take
   the browser's own control either way. A hand-drawn checkbox is a control the
   keyboard and the screen reader have to be taught about again, and there is
   nothing wrong with the one the platform ships.
   =========================================================================== */

.kontakt-einwilligung {
    display: flex;
    flex-direction: column;
    gap: 12px;
    padding-top: 4px;
}

.kontakt-check {
    display: grid;
    grid-template-columns: auto minmax(0, 1fr);
    align-items: start;
    gap: 10px;
    margin: 0;
}

/* Nudged down to sit on the first LINE of the label rather than on the top of
   the block — a checkbox is 16px and a line of this text is about 21. */
.kontakt-check input[type="checkbox"] {
    margin: 3px 0 0;
    width: 16px;
    height: 16px;
    accent-color: var(--color-anchor);
}

/* NORMAL WEIGHT, explicitly. The form's own `label` rule is bold, which is
   right for a field name — "Nachricht" — and wrong for a sentence. Three bold
   lines of consent text read as a warning rather than as something to read. */
.kontakt-check label {
    font-size: var(--text-small);
    font-weight: 400;
    line-height: 1.5;
    color: var(--color-text);
}

.kontakt-check label a {
    color: var(--color-anchor);
    text-decoration: underline;
    text-underline-offset: 2px;
}

.kontakt-check label a:hover {
    color: var(--color-anchor-hover);
}

/* The same marker the required text fields already wear, so "required" looks
   like one thing on this form and not two. */
.kontakt-pflicht {
    color: var(--color-accent-hover);
}

/* ===========================================================================
   SWEB-428 — THE LANGUAGE CONTROL IS NOW A GLOBE-ICON DISCLOSURE, LAST IN THE
   HEADER BAR, AFTER ANFRAGELISTE. Restructures SWEB-390's permanently-visible
   "DE · EN" pair into a one-icon `<details>`/`<summary>` that opens on click
   with no script.

   THE ICON REUSES THE NAV'S OWN ANCHOR BLUE, the identical token
   `--color-anchor` / `--color-anchor-hover` pairing SWEB-390 already used and
   the sweep this file records for `.site-nav-link` already measured clear on
   white — nothing about the palette changed, only the shape of the control.
   The glyph itself carries its colour as a `fill="var(--color-anchor)"`
   attribute inline in the markup (`partials/language-switch.php`), the same
   pattern `.product-thumb-figure`'s placeholder already uses a few hundred
   lines up this file — not a class here, because the colour lives on the
   SVG path itself, but the SAME token and the SAME house rule: no hex outside
   `tokens.css`.

   THE PANEL IS A CARD, not a menu widget: `background`, `border` and
   `box-shadow` matching `.category-list-item`'s own values elsewhere in this
   file, because a floating panel and a card are the same visual problem
   (a raised rectangle of options on a white page) and this site already has
   an answer for it.

   THE CURRENT LANGUAGE IS STILL THE MUTED COLOUR, unchanged from SWEB-390:
   the nav marks the page you are ON as the interesting one, and this control
   marks the language you are already reading as the one that is NOT a
   choice — a visitor looking for the other language needs that link to be
   the loud half.
   =========================================================================== */

.site-language {
    flex: 0 0 auto;
}

.site-language-details {
    position: relative;
}

/* Native disclosure marker (the browser's own triangle) removed: the globe
   is the whole trigger, per AC2 — no second, unnamed shape beside it. */
.site-language-trigger {
    display: flex;
    align-items: center;
    justify-content: center;
    width: 32px;
    height: 32px;
    padding: 0;
    list-style: none;
    cursor: pointer;
    border-radius: 4px;
}

.site-language-trigger::-webkit-details-marker {
    display: none;
}

.site-language-trigger::marker {
    content: "";
}

.site-language-trigger:focus-visible {
    outline: 2px solid var(--color-anchor);
    outline-offset: 2px;
}

.site-language-icon {
    width: 22px;
    height: 22px;
}

.site-language-trigger:hover .site-language-icon path {
    fill: var(--color-anchor-hover);
}

.site-language-details[open] > .site-language-trigger .site-language-icon path {
    fill: var(--color-anchor-hover);
}

.site-language-list {
    position: absolute;
    top: calc(100% + 8px);
    right: 0;
    z-index: 20;
    display: flex;
    flex-direction: column;
    gap: 2px;
    min-width: 130px;
    margin: 0;
    padding: 6px;
    list-style: none;
    background: var(--color-surface);
    border: 1px solid var(--color-border);
    border-radius: 6px;
    box-shadow: 0 4px 14px rgba(0, 0, 0, 0.10);
}

.site-language-link,
.site-language-current {
    display: block;
    padding: 6px 10px;
    font-size: var(--text-body);
    font-weight: 600;
    border-radius: 4px;
}

.site-language-link {
    color: var(--color-anchor);
}

.site-language-link:hover,
.site-language-link:focus-visible {
    color: var(--color-anchor-hover);
    background: var(--color-anchor-surface);
}

.site-language-current {
    color: var(--color-text-muted);
}
