/**
 * Design tokens, light and dark (framework core).
 *
 * Three layers, in this order:
 *   1. :root                       light values, the baseline
 *   2. @media (prefers-color-scheme: dark)   the system preference
 *   3. :root[data-theme="light|dark"]        an explicit choice, which wins
 *
 * Layer 3 must come last and must set BOTH directions, or a visitor whose system
 * is dark cannot choose light: the media query would keep overriding them.
 *
 * A fork rebrands by overriding the --brand-* values in its own stylesheet. The
 * semantic tokens below are derived from them, so a single accent change carries
 * through both themes.
 *
 * Every foreground/background pair here is measured against WCAG 2.1 AA
 * (>= 4.5:1 body text, >= 3.0:1 large text and UI). tests/contrast.test.mjs
 * re-measures them, so a rebrand that breaks contrast fails the test run rather
 * than shipping.
 */

/* JBC-03 (christiane-mayer): the original site (christianemayer.de) sets
   Roboto as every Elementor global-typography role (post-12.css's Kit
   stylesheet, --e-global-typography-*-font-family: "Roboto" on all four
   roles: primary, secondary, text, accent). framework.md's hard rule
   ("no third-party loads before consent") and tools/gate/rules/fonts.mjs's
   FONT-EXTERNAL check both require a local file, not a Google Fonts CDN
   link, so this ships the three weights the site actually uses (400 body,
   500 emphasis/nav, 700 headings) as self-hosted woff2, "latin" subset only
   (U+0000-00FF, which covers German umlauts and ß plus the full ASCII/EN
   range; the "latin-ext" subset Google also serves is Central/East-European
   diacritics this fork's DE/EN content never uses). font-display: swap
   avoids a flash of invisible text while the file loads. */
@font-face {
  font-family: "Roboto";
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url("../fonts/roboto-400.woff2") format("woff2");
}
@font-face {
  font-family: "Roboto";
  font-style: normal;
  font-weight: 500;
  font-display: swap;
  src: url("../fonts/roboto-500.woff2") format("woff2");
}
@font-face {
  font-family: "Roboto";
  font-style: normal;
  font-weight: 700;
  font-display: swap;
  src: url("../fonts/roboto-700.woff2") format("woff2");
}

