/**
 * 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.
 */

:root {
  color-scheme: light dark;

  /* --- brand seed: a fork overrides these five and inherits the rest --------
     SHR-14a (16.09.2026): re-seeded from this fork's own values (git history
     of this file before the Core-Nachzug), not the house teal above. --brand-
     accent is deliberately NOT the exact live button colour (rgb(56,126,153)
     /#387E99): that value is only 4.40:1 as link TEXT on --bg (#FBFBFC), just
     under the 4.5:1 AA floor tests/contrast.test.mjs enforces. This shade
     clears it while staying visually the same teal-blue on a button (white
     text there only needs 3:1). --brand-ink is a near-black, not the house
     navy, matching the live site's flat black headings without going full
     #000. */
  --brand-accent:        #2F6D85;   /* primary action / link colour, light theme */
  --brand-accent-dark:   #6BB3D1;   /* the same idea on a dark surface          */
  --brand-accent-2:      #2C6480;   /* gradient partner                          */
  --brand-ink:           #14181B;   /* headline near-black                       */
  --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. */
  /* SHR-14a: re-derived from this fork's --brand-accent #2F6D85 (rgba of its
     RGB, same .12 alpha the house value uses). */
  --accent-soft: rgba(47, 109, 133, .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.
     SHR-14a: re-measured for this fork's #2F6D85 hue (same HSL saturation/
     lightness the house block above applies to its own accent, only the hue
     shifted). Measured: --btn-2-text on --btn-2-bg = 11.58:1, --btn-2-line
     on --bg #FBFBFC = 4.19:1. */
  --btn-2-bg:    #E1ECF0;
  --btn-2-text:  #083040;
  --btn-2-line:  #4F8093;

  /* 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);
  --maxw: 1200px;
  /* SHR-14a: Roboto first, matches the live site (self-hosted, see
     site/assets/fonts/fonts.css + the woff2 files, both core:once-seeded,
     unaffected by this Core-Nachzug). Not part of the five-value brand seed
     above, a font choice is this fork's own independent of it. */
  --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);
    --accent-2:    #57C7C5;
    --accent-text: var(--brand-accent-dark);   /* 6.6:1 on --surface */
    --accent-soft: rgba(107, 179, 209, .16);
    --on-accent:   #06231F;                    /* 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. */
    --btn-2-bg:    #17303B;
    --btn-2-text:  #BEE1EF;
    --btn-2-line:  #457387;

    /* 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:    #57C7C5;
  --accent-text: var(--brand-accent-dark);
  --accent-soft: rgba(107, 179, 209, .16);
  --on-accent:   #06231F;
  --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:    #17303B;
  --btn-2-text:  #BEE1EF;
  --btn-2-line:  #457387;

  --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(47, 109, 133, .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:    #E1ECF0;
  --btn-2-text:  #083040;
  --btn-2-line:  #4F8093;

  --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 {
  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; }

/* 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;
  }
}