:root {
  color-scheme: light dark;

  /* --- brand seed: a fork overrides these five and inherits the rest --------
     JBC-03 (christiane-mayer): sourced from the original site's Elementor
     global kit (christianemayer.de, elementor/css/post-12.css, the site-wide
     Kit stylesheet), the only place a WordPress/Elementor site declares its
     brand palette once for every page. Kit primary #6a7c5b (sage green) was
     NOT usable as-is: as --accent-text (a fork's link colour, tested against
     BOTH --surface #FFF and --bg #FBFBFC) it measures 4.52:1 on white but
     only 4.37:1 on --bg, under the 4.5:1 AA floor tests/contrast.test.mjs
     enforces. Darkened to the deepest sage that still reads as the same hue
     family while clearing both surfaces with margin (6.88:1 / 6.65:1). The
     dark-theme counterpart is a fresh, brighter sage (not a straight
     lighten of the light value, same reasoning tokens.css's own dark block
     comments give for every other token pair) so it reads AS an accent
     against a dark surface, not as a desaturated afterthought. */
  --brand-accent:        #4F5F44;   /* primary action / link colour, light theme */
  --brand-accent-dark:   #8FBE79;   /* the same idea on a dark surface          */
  --brand-accent-2:      #5F7350;   /* gradient partner                          */
  /* JBC-27 (25.09.2026): theme-fix ON PURPOSE, KEIN Dark-Wert. Grund ist
     site/editor/assets/admin-brand.css (core:once[editor]): es bindet
     --rbe-navy-950/--rbe-navy-900 an genau DIESEN Token, weil admin.css's
     eigener Kommentar die Admin-Topleiste/den Login-Gradient/den
     Gold-Button-Text als "fixed brand ink... intentionally does NOT
     change between themes" beschreibt, gegen einen FESTEN weißen Text
     (.rbe-topbar { color:#fff }). Ein Versuch, --brand-ink hier einen
     Dark-Wert zu geben, riss admin-brand.css's Bindung mit: Topbar-
     Hintergrund gegen den festen weißen Text fiel gemessen von 13,77:1 auf
     1,16:1, siehe PR #14 (JBC-27) fuer die volle Messung. NIE als
     Textfarbe auf --bg/--surface verwenden, im dunklen Theme misst
     --brand-ink dort nur 1,19 bis 1,34:1 (site.css's .site-nav-brand,
     page-home.css/page-ueber-mich.css's Headings dokumentieren dieselbe
     Falle aus der JBC-10-Review). Für Text immer var(--text) oder
     var(--text-2) nutzen, die sind theme-aware. */
  --brand-ink:           #26301F;   /* headline ink, sage-tinted deep neutral   */
  --brand-surface:       #FFFFFF;

  /* --- surfaces ------------------------------------------------------------ */
  --bg:          #FBFBFC;
  --bg-2:        #F2F5F6;
  --surface:     var(--brand-surface);
  --surface-2:   #F7F9FA;
  /* --line is decorative (card dividers, hairlines) and is not held to the 3:1
     rule. --line-strong is the one that bounds a UI component (input, select,
     checkbox) and therefore must meet WCAG 1.4.11 non-text contrast. Using one
     token for both is what quietly makes form fields fail. */
  --line:        #D3DDE1;
  --line-soft:   #E6ECEF;
  --line-strong: #7E8F9A;

  /* --- text ---------------------------------------------------------------- */
  --text:        #14202B;   /* 15.5:1 on --surface  */
  --text-2:      #4A5C68;   /* 6.95:1 on --surface  */
  /* SHR-02: raised from #64757F. That value cleared --surface (4.78:1) and
     --bg (4.62:1) but sat at 4.36:1 on --bg-2, below the 4.5:1 AA floor
     (design-reviewer BLOCKER, PR #166; shop.css's .mv-ext-openall-note still
     carries that history and its own workaround of using --text-2 instead).
     #5E6F79 clears all three: 5.22:1 / 5.04:1 / 4.76:1. */
  --text-3:      #5E6F79;   /* 4.76:1 on --bg-2, the AA floor for body text */

  /* --- accent -------------------------------------------------------------- */
  --accent:      var(--brand-accent);
  --accent-2:    var(--brand-accent-2);
  --accent-text: var(--brand-accent);      /* accent used AS text on a surface */
  /* Brand seed, sixth member: not derived from --brand-accent via var(), so a
     fork rebrand that only overrides the five --brand-* values above leaves
     this hardcoded to teal. A fork MUST override --accent-soft (and its dark
     counterpart below) alongside the --brand-* set, or its badges/pills keep
     the house tint after a rebrand. */
  /* JBC-03: recomputed from --brand-accent #4F5F44 at the same .12 alpha
     the framework default used, composited over --surface per
     resolveHex()'s own compositing rule. --accent-text on this composited
     value measures 5.79:1, comfortably clear of the 4.5:1 pair the gate
     checks ('label on the "+n" gallery overflow tile / verified-review
     badge'). */
  --accent-soft: rgba(79, 95, 68, .12);    /* tinted pill background           */
  --on-accent:   #FFFFFF;                  /* text on a filled accent surface  */
  --panel-a:     #E8EFF1;                  /* image placeholder gradient       */
  --panel-b:     #F5F8F9;

  /* --- secondary button (SHR-02): a solid tint, not --surface-2/--line-strong
     (that pairing is a plain neutral card, not a button). No color-mix(): the
     gate (tools/gate/rules/contrast.mjs) resolves one var() indirection to a
     literal hex, it cannot evaluate a color-mix() expression. Measured:
     --btn-2-text on --btn-2-bg = 9.87:1, --btn-2-line on --bg = 3.43:1.
     Same brand-seed caveat as --accent-soft above: these three are hand-
     picked hex, not derived from --brand-accent via var(), so a fork
     rebrand that only overrides the five --brand-* values leaves the
     secondary button on the house teal tint. A fork MUST re-measure and
     override --btn-2-bg/--btn-2-text/--btn-2-line (light and dark, see the
     matching blocks below) alongside the --brand-* set.
     JBC-03: re-measured for the sage rebrand, --btn-2-text on --btn-2-bg =
     9.24:1, --btn-2-line on --bg = 4.06:1. */
  --btn-2-bg:    #E7EDE3;
  --btn-2-text:  #33402A;
  --btn-2-line:  #6E8259;

  /* Chevron for styled <select> fields. An SVG in a background-image cannot
     inherit currentColor, so the ink is baked into the data URI and must be
     re-declared per theme, or a dark theme gets an invisible dark arrow.
     Ink mirrors --text-2 (#4A5C68): measured 6.95:1 on --surface #FFFFFF at
     rest and 6.25:1 on --error-bg #FDF0EF (aria-invalid), both >= 3:1
     (WCAG 1.4.11). Changing --text-2 does NOT update this URI; keep in sync. */
  --select-arrow: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' viewBox='0 0 16 16' fill='none'%3E%3Cpath d='M4 6l4 4 4-4' stroke='%234A5C68' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");

  /* --- status -------------------------------------------------------------- */
  --error:       #B3261E;  --error-bg:   #FDF0EF;
  --success:     #1F6B4F;  --success-bg: #EDF8F3;
  --warning:     #8A5A00;  --warning-bg: #FFF6E6;
  --info:        #1F5F80;  --info-bg:    #EDF6FA;

  /* --- elevation: tinted, never pure black --------------------------------- */
  --shadow-1: 0 1px 3px rgba(10, 37, 64, .07), 0 1px 2px rgba(10, 37, 64, .05);
  --shadow-2: 0 4px 12px rgba(10, 37, 64, .10), 0 2px 4px rgba(10, 37, 64, .07);
  --shadow-3: 0 8px 24px rgba(10, 37, 64, .14), 0 4px 8px rgba(10, 37, 64, .09);
  --shadow:   var(--shadow-2);

  /* --- geometry + type ----------------------------------------------------- */
  --radius-sm: 6px;  --radius-md: 10px;  --radius-lg: 14px;  --radius: var(--radius-md);
  /* JBC-03, C-08/F-10 (Olli-Entscheid): every <img>/<video> on the site gets
     this rounded-corner treatment (see the global `img, video` rule below),
     so it is its own token, not folded into --radius-sm/md/lg above, which
     bound UI components (buttons, inputs, cards) with a different design
     intent. A fork that wants square media overrides this one value. */
  --radius-media: 9px;
  --maxw: 1200px;
  /* JBC-03: Roboto is the ONLY font christianemayer.de's Elementor Kit sets
     for every text role (see the @font-face block above), so heading and
     body share one family here too. hero.php/cta-band.php (the framework's
     existing --font-heading consumers) set no explicit font-weight of
     their own; their <h1>/<h2> fall back to the browser's UA-default
     "bold", which the 700 @font-face above resolves to a real bold glyph
     instead of a synthetic (faux-bold) one. system-ui stays as the
     fallback chain in case the local woff2 fails to load, not removed
     just because Roboto is now first. */
  --font-heading: "Roboto", system-ui, -apple-system, "Segoe UI", sans-serif;
  --font-body:    "Roboto", system-ui, -apple-system, "Segoe UI", sans-serif;
}

/* --- dark values, applied to both the system preference and the explicit
       choice. Kept in one custom property block via @media + attribute so the
       numbers exist exactly once. ------------------------------------------- */
@media (prefers-color-scheme: dark) {
  :root {
    --bg:        #0E1519;
    --bg-2:      #141E24;
    --surface:   #162128;
    --surface-2: #1D2A32;
    --line:      #2E3F49;
    --line-soft: #24333C;
    --line-strong: #71818B;

    --text:      #F1F5F7;   /* 15.0:1 on --surface */
    --text-2:    #C2CDD4;   /* 9.6:1  on --surface */
    --text-3:    #97A6AF;   /* 5.6:1  on --surface */

    --accent:      var(--brand-accent-dark);
    /* JBC-03: fresh bright sage, not a straight lighten of the light-theme
       accent (see the --brand-* block's own comment for why), brighter
       than --brand-accent-dark for the same "hover intensifies" direction
       the light theme's --brand-accent-2 uses. 10.14:1 on --surface
       #162128 as text (the '.shop-card CTA hover label' pair). */
    --accent-2:    #A9D98F;
    --accent-text: var(--brand-accent-dark);   /* 7.65:1 on --surface */
    /* JBC-03: recomputed from --brand-accent-dark #8FBE79 at the same .16
       alpha, composited over --surface #162128. --accent-text on this
       composited value measures 5.6:1, clear of the 4.5:1 pair the gate
       checks. */
    --accent-soft: rgba(143, 190, 121, .16);
    /* JBC-03: sage-tinted deep ink, not the framework default's near-black
       teal-neutral, for the same brand-cohesion reason as --brand-ink.
       8.03:1 on --brand-accent-dark #8FBE79. */
    --on-accent:   #0C1F14;                    /* dark ink on the bright accent */
    --panel-a:     #1E2C34;
    --panel-b:     #162128;

    /* SHR-02 secondary button, dark theme: text on bg 9.72:1, line on --bg
       3.74:1. Same brand-seed caveat as the light block above: hand-picked
       hex, not derived from --brand-accent-dark, a fork rebrand must
       override this too.
       JBC-03: re-measured for the sage rebrand, --btn-2-text on --btn-2-bg =
       10.03:1, --btn-2-line on --bg #0E1519 = 3.81:1. */
    --btn-2-bg:    #223327;
    --btn-2-text:  #CFE6C2;
    --btn-2-line:  #5C7A4C;

    /* Ink mirrors dark --text-2 (#C2CDD4): measured 10.12:1 on --surface
       #162128 and 10.61:1 on --error-bg #2A1614, both >= 3:1. */
    --select-arrow: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' viewBox='0 0 16 16' fill='none'%3E%3Cpath d='M4 6l4 4 4-4' stroke='%23C2CDD4' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");

    --error:   #F2857D;  --error-bg:   #2A1614;
    --success: #6FD3A8;  --success-bg: #10241C;
    --warning: #E8B45C;  --warning-bg: #2A2013;
    --info:    #7FC3E4;  --info-bg:    #12242E;

    /* Shadows carry almost no weight on dark surfaces; a border does the work. */
    --shadow-1: 0 1px 3px rgba(0, 0, 0, .40);
    --shadow-2: 0 4px 12px rgba(0, 0, 0, .45);
    --shadow-3: 0 8px 24px rgba(0, 0, 0, .55);
  }
}

/* Explicit choice wins over the system preference, in BOTH directions. */
:root[data-theme="dark"] {
  --bg:        #0E1519;
  --bg-2:      #141E24;
  --surface:   #162128;
  --surface-2: #1D2A32;
  --line:      #2E3F49;
  --line-soft: #24333C;
  --line-strong: #71818B;

  --text:      #F1F5F7;
  --text-2:    #C2CDD4;
  --text-3:    #97A6AF;

  --accent:      var(--brand-accent-dark);
  --accent-2:    #A9D98F;
  --accent-text: var(--brand-accent-dark);
  --accent-soft: rgba(143, 190, 121, .16);
  --on-accent:   #0C1F14;
  --panel-a:     #1E2C34;
  --panel-b:     #162128;

  /* Brand-seed caveat, same as --accent-soft above: see the @media dark
     block's --btn-2-* comment for the measured ratios. */
  --btn-2-bg:    #223327;
  --btn-2-text:  #CFE6C2;
  --btn-2-line:  #5C7A4C;

  --select-arrow: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' viewBox='0 0 16 16' fill='none'%3E%3Cpath d='M4 6l4 4 4-4' stroke='%23C2CDD4' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");

  --error:   #F2857D;  --error-bg:   #2A1614;
  --success: #6FD3A8;  --success-bg: #10241C;
  --warning: #E8B45C;  --warning-bg: #2A2013;
  --info:    #7FC3E4;  --info-bg:    #12242E;

  --shadow-1: 0 1px 3px rgba(0, 0, 0, .40);
  --shadow-2: 0 4px 12px rgba(0, 0, 0, .45);
  --shadow-3: 0 8px 24px rgba(0, 0, 0, .55);

  color-scheme: dark;
}

/* A visitor on a dark system who picks light gets the baseline back. */
:root[data-theme="light"] {
  --bg:        #FBFBFC;
  --bg-2:      #F2F5F6;
  --surface:   #FFFFFF;
  --surface-2: #F7F9FA;
  --line:      #D3DDE1;
  --line-soft: #E6ECEF;
  --line-strong: #7E8F9A;

  --text:      #14202B;
  --text-2:    #4A5C68;
  --text-3:    #5E6F79;

  --accent:      var(--brand-accent);
  --accent-2:    var(--brand-accent-2);
  --accent-text: var(--brand-accent);
  --accent-soft: rgba(79, 95, 68, .12);
  --on-accent:   #FFFFFF;
  --panel-a:     #E8EFF1;
  --panel-b:     #F5F8F9;

  /* Brand-seed caveat, same as --accent-soft above: see the :root block's
     --btn-2-* comment for the measured ratios. */
  --btn-2-bg:    #E7EDE3;
  --btn-2-text:  #33402A;
  --btn-2-line:  #6E8259;

  --select-arrow: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' viewBox='0 0 16 16' fill='none'%3E%3Cpath d='M4 6l4 4 4-4' stroke='%234A5C68' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");

  --error:   #B3261E;  --error-bg:   #FDF0EF;
  --success: #1F6B4F;  --success-bg: #EDF8F3;
  --warning: #8A5A00;  --warning-bg: #FFF6E6;
  --info:    #1F5F80;  --info-bg:    #EDF6FA;

  --shadow-1: 0 1px 3px rgba(10, 37, 64, .07), 0 1px 2px rgba(10, 37, 64, .05);
  --shadow-2: 0 4px 12px rgba(10, 37, 64, .10), 0 2px 4px rgba(10, 37, 64, .07);
  --shadow-3: 0 8px 24px rgba(10, 37, 64, .14), 0 4px 8px rgba(10, 37, 64, .09);

  color-scheme: light;
}

/* --- baseline ------------------------------------------------------------- */
body {
  /* margin:0 is a global reset, not shop-specific: the default UA margin
     (~8px each side) pushed the DE shop startseite's scrollWidth to 402px
     against a 390px viewport, a horizontal-overflow bug the shop's own
     .shop-cat-subcat min-width fix (below) could not have caused on its
     own. SHRX-N3, 16.09.2026. */
  margin: 0;
  background: var(--bg);
  color: var(--text);
  font-family: var(--font-body);
  /* Theme swaps are instant on purpose: a colour transition on every element
     costs a repaint of the whole page and buys nothing. */
}

/* Buttons are always solid. A transparent or border-only button reads as a hint,
   not as an action, and fails the 3:1 UI-component contrast rule on most
   backgrounds. Disabled state is opacity, never a colour that blends in.
   SHR-02: inline-flex + centered content, not inline-block. A button's own
   text-centering must not depend on the height a PARENT flex container
   happens to stretch it to (a hero row's `display:flex` with no
   `align-items` set stretches its children to the row's cross-size by
   default, see .claude/rules/framework.md's shorthand-clobber entries for
   the same "the parent's layout leaks into the child" family of bug) --
   `box-sizing:border-box` keeps the declared padding/min-height budget
   exact once the box is centered internally instead of relying on line-height
   alone. `gap` only matters if a button ever carries an icon plus a label. */
.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  box-sizing: border-box;
  gap: 8px;
  padding: 12px 24px;
  min-height: 44px;
  border: 0;
  border-radius: var(--radius-md);
  font: inherit;
  font-weight: 600;
  cursor: pointer;
  text-decoration: none;
  /* Deliberately short and GPU-cheap (filter + transform only, see the
     prefers-reduced-motion block below). A hover/press swap is not the
     "theme swaps are instant on purpose" case above: it happens on every
     interaction, not once per visit, so a felt (not just correct) response
     earns its 150ms. */
  transition: filter 150ms ease, transform 100ms ease;
}
.btn-primary { background: var(--accent); color: var(--on-accent); }
/* SHR-02: was `filter:brightness(1.08)`. shop.css's .shop-btn already used a
   background swap to --accent-2 for its own hover state (and had a
   dedicated contrast test for exactly that pair); .btn-primary now uses the
   same mechanic so the two button systems share one hover behaviour instead
   of two, see tests/contrast.test.mjs's ".btn-primary/.shop-btn hover state"
   test below (moved off .shop-btn onto this shared pairing). */
.btn-primary:hover { background: var(--accent-2); }
.btn-primary:active { filter: brightness(.94); transform: scale(.97); }
/* SHR-02: was --surface-2/--text/--line-strong, a plain neutral card
   pairing indistinguishable from a static panel, not a button. --btn-2-*
   is a solid tint (measured contrast, see the token comments above), so a
   secondary button still reads as an action while staying visually
   subordinate to .btn-primary. */
.btn-secondary { background: var(--btn-2-bg); color: var(--btn-2-text); border: 1px solid var(--btn-2-line); }
.btn-secondary:active { transform: scale(.97); }

/* Form controls bound a UI component, so they take the 3:1 border. */
input, select, textarea {
  min-height: 44px;
  padding: 10px 12px;
  border: 1px solid var(--line-strong);
  border-radius: var(--radius-sm);
  background: var(--surface);
  color: var(--text);
  font: inherit;
}
input[aria-invalid="true"], select[aria-invalid="true"], textarea[aria-invalid="true"] {
  border-color: var(--error);
  background: var(--error-bg);
}
/* Design-review finding (PR shop-c2, the framework's first type="checkbox"
   usage): without this exception a native checkbox inherits the text-field
   rule above (min-height 44px, padding, border, background), rendering as
   an oversized bordered square instead of a small native tick-box. */
input[type="checkbox"], input[type="radio"] {
  min-height: auto;
  width: auto;
  padding: 0;
}
/* Select fields: system chrome off, own chevron, otherwise identical to an
   input. Every engine paints its own arrow at its own position (iOS, Chrome,
   Safari all differ), so the field looks foreign next to the styled inputs
   until appearance is disabled. The OPEN list stays native on purpose:
   <option> is not styleable and a JS replacement costs accessibility and the
   familiar touch behaviour. multiple/size are excluded, they have no arrow.
   Geometry lives here (theme-neutral); only the arrow ink is themed via
   --select-arrow. Focus survives appearance:none because it is the global
   :focus-visible outline below, not a system style. Font size is 16px via
   font:inherit on the body default, so iOS Safari does not zoom on tap.
   padding-right = 13px edge + 16px arrow + 13px gap to the value text. */
select:not([multiple]):not([size]) {
  -webkit-appearance: none;
  -moz-appearance: none;
  appearance: none;
  background-image: var(--select-arrow);
  background-repeat: no-repeat;
  background-position: right 13px center;
  background-size: 16px 16px;
  padding-right: 42px;
}
.btn:disabled, .btn[aria-disabled="true"] { opacity: .5; cursor: not-allowed; }
/* Same footgun this file's own `.btn` rule above creates for itself, and
   that shop.css already fixes three times over for its own classes
   (`.shop-lightbox-slide[hidden]`, `[data-shop-loadmore]>.shop-card[hidden]`,
   `.shop-sticky[hidden]`): `.btn{display:inline-flex}` (SHR-02, was
   `inline-block`) is an equal-or-lower specificity author rule than the
   browser's own `[hidden]{display:none}`
   user-agent rule, so on source order alone `.btn` can win and leave a
   `hidden`-flagged button visible. Confirmed real (R2 review of PR #200):
   site/cart/index.php's `<button class="btn btn-primary" data-mv-cart-
   checkout hidden>` stayed visible in a real Chromium render even with an
   external-only cart, where shop.js's updateCartUI() sets `.hidden = true`
   because there is no checkout for pure partner items. `.btn[hidden]` adds
   an attribute selector, so it always outranks the plain `.btn` class rule
   regardless of source order, the same fix shape as the three shop.css
   cases above. */
.btn[hidden] { display: none; }

/* Safety net for this same bug CLASS (four confirmed instances across
   PDP-03/PDP-05/SAU-K1b/SAU-K5, see the `.btn[hidden]` rule above and
   shop.css's three matching fixes): a fifth author rule that sets `display`
   on a class an element also carries `hidden` on would silently repeat this
   exact bug. This rule is the backstop, not a replacement for the specific
   per-selector fixes above -- keep adding those too, they document WHY a
   given element needs it. `:not([hidden="until-found"])` excludes the
   browser's find-in-page reveal state (unused in this codebase today, cheap
   to exclude in case a future fork adopts it). `!important` is deliberate
   here: this is the one place in the design system whose entire job is to
   out-rank every other author rule, by definition, not a shortcut around
   normal specificity. Loaded on every page that includes this file (see
   shop.css's own copy of this rule for the pages that load shop.css
   without tokens.css, i.e. build-shop.mjs's generated category/product
   pages -- headCommon() links only shop.css, so tokens.css's copy alone
   would never reach those). */
[hidden]:not([hidden="until-found"]) { display: none !important; }

/* Content wrapper for the reference server-rendered pages (danke/, download/,
   freebie/). Same padding rhythm as the blog templates' .bwrap/.art-wrap
   (scripts/build-blog.mjs): 24px horizontal gutter, generous vertical air.
   Added 2026-08-19 (design-audit finding): before this rule .wrap had NO
   definition anywhere, so its content sat flush against the viewport edge
   with only the default 8px body margin, confirmed BLOCKER at 390px and
   1280px alike. A fork narrows this further per page (see freebie/index.php
   .freebie-form) but every .wrap now has a floor gutter even unstyled. */
.wrap { max-width: 640px; margin: 0 auto; padding: 48px 24px 72px; }

/* JBC-03, C-08/F-10 (Olli-Entscheid): every self-hosted photo and video on
   the site gets a soft rounded corner by default via --radius-media, not a
   per-page or per-component rule that would have to be repeated on every
   new page. A component that already sets its own border-radius (shop.css's
   product thumbnails, the AI-disclosure badge below) keeps a more specific
   selector of its own and wins the cascade normally; this bare-element rule
   only fills the gap for markup with no such class (hero photos, inline
   content images, team portraits, the self-hosted <video> component). */
img, video { border-radius: var(--radius-media); }

/* AI Act Art. 50(4) on-image mark (DESIGN.md "KI-Kennzeichnung",
   site_ai_disclosure_badge() / site/partials/ai-disclosure.html). A caption
   placed below the image is not, on its own, "eindeutig zuzuordnen" once a
   page has more than one image nearby; this is the mark that sits ON the
   medium. Wrap the <img>/<picture> in .ai-badge-media so .ai-badge has a
   positioned ancestor to anchor to, without forcing a display type onto
   whatever element the fork already uses for that wrapper (div, figure,
   picture). aria-hidden on the badge itself (set by site_ai_disclosure_badge())
   because the same label is already carried by the alt text and the caption
   paragraph; the badge is the extra layer for sighted users only. 13px is
   the floor, not a default to shrink further (matches the body-text AA
   floor rhythm --text-3 uses above), and the background is tinted rather
   than pure black for the same "never pure black" reason the shadows are. */
.ai-badge-media { position: relative; }
.ai-badge {
  position: absolute;
  top: 8px;
  right: 8px;
  padding: 4px 8px;
  border-radius: var(--radius-sm);
  background: rgba(10, 37, 64, .72);
  color: #fff;
  font-size: 13px;
  line-height: 1.2;
  font-weight: 600;
  pointer-events: none;
}

a { color: var(--accent-text); }

:where(a, button, input, select, textarea, summary, [tabindex]):focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}

@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: .01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: .01ms !important;
    scroll-behavior: auto !important;
  }
}
