/* Fahrradcenter App - design foundation.
   Warm editorial palette in line with the Hesslich website, mobile first,
   generous touch targets for an audience that includes many 50+ customers. */

/* the shop's own Futura, the same two files the website serves. Headings only,
   the body uses the phone's system font. See DESIGN-SPEC section 6. */
@font-face {
    font-family: "Futura FCH";
    src: url("/assets/fonts/futura-100-book-400.woff2") format("woff2");
    font-weight: 400;
    font-style: normal;
    font-display: swap;
}
@font-face {
    font-family: "Futura FCH";
    src: url("/assets/fonts/futura-100-700.woff2") format("woff2");
    font-weight: 700;
    font-style: normal;
    font-display: swap;
}

/* Light mode. Colours are taken from the live website fahrradcenter-hesslich.de,
   not invented. See 04-Design/DESIGN-SPEC.md section 5.
   --accent is a FILL only. Gold on a light background fails contrast as text (1.8:1),
   so text and icons use --accent-text instead. */
:root {
    color-scheme: light;
    /* the shop, 29.07.: no background tint, put the depth into the controls instead.
       Page and cards are both white now, cards separate through their border and
       a very quiet lift. */
    --bg: #FFFFFF;
    --surface: #FFFFFF;
    --surface-2: #F7F7F8;
    /* The band under the header on "Meine Räder". Its own token because it has
       to do without a border and therefore needs more
       contrast than --surface-2, which was built to be barely there next to a
       hairline. Measured against --bg: 13 points here, 12 in dark mode, in both
       cases visible as a surface but still quiet. --surface would not do: in
       light mode it is the same white as the page. */
    --band: #F2F2F4;
    /* The first layer of each shadow is a 1px ring, and it replaces the border on
       everything that can be pressed. Measured on a large travel app,
       whose cards carry no border at all: a ring inside the shadow reads as an
       edge without ever drawing a hard line, and it fixes the finding from the
       same day that a plain hairline only reaches 1.29:1 against white.
       Plain surfaces (panels, text blocks) keep their border, they have no depth. */
    --schatten-karte: 0 0 0 1px rgba(17, 17, 20, 0.05), 0 1px 2px rgba(17, 17, 20, 0.04), 0 4px 12px rgba(17, 17, 20, 0.05);
    --schatten-knopf: 0 0 0 1px rgba(17, 17, 20, 0.06), 0 2px 4px rgba(17, 17, 20, 0.14), 0 6px 16px rgba(17, 17, 20, 0.10);
    /* Not pure black. #121214 was harder than any of the apps the shop pointed at,
       they sit around #222. Measured: 16.43:1 against white, was 18.71. Both are
       miles above the 4.5:1 the guidelines ask for, so this costs nothing and
       takes the glare off long texts. */
    --ink: #1F1F23;
    /* One step darker than --ink, for the words that carry a row (we, 06.08.:
       "im Lightmode den Hauptwoertern eine etwas dunklere Farbe"). At weight 400 a
       label looks lighter than it measures, this puts the weight back without
       making the text bold again. */
    --ink-stark: #101014;
    --ink-soft: #63636B;
    /* Pfeil der Auswahlfelder. Als Variable, weil ein Data-URI kein currentColor kennt
       und der Pfeil den Modus trotzdem mitgehen muss. */
    --pfeil-runter: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%2363636B' stroke-width='1.8' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='m7 10 5 5 5-5'/%3E%3C/svg%3E");
    --line: #DCDCE2;
    --line-strong: #C9C9D0;
    /* Kante von BEDIENELEMENTEN, getrennt von --line.
       WCAG 1.4.11 verlangt 3:1 fuer den Rand von Eingabefeldern, Auswahlfeldern
       und Knoepfen; --line liegt gemessen bei 1,37:1 auf Weiss. Eigenes Token,
       weil --line weiter die DEKORATIVEN Haarlinien der Listen und Karten traegt:
       die duerfen leise bleiben, sonst wird die ganze App ein Gitter.
       Gemessen: 3,25:1 auf Weiss, 3,04:1 auf Kartenflaeche. */
    --line-bedien: #8E8E96;
    /* Grown up instead of playful (we, 29.07., pointing at how a large
       retailer does it): the main action is near black, the brand gold appears
       only in small doses. A big gold button always reads a little like a toy.
       the shop's logo is black anyway, so this is closer to his brand, not further. */
    --accent: #17171B;          /* fill for the primary action */
    --accent-ink: #FFFFFF;      /* text on it */
    --accent-text: #6B5410;     /* the brand thread: links, active tab, small marks */
    --gold: #DFB129;            /* the actual brand gold, for bands and marks only */
    --ok-bg: #eaf3ea;
    --ok-line: #c6dfc9;
    --ok-ink: #2f6b46;
    --warn-bg: #fdf6ec;
    --warn-line: #ecd9bd;
    --warn-ink: #8a5a1c;
    --fehler-bg: #fdf3f0;
    --fehler-line: #eccfc5;
    --fehler-ink: #7c3a26;
    --tabbar-bg: rgba(255, 255, 255, 0.92);
    /* Tighter corners read more technical than soft ones (we, 29.07.: less
       radius again). Below 6px it starts to look unfinished rather than sharp. */
    --radius: 8px;
    --radius-gross: 10px;
    --font-mono: ui-monospace, "SF Mono", "Menlo", "Consolas", monospace;
    --pad: clamp(16px, 4vw, 24px);
    --tabbar-h: calc(72px + env(safe-area-inset-bottom, 0px));
    /* The raised card button sticks out 20px above the bar. Content has to keep
       clear of the BUTTON, not just of the bar, otherwise it slides underneath it
       . 8px on top so
       nothing touches. Every screen uses this, never --tabbar-h alone. */
    --tabbar-platz: calc(var(--tabbar-h) + 28px);
    --uci-blau: #006BB6;
    --uci-rot: #CC0923;
    --uci-schwarz: #111111;
    --uci-gelb: #F5D60A;
    --uci-gruen: #008C00;
    /* Colour, taken from the brand instead of invented (we, 06.08.: "es fehlt
       etwas, das Farbe reinbringt"). These are the five UCI stripes from the shop's
       own logo, each as a very pale disc with the strong tone on top. Pale enough
       to stay grown up, present enough that the start screen is not just grey.
       Used behind icons only, never as a large surface. */
    --ton-blau-bg: #E8F1F8;   --ton-blau-ink: #005A99;
    --ton-gruen-bg: #E6F3E6;  --ton-gruen-ink: #00700A;
    --ton-gold-bg: #FBF3DF;   --ton-gold-ink: #6B5410;
    --ton-rot-bg: #FBEAEC;    --ton-rot-ink: #B00A1E;
    --ton-neutral-bg: #F2F2F4;
    /* WhatsApp brand green. The bright #25D366 only reaches 1.98:1 on white, so
       light mode uses their dark green (4.14:1) and dark mode the bright one
       (9.27:1). Both are official WhatsApp colours, measured. */
    --whatsapp: #128C7E;
}

/* Dark mode. A dark grey, not near black: "nicht so ein dunkles
   Schwarz sondern eher ein dunkleres Grau". Measured after the change: body text
   15.52:1, secondary 6.22:1, brand thread 9.98:1, all well above AA.
   The:not clause matters: without it the system setting would override a manual
   choice of "hell". The two blocks are deliberately duplicated, plain CSS cannot
   share a value set between a media query and an attribute selector. */
@media (prefers-color-scheme: dark) {
    :root:not([data-theme="light"]) {
        color-scheme: dark;
        --bg: #1A1A1D;
        --surface: #232327;
        --surface-2: #1E1E22;
        --band: #26262B;
        --ink: #F2F2F3;
        --ink-soft: #9A9AA2;
    /* Pfeil der Auswahlfelder. Als Variable, weil ein Data-URI kein currentColor kennt
       und der Pfeil den Modus trotzdem mitgehen muss. */
    --pfeil-runter: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%239A9AA2' stroke-width='1.8' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='m7 10 5 5 5-5'/%3E%3C/svg%3E");
        --ink-stark: #FFFFFF;
        --line: #38383F;
        --line-strong: #3F3F47;
        /* Gemessen: 3,44:1 auf dem Grund, 3,10:1 auf Flaechen. */
        --line-bedien: #6E6E77;
        /* Inverted on dark: the main action is near white, which reads as the
           most expensive option on a black ground. Gold stays the brand thread. */
        --accent: #F2F2F3;
        --accent-ink: #1A1A1D;
        --accent-text: #E8C04A;
        --ok-bg: #10201a;
        --ok-line: #1f3d31;
        --ok-ink: #6fd39f;
        --warn-bg: #241d10;
        --warn-line: #43361c;
        --warn-ink: #e5bb6b;
        --fehler-bg: #24151a;
        --fehler-line: #452630;
        --fehler-ink: #ef7a7a;
        --tabbar-bg: rgba(26, 26, 29, 0.92);
        --ton-blau-bg: #10202C;   --ton-blau-ink: #6FB6E8;
        --ton-gruen-bg: #102315;  --ton-gruen-ink: #6FCB7A;
        --ton-gold-bg: #26200E;   --ton-gold-ink: #E8C04A;
        --ton-rot-bg: #2A1417;    --ton-rot-ink: #F08A96;
        --ton-neutral-bg: #26262B;
        --whatsapp: #25D366;
        --schatten-karte: 0 0 0 1px rgba(255, 255, 255, 0.07), 0 1px 2px rgba(0, 0, 0, 0.55);
        --schatten-knopf: 0 0 0 1px rgba(255, 255, 255, 0.09), 0 2px 6px rgba(0, 0, 0, 0.6);
        /* The UCI stripe has a black band. On a dark ground it would vanish,
           so it flips to the light tone and keeps the five-band rhythm. */
        --uci-schwarz: #F2F2F3;
    }
}

:root[data-theme="dark"] {
    color-scheme: dark;
    --bg: #1A1A1D;
    --surface: #232327;
    --surface-2: #1E1E22;
    --band: #26262B;
    --ink: #F2F2F3;
    --ink-soft: #9A9AA2;
    /* Pfeil der Auswahlfelder. Als Variable, weil ein Data-URI kein currentColor kennt
       und der Pfeil den Modus trotzdem mitgehen muss. */
    --pfeil-runter: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%239A9AA2' stroke-width='1.8' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='m7 10 5 5 5-5'/%3E%3C/svg%3E");
    --ink-stark: #FFFFFF;
    --line: #38383F;
    --line-strong: #3F3F47;
    --line-bedien: #6E6E77;
    --accent: #F2F2F3;
    --accent-ink: #1A1A1D;
    --accent-text: #E8C04A;
    --ok-bg: #10201a;
    --ok-line: #1f3d31;
    --ok-ink: #6fd39f;
    --warn-bg: #241d10;
    --warn-line: #43361c;
    --warn-ink: #e5bb6b;
    --fehler-bg: #24151a;
    --fehler-line: #452630;
    --fehler-ink: #ef7a7a;
    --tabbar-bg: rgba(26, 26, 29, 0.92);
    --ton-blau-bg: #10202C;   --ton-blau-ink: #6FB6E8;
    --ton-gruen-bg: #102315;  --ton-gruen-ink: #6FCB7A;
    --ton-gold-bg: #26200E;   --ton-gold-ink: #E8C04A;
    --ton-rot-bg: #2A1417;    --ton-rot-ink: #F08A96;
    --ton-neutral-bg: #26262B;
    --whatsapp: #25D366;
    --schatten-karte: 0 0 0 1px rgba(255, 255, 255, 0.07), 0 1px 2px rgba(0, 0, 0, 0.55);
    --schatten-knopf: 0 0 0 1px rgba(255, 255, 255, 0.09), 0 2px 6px rgba(0, 0, 0, 0.6);
    /* Must be repeated here: with a light system setting the media query above
       never applies, and the black UCI band would be invisible. */
    --uci-schwarz: #F2F2F3;
}

@media (prefers-reduced-motion: reduce) {
    *, *::before, *::after {
        animation-duration: 0.01ms !important;
        transition-duration: 0.01ms !important;
    }
}

* { margin: 0; padding: 0; box-sizing: border-box; }

/* The hidden attribute has to win, and this is the one place in the app where
   !important is the right tool rather than a repair.

   The browser's own rule for [hidden] is display:none, but it comes from the
   user agent stylesheet, and ANY rule of ours beats it. Every component that
   sets display:flex therefore silently switched hidden off. Found,
   and it was not theory.install-hinweis is flex, so the cross on the start
   screen banner set hidden and nothing happened, and the banner also showed up
   on the very first visit and inside the installed app, both of which the code
   in app.js explicitly rules out.

   Written once, globally, instead of a [hidden] rule per component: the second
   kind is exactly what produced the bug, because the next component with its own
   display value brings it straight back. */
[hidden] { display: none !important; }

/* There for screen readers, not on screen. Used where a field is explained by
   what stands next to it, so a visible label would only repeat the obvious.
   Not display:none, that would take it out of the accessibility tree too. */
.sr-only {
    position: absolute; width: 1px; height: 1px;
    margin: -1px; padding: 0; border: 0;
    clip-path: inset(50%); overflow: hidden; white-space: nowrap;
}

/* No single word left alone on the last line. The browser does
   this for us: "pretty" pulls a word down from the line above rather than
   leaving an orphan, "balance" evens out the lines of a short heading. Browsers
   that do not know the property simply ignore it, so nothing breaks. */
p, li, dd, dt,
.menue-label, .menue-sub, .sz-label, .kachel-sub, .kachel-titel,
.slide-unter, .leer-text, .flow-lead, .flow-sub, .flow-help,
.panel-sub, .footnote, .banner, .alert, .notice, .ak-hilfe, .karte-hinweis,
.termin-hinweis, .field-hint, .such-meta, .doku-sub, .konto-kopf-sub {
    text-wrap: pretty;
}
h1, h2, h3,
.kopf-titel, .panel-title, .konto-kopf-name, .start-hallo, .hero-title,
.flow-title, .termin-datum, .slide-titel, .ak-titel, .such-gruppe, .dialog-titel {
    text-wrap: balance;
}

/* overscroll-behavior has to sit on the root element: only from there does it
   reach the viewport itself. On body alone the rubber band survives.
   BOTH axes: only -y was set, and on our iPhone the page
   could still be dragged SIDEWAYS a few pixels ("ich kann horizontal
   scrollen") although nothing measurably overflows in Chromium OR WebKit at
   375 and 430. Same rubber-band mechanic as the vertical case on 09.08. */
html { -webkit-text-size-adjust: 100%; overscroll-behavior: none; }

/* Body uses the phone's own font: nothing to download, best legibility at small
   sizes, and it makes the PWA feel native. Futura carries the brand in headings
   only, where its low x-height does not hurt readers over 50. */
body {
    font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
    background: var(--bg);
    color: var(--ink);
    font-size: 17px;
    line-height: 1.55;
    min-height: 100dvh;
    /* Without this iOS lets you drag the page a few pixels even when everything
       fits, and it reads like the screen is hiding something below the fold
       (we on an iPhone 17 Pro Max: "trotzdem kann man minimal
       scrollen"). The rubber band is a browser behaviour, not overflowing
       content, so no amount of shortening the page removes it. Both axes,
       see the html rule above. */
    overscroll-behavior: none;
}

.hero-title,
.pagehead-hello,
.flow-title,
.panel-title,
.flow-step,
.hero-badge {
    font-family: "Futura FCH", "Futura", "Avenir Next", -apple-system, sans-serif;
}

/* The app is exactly one screen tall and never scrolls itself. Only.main does,
   and only when its content is actually taller (the start
   screen could be dragged up and down although everything fit on it).
   Three things follow from this, all of them wanted:
     - the header above cannot move, not even with the rubber band on iOS
     - a screen that fits does not scroll at all
     - contain keeps a scroll inside.main from becoming a page scroll */
.app { height: 100dvh; display: flex; flex-direction: column; overflow: hidden; }

.main {
    flex: 1;
    width: 100%;
    max-width: 560px;
    margin: 0 auto;
    /* No padding on top. A scroll container counts its top padding as part of
       the scrollport, so a sticky bar with top: 0 comes to rest BELOW that
       padding and content slides through the strip above it. Measured: the ad
       banner showed through a 16px band over the search bar. The air moves to
       the first child, and the search bar carries it as its own padding in the
       page colour, so nothing can pass over it. */
    padding: 0 var(--pad);
    /* Only enough for the raised card button, which reaches up out of the bar.
       The bar itself is a row of its own now and needs no reserve. */
    padding-bottom: 26px;
    overflow-y: auto;
    overscroll-behavior: contain;
}
.main > :first-child { margin-top: var(--pad); }
.main--unter-suche > :first-child { margin-top: 0; }
.main--full { padding-bottom: var(--pad); }

/* ===== Welcome / hero ===== */
.hero { padding-top: clamp(24px, 8vh, 72px); }
.hero-badge {
    display: inline-block;
    font-size: 12px;
    letter-spacing: 0.16em;
    text-transform: uppercase;
    color: var(--accent-text);
    border: 1px solid var(--line);
    background: var(--surface);
    border-radius: 100px;
    padding: 6px 14px;
    margin-bottom: 18px;
}
.hero-title {
    font-size: clamp(30px, 8vw, 42px);
    line-height: 1.12;
    font-weight: 500;
    letter-spacing: -0.02em;
    margin-bottom: 12px;
}
.hero-sub { color: var(--ink-soft); max-width: 42ch; margin-bottom: 28px; }

.hero-actions { display: flex; flex-direction: column; gap: 12px; margin-bottom: 32px; }
.hero-hinweis { color: var(--ink-soft); font-size: 14px; text-align: center; max-width: 40ch; margin: 0 auto; }

.btn {
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: 2px;
    min-height: 56px;
    padding: 12px 20px;
    border-radius: var(--radius);
    border: 1px solid transparent;
    font: inherit;
    font-weight: 500;
    text-decoration: none;
    cursor: pointer;
    transition: transform 0.12s ease, box-shadow 0.12s ease;
}
.btn:active { transform: scale(0.98); }
/* Depth lives on controls only, never on plain surfaces.
   That keeps the app quiet and makes "you can press this" unmistakable, which
   matters most for the older half of the customers. */
.btn--primary {
    background: var(--accent); color: var(--accent-ink);
    box-shadow: var(--schatten-knopf);
}
.btn--primary:active { box-shadow: none; }
.btn--ghost { background: var(--surface); color: var(--ink); border-color: var(--line-bedien); }
/* Third button level from the catalogue: reads as a link, but keeps a full
   48px target. A bare inline link gave "Passwort vergessen" about 20px, which
   fails DESIGN-SPEC section 12. */
.btn--text {
    background: none; border-color: transparent;
    color: var(--accent-text); min-height: 48px; font-weight: 500;
}
.btn[disabled] { opacity: 0.55; cursor: default; }
.btn-note { font-size: 12px; font-weight: 400; opacity: 0.8; }

.hero-features { list-style: none; display: grid; gap: 10px; color: var(--ink-soft); }
.hero-features li {
    background: var(--surface);
    border: 1px solid var(--line);
    border-radius: var(--radius);
    padding: 12px 16px;
}

/* --- App header: profile picture, greeting, contact ---------------------
   Same arrangement both apps the shop pointed at use: identity on the left,
   an action on the right, the search field underneath. */
/* Sits above the scrolling area, not inside it (it has to be
   really fixed and must not move by a few pixels either). It is a flex item of
   .app now, so it is not scrollable at all rather than merely sticky. That is
   the difference he noticed: a sticky row still rides along with the rubber
   band, because the page underneath it was the thing scrolling.
   Since it left.main it has to bring that container's width and padding along
   itself. */
.app-kopf {
    flex: none;
    display: flex; align-items: center; gap: 12px;
    width: 100%; max-width: 560px; margin: 0 auto;
    padding: 6px var(--pad) 14px;
    background: var(--bg);
    position: relative; z-index: 20;
}
/* 48, not 44: the spec asks for 48px and these three are on every single screen.
   The visible circle stays 44px, the extra four pixels are pure tap area. */
.app-kopf-profil, .app-kopf-aktion {
    display: flex; align-items: center; justify-content: center;
    min-width: 48px; min-height: 48px; text-decoration: none; color: var(--ink-soft);
}
.app-kopf-mitte a {
    display: flex; align-items: center; justify-content: center; min-height: 48px;
}
.app-kopf-aktion {
    border: 1px solid var(--line); border-radius: 50%;
    background: var(--surface); box-shadow: var(--schatten-karte);
}
/* In WhatsApp's own green when the button really opens WhatsApp.
   Selected by the target, so a shop without a WhatsApp number keeps a neutral
   phone button instead of a green one that lies about where it leads. */
.app-kopf-aktion[href*="wa.me"] { color: var(--whatsapp); }
.app-kopf-mitte { flex: 1; min-width: 0; display: flex; justify-content: center; }
/* Without a contact action the logo would drift off centre, so the empty slot
   keeps its width. */
.app-kopf-luecke { width: 44px; flex: none; }
/* The page title sits right under the header, it does not need its own top air. */
.app-kopf + .kopf { margin-top: -2px; }
/* the shop's mark carries the header, the greeting moves below the search where it
   belongs to the content rather than competing with the brand. */
/* A touch bigger: the mark is the only branding on the screen.
   Held at 54px because at 375x812 the start screen still has to fit without
   scrolling, measured. */
.kopf-logo { display: block; width: 54px; height: auto; }
.start-gruss {
    font-family: "Futura FCH", "Futura", "Avenir Next", sans-serif;
    font-size: 24px; font-weight: 400; text-align: center; margin-bottom: 16px;
}

.avatar {
    display: flex; align-items: center; justify-content: center;
    width: 44px; height: 44px; border-radius: 50%; overflow: hidden;
    background: var(--surface-2); border: 1px solid var(--line);
    font-weight: 500; font-size: 15px; letter-spacing: 0.03em; color: var(--ink-soft);
}
.avatar img { width: 100%; height: 100%; object-fit: cover; display: block; }
.avatar--72 { width: 72px; height: 72px; font-size: 22px; flex: none; }

.profil-zeile { display: flex; align-items: center; gap: 16px; }
.profil-form { display: flex; flex-direction: column; gap: 8px; flex: 1; min-width: 0; }
.profil-form .btn { min-height: 48px; }
/* The file input itself is hidden inside the label, which is the only way to
   give it the same look and the same 48px target as every other button. */
.profil-waehlen { flex-direction: row; gap: 8px; position: relative; overflow: hidden; }
.profil-waehlen input[type="file"] {
    position: absolute; inset: 0; opacity: 0; cursor: pointer;
    font-size: 0; width: 100%; height: 100%;
}
.profil-entfernen { display: flex; justify-content: center; margin-top: 10px; }

/* Open list of options instead of a dropdown (the shop, for the leasing
   providers). Sixteen entries plus "Kein Leasing", so every row is trimmed to
   what it needs: 44px is the tap target, the hairline does the separating, and
   there is no card around it. The rows sit on the panel, not in a second box.
   The whole list is a field, so it carries the same recessed surface as an input
   and the same rounded edge. */
.wahl { border: 0; padding: 0; margin: 0; min-width: 0; }
/* The head of the list. Reads like a field label with its answer next to it, and
   folds the sixteen providers away once one of them is chosen. */
.wahl-kopf {
    display: flex; align-items: center; gap: 10px;
    min-height: 44px; padding: 4px 2px;
    cursor: pointer; list-style: none;
}
.wahl-kopf::-webkit-details-marker { display: none; }
.wahl-kopf .field-label { flex: none; }
/* An insurance number may be up to 40 characters. It gets whatever room is left
   and ends in an ellipsis rather than pushing the arrow off the screen. */
.wahl-wert {
    margin-left: auto; color: var(--ink);
    min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.wahl + .wahl { margin-top: 4px; }
/* Nothing chosen yet: the answer would only repeat the first line of the open
   list below it. */
.wahl[open] .wahl-wert { display: none; }
.wahl[open] .wahl-kopf { border-bottom: 1px solid var(--line); }
.wahl[open] .ak-pfeil { transform: rotate(180deg); }
/* The open radio list that used to live here (.wahl-liste/.wahl-zeile) became
   the picker drum; its no-JS base below carries the same row
   grammar. Only the Versicherungsnummer fold still uses.wahl itself. */

/* The picker drum.
   Replaces the fold-out list for leasing and insurer. Always compact: three
   44px rows are visible, the middle one is the answer, everything else fades
   out through the mask. scroll-snap does the detent, a script checks whatever
   lands in the middle. No tick sound and no vibration: iOS browsers expose
   neither, and the shop said the tick is optional anyway.

   Everything drum-shaped hangs on html.js (set inline in the layout head).
   WITHOUT the script the same markup renders as the flat radio list the screen
   had before: only the script can centre the stored value, and a drum without
   centring would present the first row as the answer. A plain list that tells
   the truth beats a drum that lies. */
.walze-feld { display: flex; flex-direction: column; gap: 2px; }
.walze-feld + .walze-feld { margin-top: 10px; }
.wahl-walze { position: relative; }
/* No-JS base: ordinary rows with a visible radio, same grammar as.wahl-zeile. */
.walze-zeile {
    display: flex; align-items: center; gap: 12px;
    min-height: 44px; padding: 4px 2px;
    border-bottom: 1px solid var(--line);
    cursor: pointer;
}
.walze-zeile:last-child { border-bottom: 0; }
.walze-zeile input[type="radio"] { flex: none; width: 20px; height: 20px; margin: 0; accent-color: var(--ink); }
.walze-zeile span { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.walze-zeile:has(input:checked) { font-weight: 500; }
.walze-zeile:has(input:focus-visible) { outline: 2px solid var(--ink); outline-offset: -2px; }

/* With the script: the drum. The lens sits on the wrapper, not in the
   scroller, so it stays put while the rows move underneath it. The 44px
   padding inside the track lets the first and last entry reach the middle.
   The mask floors at 55 % alpha instead of fading to nothing: the edge rows
   are real tap targets and have to stay legible (Kontrast-Befund 13.08.). */
html.js .wahl-walze::before {
    content: ''; position: absolute; left: 0; right: 0; top: 50%; height: 44px;
    transform: translateY(-50%);
    background: color-mix(in srgb, var(--ink) 7%, transparent);
    border-radius: 12px; pointer-events: none;
}
/* Five to seven rows visible like the iOS clock ("auch
   wenn das Fenster ein bisschen größer wird"). The perspective makes the
   per-row transforms from the script read as a real cylinder. Deliberately NO
   mask here: the fading is done per row by the same script that bends the
   drum (a mask is a grouping property and WebKit may flatten the child 3D
   transforms it contains, which would turn the drum back into a flat list on
   exactly the iPhones it was built for). */
html.js .walze-spur {
    position: relative;
    height: 220px; padding: 88px 0;
    overflow-y: auto; overscroll-behavior: contain;
    scroll-snap-type: y mandatory;
    perspective: 900px;
    scrollbar-width: none;
}
html.js .walze-spur::-webkit-scrollbar { display: none; }
html.js .walze-zeile {
    height: 44px; padding: 0; border-bottom: 0;
    justify-content: center;
    scroll-snap-align: center;
    color: var(--ink-soft);
    /* A drum is grabbed and dragged; without this the browser starts SELECTING
       the row text instead. */
    user-select: none; -webkit-user-select: none;
    -webkit-tap-highlight-color: transparent;
}
html.js .walze-zeile span { padding: 0 12px; }
/* On the drum the middle row is the marker, the radio dot would only repeat it. */
html.js .walze-zeile input[type="radio"] {
    position: absolute; width: 1px; height: 1px;
    clip-path: inset(50%); overflow: hidden;
}
html.js .walze-zeile:has(input:checked) { color: var(--ink-stark); font-weight: 600; }
html.js .walze-zeile:has(input:focus-visible) { outline: 0; }
html.js .wahl-walze:has(input:focus-visible) { outline: 2px solid var(--ink); outline-offset: 2px; border-radius: 12px; }
/* The saving status under the drums: the wheels save
   themselves like the iPhone clock, this line is the receipt. Created by the
   script, so without JavaScript the form keeps its button and needs no line. */
.speicher-status { font-size: 14px; color: var(--ink-soft); min-height: 20px; margin: 0; text-align: center; }
.speicher-status--fehler { color: var(--fehler-ink); }

/* Notes on a bike. A list of short lines, each with its own
   way out. Deliberately quiet: this sits inside an opened bike, which is already
   the loudest thing on the screen. */
.notizen { margin-top: 18px; }
.notizen-titel { font-size: 14px; color: var(--ink-soft); margin-bottom: 8px; }
.notizen-liste { list-style: none; margin: 0 0 12px; padding: 0; }
.notiz {
    display: flex; align-items: center; gap: 10px;
    min-height: 40px; padding: 6px 0;
    border-bottom: 1px solid var(--line);
}
.notiz:last-child { border-bottom: 0; }
.notiz-text { flex: 1; min-width: 0; overflow-wrap: anywhere; }
.notiz-weg { flex: none; display: flex; }
/* 44px target around an 18px cross: the cross may be small, the tap must not be. */
.notiz-loeschen {
    display: flex; align-items: center; justify-content: center;
    width: 44px; height: 44px; margin-right: -12px;
    background: none; border: 0; padding: 0; cursor: pointer;
    color: var(--ink-soft);
}
.notiz-loeschen:hover { color: var(--ink); }
/* Notepad above, button across the full width under it, exactly like every other
   button in the app. Both on one line was the first attempt
   and measured 146px of typing room on a 375px phone, for entries like
   "Schwalbe Marathon Plus". */
.notiz-neu { display: flex; flex-direction: column; gap: 12px; }
.notiz-neu .field { margin: 0; }
/* The explanation under the button needs air, it was sitting right on it
   . */
.notizen > .field-hint { display: block; margin-top: 16px; }

/* Section picker for the profile picture. The frame is square, the
   dark ring on top shows the round cut that the avatar will actually use, so
   what you drag into the circle is exactly what you get. Everything outside it
   stays visible but dimmed: you can see what you are leaving out.
   The canvas carries the picture, hence touch-action none, otherwise the phone
   scrolls the page instead of moving the image. */
.zuschnitt { margin-bottom: 20px; display: flex; flex-direction: column; align-items: center; }
.zuschnitt-rahmen {
    position: relative; width: min(240px, 68vw); aspect-ratio: 1;
    border-radius: var(--radius-gross); overflow: hidden;
    background: var(--surface-2); border: 1px solid var(--line);
}
.zuschnitt-flaeche {
    display: block; width: 100%; height: 100%;
    touch-action: none; cursor: grab;
}
.zuschnitt-flaeche:active { cursor: grabbing; }
.zuschnitt-flaeche:focus-visible { outline: 2px solid var(--ink); outline-offset: -2px; }
.zuschnitt-rahmen::after {
    content: ""; position: absolute; inset: 0; pointer-events: none;
    border-radius: inherit;
    /* The ring: everything outside the circle goes half dark. One shadow instead
       of an overlay element, so nothing can ever land on top of the canvas and
       swallow a drag. */
    box-shadow: inset 0 0 0 2000px rgba(0, 0, 0, 0.42);
    /* closest-side, so the circle is the one inscribed in the square. With the
       default farthest-corner the ray runs to the corner and the cut-out would
       come out too big. */
    mask: radial-gradient(circle closest-side at 50% 50%, transparent 0 99%, #000 100%);
    -webkit-mask: radial-gradient(circle closest-side at 50% 50%, transparent 0 99%, #000 100%);
}
.zuschnitt-hinweis {
    margin: 12px 0 0; font-size: 14px; color: var(--ink-soft); text-align: center;
}
.zuschnitt-zoom { display: flex; align-items: center; gap: 10px; width: min(240px, 68vw); margin-top: 10px; }
.zuschnitt-zoom-icon { display: flex; color: var(--ink-soft); flex: none; }
.zuschnitt-zoom input[type="range"] { flex: 1; min-width: 0; height: 44px; accent-color: var(--ink); }
.profil-anders { display: flex; justify-content: center; margin-top: 10px; }

/* Account footer: sign-out, then the customer number as quiet reference. */
.konto-abschluss { margin-top: 26px; display: flex; flex-direction: column; gap: 12px; }
.konto-meta { text-align: center; color: var(--ink-soft); font-size: 14px; }

/* --- Search ------------------------------------------------------------- */
/* Sticks right under the header. The wrapper carries the page colour and the
   gap below the bar, so nothing scrolls through between the two. */
/* Sticks to the top of the scrolling area, so directly under the header.
   It reaches across the FULL width, not just to the page margin: the banner
   underneath bleeds to both screen edges, and anything narrower left two
   slivers of banner showing beside the bar while it scrolled past
   . The inner padding puts the bar itself back where it was,
   10px past the page margin on each side. */
/* A row of.app, not part of the page. Sticky held its position while
   scrolling but still came along with the rubber band at the top; out here
   nothing can move it. Same width rules as.main so the bar lines up with the
   content on a wide screen. */
.such-fix {
    flex: none;
    width: 100%; max-width: 560px; margin: 0 auto;
    background: var(--bg);
    padding: 0 calc(var(--pad) - 10px) 14px;
    position: relative; z-index: 15;
}
/* The gap below now belongs to the wrapper, otherwise it would count twice. */
.such-fix .suchleiste { margin: 0; }
.suchleiste {
    display: flex; align-items: center; gap: 10px;
    min-height: 58px; padding: 0 18px; margin-bottom: 22px;
    background: var(--surface); border: 1px solid var(--line-bedien);
    border-radius: var(--radius-gross); box-shadow: var(--schatten-karte);
}
.suchleiste-icon { display: flex; color: var(--ink-soft); flex: none; }
.suchleiste input {
    flex: 1; min-width: 0; border: none; background: none; font: inherit;
    font-size: 16px; color: var(--ink); padding: 14px 0;
}
.suchleiste input:focus { outline: none; }
.suchleiste:focus-within { border-color: var(--accent); }
.suchleiste input::-webkit-search-cancel-button { -webkit-appearance: none; }
.suchleiste--seite { margin-bottom: 22px; }

.such-gruppe {
    font-family: "Futura FCH", "Futura", "Avenir Next", sans-serif;
    font-size: 17px; margin: 22px 0 10px; text-align: center;
}
.such-zeile {
    display: flex; align-items: center; gap: 12px;
    min-height: 64px; padding: 12px 16px; text-decoration: none; color: inherit;
    background: var(--surface); border: 1px solid var(--line);
    border-radius: var(--radius); box-shadow: var(--schatten-karte);
}
.such-icon { display: flex; flex: none; color: var(--accent-text); }
.such-text { display: flex; flex-direction: column; min-width: 0; flex: 1; }
.such-titel { font-weight: 500; overflow-wrap: anywhere; }
.such-meta { color: var(--ink-soft); font-size: 14px; }

/* ===== Start page =====
   Centred and quiet: one accent per screen, no shadows, no floating elements.
   DESIGN-SPEC principles 6 and 7. */
.start-kopf { display: flex; flex-direction: column; align-items: center; padding: 10px 0 24px; }
.start-kopf .uci-band { margin-bottom: 14px; }
.start-hallo {
    font-family: "Futura FCH", "Futura", "Avenir Next", sans-serif;
    font-size: clamp(28px, 7.5vw, 34px);
    font-weight: 400;
    letter-spacing: 0.01em;
    text-align: center;
}

.termin { text-align: center; margin-bottom: 14px; }
.termin-hinweis { color: var(--ink-soft); font-size: 15px; margin-top: 6px; }

.banner--link { display: block; text-align: center; text-decoration: none; }

/* Tiles: two per row, content centred inside each. */
.kacheln { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 14px; }
.kachel {
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: 4px;
    min-height: 152px;
    padding: 22px 14px;
    text-align: center;
    text-decoration: none;
    color: inherit;
    background: var(--surface);
    border: 1px solid var(--line);
    border-radius: var(--radius-gross);
    box-shadow: var(--schatten-karte);
    transition: border-color 0.2s ease, transform 0.12s ease, box-shadow 0.12s ease;
}
.kachel:active { transform: scale(0.99); box-shadow: none; }
/* The icon sits on a pale disc in one of the five logo colours. That is where the
   colour in this app comes from: not from large surfaces, which would look
   childish, but from four small discs on the one screen everybody sees first. */
.kachel-icon {
    display: flex; align-items: center; justify-content: center;
    width: 46px; height: 46px; border-radius: 50%;
    background: var(--ton-neutral-bg); color: var(--ink-soft);
    margin-bottom: 10px;
}
.kachel--blau .kachel-icon  { background: var(--ton-blau-bg);  color: var(--ton-blau-ink); }
.kachel--gruen .kachel-icon { background: var(--ton-gruen-bg); color: var(--ton-gruen-ink); }
.kachel--gold .kachel-icon  { background: var(--ton-gold-bg);  color: var(--ton-gold-ink); }
.kachel--rot .kachel-icon   { background: var(--ton-rot-bg);   color: var(--ton-rot-ink); }
.kachel-titel { font-weight: 500; font-size: 16px; }
.kachel-sub { color: var(--ink-soft); font-size: 14px; line-height: 1.35; }

.icon { display: block; }

/* --- Slider: the shop's own worlds -------------------------------------
   Full width, snaps, bleeds to both screen edges. Text sits on the picture
   behind a dark scrim, so it stays readable on every photo. */
/* Air above it: the slider is a separate chapter, not a footnote to the tiles. */
.slider { margin: 30px calc(var(--pad) * -1) 22px; }
/* Edge to edge: one slide is exactly one screen, no
   padding beside it and no gap between two of them. It used to be a card at 88 %
   with the next one peeking in, which left a strip of page on both sides.
   The gap has to go with it: at full width it would show as a white seam while
   the finger is still dragging. */
.slider-spur {
    display: flex; gap: 0;
    overflow-x: auto; scroll-snap-type: x mandatory;
    padding: 0; scroll-padding: 0;
    -webkit-overflow-scrolling: touch; scrollbar-width: none;
}
.slider-spur::-webkit-scrollbar { display: none; }
.slide {
    position: relative; flex: 0 0 100%; scroll-snap-align: center;
    display: block; aspect-ratio: 16 / 9;
    /* Small radius, no shadow: a lift only makes sense on something with page
       around it, and at the screen edge it would have nowhere to fall. */
    border-radius: var(--radius); overflow: hidden;
    text-decoration: none; color: #fff; background: var(--surface-2);
}
.slide-bild { width: 100%; height: 100%; object-fit: cover; display: block; }

/* No photo: a white card carrying the mark. One vertical stack in normal flow,
   nothing positioned on top of anything else, so the words can never slide over
   the logo or the rainbow again. */
/* Follows the display mode since 06.08. As a card at 88 % width a white panel
   was fine, across the full width it looked like a hole in a dark screen. */
/* A quiet outline so the white card does not melt into the white page (we,
   06.08.). Inside the shadow rather than as a border, that way the photo slides
   next to it keep exactly the same size. */
.slide--marke {
    background: var(--surface); border: 0;
    box-shadow: inset 0 0 0 1px var(--line-strong);
}
.slide-marke {
    position: absolute; inset: 0;
    display: flex; flex-direction: column; align-items: center; justify-content: center;
    gap: 8px; padding: 18px 22px; text-align: center;
}
.slide-logo { width: 54px; height: auto; margin-bottom: 2px; }
.slide-marke .uci-band { width: 64px; margin-bottom: 6px; }
.slide-marke .slide-titel { color: var(--ink); }
.slide-marke .slide-unter { color: var(--ink-soft); max-width: 30ch; }

/* Promotions the shop creates himself. Gold marks it as an offer at a glance, and
   the dark gold is the one that actually holds contrast on white. */
.slide-marker {
    font-family: "Futura FCH", "Futura", "Avenir Next", sans-serif;
    font-size: 12px; letter-spacing: 0.2em; text-transform: uppercase;
    color: var(--accent-text); background: rgba(223, 177, 41, 0.16);
    border: 1px solid rgba(223, 177, 41, 0.55);
    border-radius: 999px; padding: 6px 16px; margin-bottom: 2px;
}
.slide--aktion .slide-marke .slide-titel { color: var(--accent-text); }

.slide-text {
    position: absolute; left: 0; right: 0; bottom: 0;
    display: flex; flex-direction: column; gap: 3px; padding: 44px 16px 15px;
    background: linear-gradient(to top,
        rgba(0, 0, 0, 0.82) 0%, rgba(0, 0, 0, 0.55) 45%, rgba(0, 0, 0, 0) 100%);
}
.slide-titel {
    font-family: "Futura FCH", "Futura", "Avenir Next", sans-serif;
    font-size: 20px; font-weight: 400; letter-spacing: 0.01em;
}
.slide-unter { font-size: 14px; line-height: 1.35; color: rgba(255, 255, 255, 0.85); }

.slider-punkte { display: flex; justify-content: center; gap: 6px; margin-top: 10px; }
.slider-punkt {
    width: 6px; height: 6px; border-radius: 50%;
    background: var(--line-strong); transition: width 0.2s ease, background 0.2s ease;
}
.slider-punkt--aktiv { width: 18px; border-radius: 3px; background: var(--accent-text); }

/* Technical treatment of anything numeric: same-width digits so columns line
   up, monospace for identifiers so a customer number reads as a code, not prose. */
.kachel-sub, .card-betrag, .datenzeile dd, .termin-datum, .barcode-nummer {
    font-variant-numeric: tabular-nums;
}
.nummer {
    font-family: var(--font-mono);
    font-size: 0.94em;
    letter-spacing: 0.06em;
}

/* Legacy start-page classes, still used by other screens until phase 1 */
.pagehead { padding: 8px 0 20px; }
.pagehead-hello { font-size: clamp(26px, 7vw, 32px); font-weight: 500; letter-spacing: -0.02em; }
.pagehead-sub { color: var(--ink-soft); }

.cards { display: grid; gap: 12px; }
.card {
    display: flex;
    align-items: center;
    gap: 14px;
    background: var(--surface);
    border: 1px solid var(--line);
    border-radius: var(--radius);
    padding: 16px;
    text-decoration: none;
    color: inherit;
    min-height: 76px;
    transition: transform 0.12s ease;
}
.card:active { transform: scale(0.99); }
.card-icon { font-size: 28px; }
.card-title { font-weight: 500; }
.card-sub { color: var(--ink-soft); font-size: 14px; }
.card-badge {
    margin-left: auto;
    font-size: 11px;
    letter-spacing: 0.08em;
    text-transform: uppercase;
    color: var(--accent-text);
    border: 1px solid var(--line);
    border-radius: 100px;
    padding: 4px 10px;
    white-space: nowrap;
}

.footnote { margin-top: 24px; color: var(--ink-soft); font-size: 14px; text-align: center; }
.footnote a { color: var(--accent-text); }

/* ===== Tab bar ===== */
/* In the flow, not floating over the page any more. The app is
   exactly one screen tall and.main is the only thing that scrolls, so the bar
   simply is the last row of that column. Nothing can slide under it, and.main
   no longer has to reserve 100px of empty space for something that is not there.
   That reserve was most of the reason the start screen scrolled although it
   looked as if everything fit. */
.tabbar {
    flex: none;
    height: var(--tabbar-h);
    padding-bottom: env(safe-area-inset-bottom, 0px);
    background: var(--tabbar-bg);
    -webkit-backdrop-filter: blur(12px);
    backdrop-filter: blur(12px);
    border-top: 1px solid var(--line);
    display: flex;
    justify-content: space-around;
    align-items: stretch;
    /* The bar floats over a white page now, so it needs its own edge. */
    box-shadow: 0 -1px 3px rgba(17, 17, 20, 0.05);
    overflow: visible;
}
.tab {
    flex: 1;
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: 5px;
    font-size: 11px;
    letter-spacing: 0.02em;
    text-decoration: none;
    color: var(--ink-soft);
}
.tab-icon { position: relative; display: flex; align-items: center; justify-content: center; height: 24px; }
/* Count badge on the icon. Sits half over the icon's top
   right corner, the way a phone does it. Colours are the button pairing, so the
   contrast is the one already checked; a red dot would read as an error in an
   app where red means exactly that. */
.tab-punkt {
    position: absolute; top: -6px; left: 50%; margin-left: 4px;
    min-width: 18px; height: 18px; padding: 0 5px;
    display: flex; align-items: center; justify-content: center;
    border-radius: 9px;
    background: var(--accent); color: var(--accent-ink);
    font-size: 11px; font-weight: 500; line-height: 1;
    font-variant-numeric: tabular-nums;
    /* A ring in the bar's own colour, so the badge stays legible when it lands
       on the icon's strokes. */
    box-shadow: 0 0 0 2px var(--tabbar-bg);
}
/* Tab labels. "Meine Räder" is set over two lines on purpose (we,
   ), it would otherwise sit with 3px to spare on a 360px screen.
   Every label reserves the same two lines, so the icons above them stay on one
   line across all five tabs even though only one label uses both rows. */
.tab > span:last-child {
    text-align: center;
    line-height: 1.2;
    min-height: 2.4em;
    /* Narrow enough that "Meine Räder" has to break at its space and sit on two
       rows, which is what for and what keeps it from crowding its
       neighbours. A long single word hyphenates instead of overflowing. */
    max-width: 54px;
    overflow-wrap: normal;
    /* Gleiche Begruendung wie bei.sz-label: nur an der Wortfuge trennen, die
       im Sprachtext als weiches Trennzeichen steht. */
    hyphens: manual;
}
.tab--active { color: var(--accent-text); font-weight: 500; }
.tab--soon { opacity: 0.45; }

/* Raised customer card in the middle of the bar. It keeps its own colour
   whether active or not: it is a thing, not a place. */
/* 12, not 6: measured 06.08. the card label sat 6px lower than the other four,
   which made it look clipped at the bottom edge and the whole bar look uneven.
   All five baselines line up now. */
.tab--karte { justify-content: flex-end; padding-bottom: 12px; }
.tab-karte-knopf {
    display: flex; align-items: center; justify-content: center;
    /* The bar grew from 64 to 72px and the stack sits bottom aligned, so the
       negative margin has to grow with it, otherwise the button sinks into the
       bar and loses its point. */
    width: 60px; height: 60px; margin-top: -34px; margin-bottom: 4px;
    border-radius: 50%;
    /* Quiet surface, not a saturated disc. It works through being raised, not
       through colour, which is what keeps the bar looking grown up. */
    background: var(--surface-2);
    color: var(--ink);
    border: 1px solid var(--line);
    box-shadow: 0 -2px 6px rgba(17, 17, 20, 0.06), var(--schatten-karte);
    transition: transform 0.12s ease;
}
.tab--karte:active .tab-karte-knopf { transform: scale(0.94); }
.tab--karte.tab--active { color: var(--accent-text); font-weight: 500; }

/* ===== Panels (account page) ===== */
.panel {
    background: var(--surface);
    border: 1px solid var(--line);
    border-radius: var(--radius-gross);
    padding: 22px 20px;
    margin-bottom: 18px;
}
/* The tinted variant is not defined here: the
   NO BOXES block further down would undo it. It lives at the end of that block,
   together with the reason it is allowed to be a box at all. */
.panel-title { font-size: 19px; letter-spacing: -0.01em; margin-bottom: 4px; }
.panel-sub { color: var(--ink-soft); font-size: 14px; margin-bottom: 14px; }
.field-row { display: flex; gap: 10px; }
.field--plz { flex: 0 0 110px; }
.field--grow { flex: 1; }
.field-optional { font-weight: 400; color: var(--ink-soft); }
.notice {
    background: var(--ok-bg);
    border: 1px solid var(--ok-line);
    color: var(--ok-ink);
    border-radius: var(--radius);
    padding: 12px 16px;
    font-size: 15px;
    margin-bottom: 14px;
}

/* ===== Bike detail ===== */
.pagehead-hello--klein { font-size: clamp(22px, 6vw, 27px); margin-top: 6px; }
.datenliste { display: grid; gap: 0; }
/* min-width: 0 because the rows are grid items: without it one over-wide row
   (label plus checking pill) forces the whole track past the card and every
   value in every row gets clipped at the right edge (the shop's iPhone,
   ). */
.datenzeile { display: flex; justify-content: space-between; gap: 14px; padding: 10px 0; border-bottom: 1px solid var(--line); min-width: 0; }
.datenzeile:last-child { border-bottom: none; }
.datenzeile dt {
    color: var(--ink-soft); font-size: 15px;
    min-width: 0;
}
/* Ascend delivers glued article names like "Schwarzherren277x182mmmoderate465g".
   Such a word cannot break, so it widened the row past the card, and because
   .ak-eintrag clips its overflow every amount in that entry was cut off by 31px.
   Found on real data, customer FCH00002885 at 375px. ONLY on the article rows:
   on every other dt the same rule chopped the LABEL letter by letter as soon
   as the value side got wide. */
.datenzeile dt.datenzeile-artikel { overflow-wrap: anywhere; }
/* Keeps "49,90 €" from breaking between the number and the currency sign. */
.datenzeile dd { text-align: right; font-size: 15px; flex: none; white-space: nowrap; }
/* The frame-number row: number plus checking pill are wider than half a
   phone. The label keeps its line, and when the value side does not fit next
   to it, the WHOLE value drops onto its own line below, right aligned; inside
   it the pill may drop under the number the same way. Everything stays whole,
   nothing squeezes the label to a sliver and nothing pushes the card open. */
.datenzeile--nummer { flex-wrap: wrap; }
.datenzeile--nummer dt { flex: none; }
.datenzeile dd.dd-umbruch {
    flex: 0 1 auto; min-width: 0; white-space: normal;
    display: flex; flex-wrap: wrap; justify-content: flex-end; align-items: center;
    gap: 4px 8px;
    margin-left: auto;
}
.dd-umbruch .nummer { white-space: nowrap; }
/* Always one line, never two: "Nr. fehlt" wrapped as soon as the
   bike name next to it got long, and a pill broken across two lines looks like a
   fault. nowrap plus flex:none means the row shortens the title instead. */
.tag {
    display: inline-block; flex: none;
    font-size: 12px; line-height: 1.4; white-space: nowrap;
    color: var(--ink-soft); background: var(--bg);
    border: 1px solid var(--line); border-radius: 100px;
    padding: 2px 9px;
}
.tag--ok { color: var(--ok-ink); border-color: var(--ok-line); background: var(--ok-bg); }
.tag--fehlt { color: var(--warn-ink); border-color: var(--warn-line); background: var(--warn-bg); }
.card-badge--action { color: var(--ok-ink); border-color: var(--ok-line); background: var(--ok-bg); }

/* ===== Receipts + documents ===== */
.card--schmal { min-height: 60px; padding: 13px 16px; }
.card-betrag { margin-left: auto; font-weight: 500; white-space: nowrap; font-variant-numeric: tabular-nums; }
.datenzeile-artikel { color: var(--ink); }
.datenzeile--summe { padding-top: 14px; }
.doku-liste { display: grid; gap: 8px; }
.doku { display: flex; align-items: center; gap: 12px; text-decoration: none; color: inherit;
    border: 1px solid var(--line); border-radius: var(--radius); padding: 12px 14px; background: var(--bg); }
.doku-icon { display: flex; flex: none; color: var(--accent-text); }
.doku-body { display: flex; flex-direction: column; }
.doku-titel { font-weight: 500; font-size: 15px; }
.doku-sub { color: var(--ink-soft); font-size: 13px; }

/* ===== Access flow (registration + login) ===== */
.flow { padding-top: clamp(16px, 5vh, 48px); display: flex; flex-direction: column; gap: 14px; }
.flow-back { text-decoration: none; color: var(--ink-soft); font-size: 15px; }
.flow-step {
    font-size: 12px;
    letter-spacing: 0.14em;
    text-transform: uppercase;
    color: var(--accent-text);
}
.flow-title { font-size: clamp(26px, 7vw, 34px); line-height: 1.15; letter-spacing: -0.02em; }
.flow-sub { color: var(--ink-soft); }
/* Centred intro line under a header.flow-sub is left aligned, which made the
   access and login screens the only place in the app with left aligned prose
   and broke DESIGN-SPEC principle 7. */
.flow-lead {
    color: var(--ink-soft);
    text-align: center;
    max-width: 34ch;
    /* Unten Luft. Vorher stand hier 0, und was folgte klebte direkt darunter:
       auf den Haeufigen Fragen sass die Gruppe "Reinkommen" mit 0px am Text, im
       Kontakt genauso "WhatsApp schreiben". Nach unten ist
       auf diesen Screens ohnehin Platz, und wo nicht, wird sowieso gescrollt. */
    margin: 0 auto 28px;
    /* Lines of roughly equal length instead of a long one and a short rest
       . "pretty" only keeps a single word off the last line,
       "balance" evens out the whole block, which is what centred text needs to
       stop looking accidental. */
    text-wrap: balance;
}
/* Variant for a paragraph that sits directly above a button: the text reads
   left aligned, but its block is centred, so its edges and the button below
   line up instead of the button floating under a full width paragraph. */
.flow-lead--links {
    text-align: left;
    width: fit-content;
}
.flow-form { display: flex; flex-direction: column; gap: 14px; }
.flow-help { color: var(--ink-soft); font-size: 14px; margin-top: 8px; }
.flow-help a { color: var(--accent-text); font-weight: 500; }

.field { display: flex; flex-direction: column; gap: 6px; }
/* Same weight as everything else on the page.
   The label is a caption, not a heading. */
.field-label { font-weight: 400; font-size: 15px; color: var(--ink-soft); }
.field-hint { font-size: 13px; color: var(--ink-soft); }
.field input {
    font: inherit;
    font-size: 17px;   /* was 18, the largest text on the screen. 17 matches the body,
                       and stays above the 16px below which iOS zooms in. */
    padding: 14px 16px;
    border: 1.5px solid var(--line-bedien);
    border-radius: var(--radius);
    background: var(--surface);
    letter-spacing: 0.04em;
    width: 100%;
    min-width: 0;
}
/* Auswahlfelder sahen aus wie vom Betriebssystem geliehen: Als das Leasing-Feld
   am dazukam, gab es fuer `select` ueberhaupt keine Regel, also stand
   ein winziges Standardfeld neben einem grossen Textfeld.
   Jetzt dieselbe Groesse, dieselbe Kante, derselbe Innenabstand wie bei input.
   `appearance: none` schaltet auch die Farben des Betriebssystems ab, deshalb
   werden Schrift und Flaeche ausdruecklich gesetzt; sonst wiederholt sich der
   Fund vom 06.08., als Bedienelemente im Dunkelmodus auf 1,91:1 kamen. */
.field select {
    font: inherit;
    font-size: 17px;
    padding: 14px 46px 14px 16px;   /* rechts Platz fuer den eigenen Pfeil */
    border: 1.5px solid var(--line-bedien);
    border-radius: var(--radius);
    background-color: var(--surface);
    color: var(--ink);
    letter-spacing: 0.04em;
    width: 100%;
    min-width: 0;
    appearance: none;
    -webkit-appearance: none;
    background-image: var(--pfeil-runter);
    background-repeat: no-repeat;
    background-position: right 16px center;
    background-size: 20px 20px;
    cursor: pointer;
}
.field--grow { min-width: 0; }
/* Multi line field, same clothes as the single line one (the
   equipment field should feel like a notepad). Three lines to start with, and it
   may be dragged taller, never wider: horizontal resizing would break the
   column. */
.field textarea {
    font: inherit;
    font-size: 17px;
    padding: 14px 16px;
    border: 1.5px solid var(--line-bedien);
    border-radius: var(--radius);
    background: var(--surface);
    color: var(--ink);
    letter-spacing: 0.04em;
    width: 100%;
    min-width: 0;
    min-height: 96px;
    resize: vertical;
    display: block;
}
.field input:focus,
.field select:focus,
.field textarea:focus {
    outline: none; border-color: var(--accent-text);
    box-shadow: 0 0 0 3px color-mix(in srgb, var(--accent) 35%, transparent);
}

/* The whole row is the label, so the whole row is the tap target. It was 23px
   high, which is half of what the spec requires for something you tick on a phone. */
.check { display: flex; align-items: center; gap: 10px; color: var(--ink-soft); font-size: 15px;
    min-height: 48px; cursor: pointer; }
.check input { width: 22px; height: 22px; flex: none; accent-color: var(--accent); }

.alert {
    background: var(--fehler-bg);
    border: 1px solid var(--fehler-line);
    color: var(--fehler-ink);
    border-radius: var(--radius);
    padding: 12px 16px;
    font-size: 15px;
}

.divider {
    display: flex;
    align-items: center;
    gap: 12px;
    color: var(--ink-soft);
    font-size: 13px;
    margin: 4px 0;
}
.divider::before, .divider::after { content: ""; flex: 1; height: 1px; background: var(--line); }

.scan { display: flex; flex-direction: column; gap: 10px; }
.scan-buehne { position: relative; }
.scan video {
    width: 100%;
    border-radius: var(--radius);
    background: #000;
    aspect-ratio: 4 / 3;
    object-fit: cover;
    display: block;
}
/* Marks the band the reader actually looks at, which is the middle 42 percent of
   the picture (see eigenerLeser in scan.js). Without the mark people hold the
   receipt anywhere in frame and nothing happens, which reads as a broken
   scanner. The two numbers belong together: change one, change the other. */
.scan-rahmen {
    position: absolute; left: 6%; right: 6%; top: 29%; height: 42%;
    border: 2px solid rgba(255, 255, 255, 0.9);
    border-radius: 6px;
    box-shadow: 0 0 0 100vmax rgba(0, 0, 0, 0.35);
    pointer-events: none;
}
.scan-hint { text-align: center; color: var(--ink-soft); font-size: 14px; }
/* Replaces three alert boxes. Not a banner: this line changes several times
   while the camera runs, and a coloured field that keeps rewriting itself is
   restless. Red stays reserved for the two cases that really are a dead end. */
.scan-status { text-align: center; color: var(--ink-soft); font-size: 15px; min-height: 22px; }
.scan-status--fehler { color: var(--fehler-ink); }
/* Diagnosis line, deliberately quiet and small: it is there for the one case
   where somebody has to describe a failing scan over the phone, not for daily
   use, and it only appears after six seconds without a hit. */
.scan-diagnose {
    text-align: center; font-size: 12px; line-height: 1.4;
    color: var(--ink-soft); opacity: 0.75;
    font-variant-numeric: tabular-nums;
}
.hidden { display: none !important; }

/* ==========================================================================
   COMPONENT CATALOGUE (DESIGN-SPEC section 8)
   Screens are assembled from these. If a screen needs something that is not
   here, add it here first. Never invent one-off styling inside a view.
   ========================================================================== */

/* --- Logo. The file itself is chosen server side by app_logo, so the alt
       text stays intact and the manual mode override is honoured. */
.logo { display: block; width: 116px; height: auto; margin-bottom: 20px; }

/* --- UCI signature stripe, taken from the logo ------------------------- */
.uci-band {
    height: 3px;
    width: 56px;
    border-radius: 2px;
    background: linear-gradient(90deg,
        var(--uci-blau) 0 20%, var(--uci-rot) 20% 40%, var(--uci-schwarz) 40% 60%,
        var(--uci-gelb) 60% 80%, var(--uci-gruen) 80%);
}
.uci-band--breit { width: 100%; }

/* --- Page header: title centred, back arrow parked on the left ----------
   The arrow is taken out of the flow so the title stays optically centred
   whether or not there is an arrow. Tab root screens have no arrow, there is
   nowhere to go back to. */
.kopf {
    position: relative;
    display: flex; align-items: center; justify-content: center;
    /* 14px war zu knapp, die Screens wirkten oben gequetscht, waehrend unten
       Platz frei blieb. */
    min-height: 48px; margin-bottom: 22px;
}
.kopf-zurueck {
    position: absolute; left: -12px; top: 50%; transform: translateY(-50%);
    display: inline-flex; align-items: center; justify-content: center;
    width: 48px; height: 48px;
    color: var(--ink-soft); text-decoration: none; font-size: 22px;
}
.kopf-titel {
    font-family: "Futura FCH", "Futura", "Avenir Next", sans-serif;
    font-size: 22px; text-align: center;
    padding: 0 44px;
}
/* Inside.flow the gap already spaces things out, and.flow's own top padding
   was meant for screens without a header. Both would double up. */
.flow > .kopf { margin-bottom: 0; }
.flow:has(> .kopf:first-child) { padding-top: 8px; }

/* Icon next to a line of text.icon is display:block, so without this the
   icon lands above the text instead of beside it. */
.zeile-mit-icon { display: flex; align-items: center; gap: 10px; }
.zeile-mit-icon .icon { flex: none; color: var(--accent-text); }

/* --- Accordion --------------------------------------------------------
   Measured a receipt carries 3 positions in the median, 93 % stay
   under eleven, and 90 % of customers own at most two bikes. Far too little
   content to justify a page load, so details unfold in place. */
.akkordeon { display: grid; gap: 10px; }
.ak-eintrag {
    background: var(--surface);
    border: 1px solid var(--line);
    border-radius: var(--radius);
    box-shadow: var(--schatten-karte);
    overflow: hidden;
}
.ak-eintrag[open] { border-color: var(--line-strong); }
.ak-kopf {
    display: flex; align-items: center; gap: 12px;
    min-height: 64px; padding: 12px 16px;
    cursor: pointer; list-style: none;
}
.ak-kopf::-webkit-details-marker { display: none; }
.ak-titel { font-weight: 400; }
.ak-meta { color: var(--ink-soft); font-size: 14px; }
.ak-rechts { margin-left: auto; display: flex; align-items: center; gap: 10px; }
.ak-pfeil { color: var(--ink-soft); transition: transform 0.2s ease; flex: none; }
.ak-eintrag[open] .ak-pfeil { transform: rotate(180deg); }
/* While closing, the entry keeps its open attribute until the animation is done,
   otherwise the body would vanish in one frame. The arrow should not wait for
   that, so it turns back right away. Written with [open] to beat the rule above
   on specificity instead of reaching for !important. */
.ak-eintrag--schliesst[open] .ak-pfeil { transform: none; }
.ak-koerper { padding: 0 16px 16px; border-top: 1px solid var(--line); padding-top: 14px; }
.ak-form { margin-top: 16px; }
.mehr-laden { display: flex; justify-content: center; margin-top: 16px; }
.mehr-laden .btn { min-width: 240px; }
.ak-hilfe { color: var(--ink-soft); font-size: 14px; }

/* --- Appointment card, the most important recurring interaction -------- */
/* Carries the two buttons that the whole reminder concept hangs on, so it gets
   the same lift as the tiles. With only a border it sat visually behind them,
   which is backwards for the most important thing on the screen. */
.termin { background: var(--surface); border: 1px solid var(--line);
    border-radius: var(--radius-gross); padding: 18px; box-shadow: var(--schatten-karte); }
.termin-label { color: var(--ink-soft); font-size: 15px; }
.termin-datum {
    font-family: "Futura FCH", "Futura", "Avenir Next", sans-serif;
    font-size: 30px; line-height: 1.2; margin: 4px 0 16px;
}
.termin-aktionen { display: flex; gap: 10px; }
.termin-aktionen > * { flex: 1; }
.termin-aktionen .btn { width: 100%; min-height: 48px; }
.termin-hinweis a { color: var(--accent-text); }
/* A button that has to look exactly like a list row. */
.such-zeile--knopf { width: 100%; font: inherit; text-align: left; cursor: pointer; }
/* Trailing icon of a day row. Deliberately NOT.ak-pfeil: that one is the
   accordion chevron and points downwards, which promised the row would unfold
   while a tap actually moves the appointment. */
.such-waehlen { color: var(--ink-soft); flex: none; display: flex; }
.such-zeile--knopf:hover .such-waehlen,
.such-zeile--knopf:focus-visible .such-waehlen { color: var(--accent-text); }
.termin--bestaetigt .termin-datum { color: var(--ok-ink); }

/* --- Empty state -------------------------------------------------------- */
.leer { text-align: center; padding: 40px 20px; color: var(--ink-soft); }
/* Was built for an emoji. Now centres an app_icon SVG instead. */
.leer-icon { display: flex; justify-content: center; margin-bottom: 12px; color: var(--ink-soft); }
.leer-icon .icon { width: 34px; height: 34px; }
/* Centred, but with lines of roughly the same length (we, about
   the text under "Deine Dokumente"). Centred text with one long line and a short
   stub underneath reads as an accident; "balance" spreads the words evenly.
   "pretty", which this inherits further up, only protects the last line. */
.leer-text { max-width: 30ch; margin: 0 auto 18px; text-wrap: balance; }

/* --- Loading skeleton, never a spinner ---------------------------------- */
.skelett { background: var(--surface); border: 1px solid var(--line);
    border-radius: var(--radius); padding: 16px; display: grid; gap: 10px; }
.skelett-zeile { height: 14px; border-radius: 7px; background: var(--line);
    animation: skelett-puls 1.4s ease-in-out infinite; }
.skelett-zeile--kurz { width: 45%; }
@keyframes skelett-puls { 0%, 100% { opacity: 0.55; } 50% { opacity: 1; } }

/* --- Banners ------------------------------------------------------------ */
.banner { border-radius: var(--radius); padding: 12px 16px; font-size: 15px; margin-bottom: 14px;
    border: 1px solid var(--ok-line); background: var(--ok-bg); color: var(--ok-ink); }
.banner--warnung { border-color: var(--warn-line); background: var(--warn-bg); color: var(--warn-ink); }
.banner--fehler { border-color: var(--fehler-line); background: var(--fehler-bg); color: var(--fehler-ink); }

/* --- Customer card barcode area ----------------------------------------
   Stays light in BOTH modes: the till scanner needs dark bars on a light
   ground. DESIGN-SPEC section 2. Colours are literal on purpose. */
/* Shaped like the plastic card it replaces (we, 06.08.: a frame like a card,
   in landscape). 1.586 is the ISO ratio of a real bank or loyalty card, so it
   reads as a card at a glance instead of as a page with a barcode on it.
   The field stays white in BOTH display modes, that is not decoration: the till
   scanner needs dark bars on a light ground. The frame is therefore dark grey in
   both modes as well, otherwise it would vanish on the white card. */
.barcode-flaeche {
    background: #ffffff;
    color: #111111;
    border: 1px solid #D8D8DE;
    border-radius: 14px;
    aspect-ratio: 1.586;
    padding: 20px 18px;
    text-align: center;
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: 14px;
    box-shadow: 0 1px 2px rgba(17, 17, 20, 0.06), 0 8px 24px rgba(17, 17, 20, 0.10);
}
/* Small and quiet. It used to be 22px and read like a
   second headline under the barcode, which it never was.
   Deliberately not removed: this line is the way out when the till scanner
   cannot read the screen, and the app's own FAQ points at it ("dann nennen wir
   einfach deine Kundennummer"). 13px is still comfortably readable across a
   counter, it just stops competing with the code above it. */
.barcode-nummer {
    margin-top: 2px; font-size: 13px; letter-spacing: 0.1em;
    font-variant-numeric: tabular-nums; color: #6B6B70;
}

/* The card is turned a quarter turn (we, 06.08.: "von oben nach unten, damit
   man sie voll sieht und nicht gequetscht"). Held upright, the phone is narrow,
   so a landscape card across the width stays small and so does the barcode. Along
   the height there is far more room.

   That is not a stylistic choice, it is a readable code: Code128 with a number
   like FCH00001744 needs about 156 modules. Across 375px of width that leaves
   roughly 2px per module, the lower limit for a till scanner. Along the height
   it is 4 to 5px. Same handling as every loyalty app.

   The rotated card is taken out of the flow, otherwise the layout would still
   reserve its unrotated box and push everything below off screen. */
/* Upright. The quarter turn is gone: it existed only to
   buy module width, and set C in app/barcode.php buys more of it without asking
   anyone to turn their phone at the counter. */
.karte-buehne {
    /* A column: the voucher button sits under the card and the
       two are centred as one group, so the card does not jump when the button
       appears or disappears with the campaign. min-height, never height, so the
       stage simply grows when the voucher is unfolded and.main scrolls. */
    display: flex; flex-direction: column; align-items: center; justify-content: center;
    gap: 18px;
    /* Fills what is left below the header so the card sits in the middle of the
       screen instead of clinging to the top with dead space underneath. */
    min-height: calc(100dvh - var(--tabbar-h) - 190px);
}
.karte {
    width: 100%;
    display: flex; flex-direction: column;
    border-radius: 10px; overflow: hidden;
    box-shadow: 0 1px 3px rgba(17, 17, 20, 0.16), 0 6px 18px rgba(17, 17, 20, 0.10);
}
/* Every pixel of width is a wider module for the scanner, so the card runs to
   the edges of the content column and keeps its padding small. */
/* Flat: card plus voucher
   tiles should fit one screen. aspect-ratio:auto kills the credit-card
   proportion that used to drive the height to 224px; the bars get a fixed
   84px. Scanning cares about bar WIDTH, not height; his scanner reads the
   same code at this height. */
.karte .barcode-flaeche { padding: 14px 10px 10px; border-radius: 0; aspect-ratio: auto; gap: 8px; }
.karte .barcode { min-height: 0; }
.karte .barcode-svg { height: 84px; }

/* --- News from the shop, on the start screen ---------------
   Replaced the four category tiles. Reads like a note somebody wrote, not like
   a headline: the sentence carries it, the date says how fresh it is. */
/* No line of its own underneath: the appointment below already brings one, and
   two hairlines with a gap between them read as a mistake. */
/* Tighter block: heading in
   bold on top, the lines underneath close together. */
.neu { padding: 0 0 8px; }
.neu-titel { margin: 0 0 2px; font-size: 14px; font-weight: 600; color: var(--ink-stark); }
/* Another quarter smaller on the shop's ask: 13px,
   down from 17, down from 20. The news line is a side note now. */
.neu-text {
    font-size: 13px; line-height: 1.45; letter-spacing: 0;
    text-wrap: pretty;
}
.neu-text a { color: var(--accent-text); text-decoration: underline; text-underline-offset: 2px; }
.neu-wann { margin-top: 2px; font-size: 11px; color: var(--ink-soft); }
/* Older entries are quieter than the current one, so the eye still knows which
   line is today's. */
.neu-text--alt { font-size: 12px; color: var(--ink-soft); }
.neu-alt { padding-top: 16px; margin-top: 16px; border-top: 1px solid var(--line); }
/* No margin on top: the summary below is a 44px tap target around a 15px line,
   so it brings roughly 15px of its own air with it. A margin here only added a
   gap twice. */
.neu-mehr { margin-top: 0; }
.neu-knopf {
    display: inline-flex; align-items: center; gap: 6px;
    /* 40px, not 44: still a workable tap target on a full-width text row, and
       the block closes up the way the shop marked it. */
    min-height: 40px; cursor: pointer; list-style: none;
    font-size: 13px; color: var(--accent-text);
}
.neu-knopf::-webkit-details-marker { display: none; }
.neu-knopf::after { content: "▾"; font-size: 12px; }
.neu-mehr[open] .neu-knopf::after { content: "▴"; }

/* Counter bands. Green says "this is a leasing bike", blue says "insured".
   Colours carry meaning here, so both also carry their own word: nobody should
   have to know the code, and colour alone fails for a colour blind mechanic. */
.karte-band {
    /* Thicker, lighter and louder. The band is read from
       arm's length across a counter, by somebody who is also holding a bike.
       The colours run at 82 % so they carry the card instead of shouting from
       it, and the white text still clears 4.5:1 against both. */
    padding: 20px 16px;
    font-size: 19px; font-weight: 500; letter-spacing: 0.01em;
    text-align: center;
    color: #FFFFFF;
    /* An insurer plus a 40 character policy number is more than one line, and a
       band that grows is better than one that cuts the number off. */
    overflow-wrap: anywhere; line-height: 1.3;
}
.karte-band--leasing { background: rgba(31, 122, 61, 0.82); }
.karte-band--versichert { background: rgba(27, 95, 168, 0.82); }
/* The SVG fills the card: width comes from the card, height from the flex row.
   preserveAspectRatio is off in the SVG, so the bars stretch to whatever height
   is available without ever changing their relative widths. */
.barcode { display: flex; align-items: stretch; justify-content: center; flex: 1; width: 100%; min-height: 70px; }
.barcode-svg { width: 100%; height: 100%; display: block; }
.karte-hinweis { text-align: center; color: var(--ink-soft); font-size: 15px; max-width: 34ch; }

/* --- Welcome voucher ---------------------------------------------------- */
.gutschein { border: 1.5px dashed var(--accent); border-radius: var(--radius-gross);
    padding: 18px; background: var(--surface); }
.gutschein-betrag {
    font-family: "Futura FCH", "Futura", "Avenir Next", sans-serif;
    font-size: 32px; color: var(--accent-text);
}
.gutschein--gesperrt { border-style: solid; border-color: var(--line); opacity: 0.75; }
.gutschein--eingeloest { border-style: solid; border-color: var(--ok-line);
    background: var(--ok-bg); color: var(--ok-ink); }
.gutschein--eingeloest .gutschein-betrag { color: var(--ok-ink); }
/* The fresh stamp: redeemed today with the time of day. Big on purpose, the
   till looks at this line as the receipt. */
.gutschein-text--stempel { font-size: 18px; font-weight: 600; color: var(--ok-ink); }

/* Devaluing at the till, three steps on the voucher.
   The summary is the first step and dresses as a button; the two panels below
   are swapped by app.js, without it the open fold shows warning plus submit. */
/* The hold button: press and hold, the fill runs
   left to right over the hold duration (set by app.js as --halten-dauer),
   full = redeemed, letting go cancels. touch-action and callout off so iOS
   neither scrolls nor opens the long-press menu mid-hold. */
.entwerten { margin-top: 12px; }
.entwerten-halten {
    position: relative; overflow: hidden; width: 100%;
    touch-action: none; user-select: none; -webkit-user-select: none;
    -webkit-touch-callout: none; -webkit-tap-highlight-color: transparent;
}
.entwerten-fuellung {
    position: absolute; inset: 0;
    background: rgba(255, 255, 255, 0.3);
    transform: scaleX(0); transform-origin: left center;
    pointer-events: none;
}
.entwerten-halten[data-haelt] .entwerten-fuellung {
    transform: scaleX(1);
    transition: transform var(--halten-dauer, 3000ms) linear;
}
.entwerten-wort { position: relative; }

/* --- Confirmation dialog ------------------------------------------------ */
.dialog-hintergrund { position: fixed; inset: 0; background: rgba(0, 0, 0, 0.5);
    display: flex; align-items: center; justify-content: center; padding: var(--pad); z-index: 50; }
.dialog { background: var(--surface); border-radius: var(--radius-gross); padding: 22px;
    max-width: 400px; width: 100%; }
.dialog-titel { font-family: "Futura FCH", "Futura", "Avenir Next", sans-serif;
    font-size: 22px; margin-bottom: 8px; }
.dialog-aktionen { display: flex; gap: 10px; margin-top: 18px; }
.dialog-aktionen .btn { flex: 1; }

/* --- Headings sit tight -------------------------------------------------
   Measured on a large travel app: 22px over 26px line height with
   -0.44px tracking, 18px over 24px with -0.18px, so roughly 1.2 and about -1
   to -2 percent. Our headings inherited the body line height of 1.55, which
   makes a two line screen title drift apart. Futura is wide by nature, so the
   tighter setting does more here than it would in a narrow face.

   Body text is deliberately NOT in this list: 17px over 1.55 stays, that is
   what carries the older half of the customers. */
.kopf-titel,
.panel-title,
.start-hallo,
.konto-kopf-name,
.dialog-titel,
.such-gruppe,
.slide-titel,
.gutschein-betrag,
.termin-datum {
    line-height: 1.22;
    letter-spacing: -0.015em;
    /* Presence comes from size, never from weight.
       Two reasons. Futura is geometric and wide, at 700 it stops being elegant and
       turns into a block. And we only host 400 and 700, so anything in between is
       faked by the browser and looks mushy at small sizes. Headings were running
       at 700 without anyone setting it: h1 and h2 are bold by browser default. */
    font-weight: 400;
}
/* Bigger instead of bolder: 26px at weight 400 has more presence than 22px at 700,
   and it stays quiet. */
.kopf-titel { font-size: 26px; }
.panel-title { font-size: 20px; }
/* A menu row is two short lines, not prose. At 1.55 they drifted apart and the
   row looked inflated. */
.menue-label { line-height: 1.3; }
.menue-sub { line-height: 1.35; }
/* A menu row is scanned, not read: slightly heavier than body text so the eye
   catches the label before the explanation underneath. */
/* One notch lighter again: at 500 the labels read as headings,
   at 400 they read as a list. Colour still separates them from the meta line. */
.menue-label, .ak-titel, .konto-kopf-name { color: var(--ink-stark); }
.menue-label { font-weight: 400; }
.konto-kopf-name { font-weight: 400; }

/* ==========================================================================
   NO BOXES
   ==========================================================================
   Content sits on the page, not in cards. Structure comes from space and, where
   a list really needs it, from a single hairline. This block runs late on
   purpose so it overrides the component rules above without touching them, and
   the whole decision can be read, or reverted, in one place.

   What deliberately keeps a surface, with reasons:
   - buttons and input fields: they are controls, and the shop's rule from 29.07. is
     that depth belongs on controls
   -.barcode-flaeche: must stay a white field in both modes or the till scanner
     cannot read the card
   -.dialog: a modal needs a surface, otherwise it floats over live text
   -.slide: those are photographs, not boxes
   -.banner /.alert /.notice: the coloured field IS the message
   -.panel--getoent: added on the shop's word. Leasing and insurance sit
     above the bike list without belonging to it, and since the block stopped
     folding there is nothing else left to separate it. One box, on request, and
     it stays the only one.
   ========================================================================== */
.panel,
.ak-eintrag,
.card,
.doku,
.such-zeile,
.termin,
.skelett,
.hero-features li,
.konto-kopf {
    background: none;
    box-shadow: none;
    border: 0;
    border-radius: 0;
}

/* --- Start screen, order set by we 06.08. from a shopping app he likes:
       search, full width banner, four small category targets, then the
       appointment. Everything flat, separated by hairlines. ------------------ */

/* Slimmer and quieter: a recessed field rather than a card sitting on the page.
   58px and a shadow made it the loudest thing on the screen, above the banner. */
.suchleiste {
    /* 44, not 48. Below the 48 the spec asks for, but a
       search field is tapped, not read, and 44 is the long standing minimum on
       phones. Noted as a deliberate exception. */
    min-height: 44px; padding: 0 12px; margin-bottom: 16px;
    background: var(--surface-2); border-color: transparent; box-shadow: none;
    /* Reaches out past the page margin (we, 10.08.: a touch wider left and
       right). Only 10px per side, it should gain room, not become the banner. */
    margin-left: -10px; margin-right: -10px;
}
/* The field itself is the tap target, the form around it is not clickable, so it
   carries the 48px rather than the container. Measured at 47px before. */
.suchleiste input { font-size: 16px; padding: 9px 0; min-height: 44px; }
/* Only the hint gets smaller, never the field itself. iOS zooms the whole page
   as soon as a focused input sits below 16px, and it does not zoom back out.
   The placeholder is not what you type into, so shrinking it costs nothing and
   the long wording gets noticeably further. */
.suchleiste input::placeholder { font-size: 13.5px; }

/* Edge to edge (we, 10.08.: take the gaps left and right out, the picture may
   run wider than the screen). This replaces the 06.08. arrangement, where the
   slide kept 10px on each side so its rounded corners had something to sit
   against. Both were his call, the newer one wins.
   The rounding stays, only smaller (we, same day: flush is right, a small
   radius is still allowed). Nothing else in the app bleeds this far, which is
   exactly the point: the banner is the one thing here that is a picture first. */
.slider { margin: 0 calc(var(--pad) * -1) 4px; }
/* No gap either. Between two full width slides it would flash as a white seam
   while the finger is still on the screen. */
.slider-spur { gap: 0; padding: 0; scroll-padding: 0; }
.slide { flex: 0 0 100%; border-radius: var(--radius); box-shadow: none; }
/* The white cards need their outline against the white page, all the way round:
   with the radius back, a line that only runs top and bottom would stop short of
   the corners. Has to be repeated here, the rule above carries the same weight
   and stands earlier. */
.slide--marke { box-shadow: inset 0 0 0 1px var(--line-strong); }
.slider-punkte { margin: 12px 0 0; }

/* Four small targets in a row instead of four big tiles: less screen for the
   same reach, and it leaves the appointment room to be the loudest thing. */
/* Padding trimmed by 10px to pay for the second label row: with
   two-line labels the start screen had grown just past the viewport again, and
   a page that scrolls by three pixels looks broken rather than long. */
.schnellzugriff {
    display: flex; align-items: flex-start; gap: 6px;
    padding: 12px 0 16px;
    border-bottom: 1px solid var(--line);
}
.sz-eintrag {
    flex: 1 1 0; min-width: 0; min-height: 48px;
    display: flex; flex-direction: column; align-items: center; gap: 8px;
    padding: 2px; text-decoration: none; color: inherit;
    transition: opacity 0.12s ease;
}
.sz-eintrag:active { opacity: 0.6; }
.sz-icon {
    display: flex; align-items: center; justify-content: center;
    width: 52px; height: 52px; border-radius: 50%;
    background: var(--ton-neutral-bg); color: var(--ink-soft);
}
/* Neutral, not coloured: four different colours in one row read
   as four different meanings. The colour tokens stay in the file, they are still
   used for status marks. */
/* Two lines are reserved for every tile so all four labels sit on the same
   baseline, even though only "Meine Räder" needs both.
   Breaking happens at the space, and for a long single word with a proper
   hyphen. "anywhere" is gone: it used to tear "Kundenkarte" into "Kundenkart"
   and "e", which looks like a rendering fault rather than a line break. */
.sz-label {
    font-size: 13px; line-height: 1.25; text-align: center;
    /* "manual" statt "auto": die automatische Trennung des Browsers zerlegte
       "Kundenkarte" in "Kundenkar-te", weil sie die letzte passende Stelle nimmt
       und nicht die Wortfuge. Getrennt wird jetzt nur dort, wo im Text ein
       weiches Trennzeichen steht. */
    overflow-wrap: normal; hyphens: manual;
    min-height: 2.5em;
}
.sz-wert { font-size: 13px; color: var(--ink-soft); }

/* Sections on a screen: air above, a hairline where the eye needs a handrail. */
.panel {
    padding: 22px 0;
    margin-bottom: 0;
    border-top: 1px solid var(--line);
}
.panel:first-of-type { border-top: 0; padding-top: 8px; }

/* The one box the app still has: leasing and insurance on "Meine Räder" (the shop,
   ), reasoned in the NO BOXES block above.
   Stands right here and written as.panel.panel--getoent on purpose. The section
   above resets every panel to flat and full width, and.panel:first-of-type
   pulls the top padding down to 8px. Both would win against a single class, and
   this block is exactly the first panel on that screen. */
/* Edge to edge and borderless. The frame is gone
   because the colour already does the separating; a border on top of it only
   drew a second line. It runs into both side margins and carries the page title,
   which makes it read as one band under the fixed header instead of a card
   floating below it. The rounded bottom is the same move as on the ad banner:
   full width, but not a hard cut. */
.panel.panel--getoent {
    background: var(--band);
    border: 0;
    border-radius: 0 0 var(--radius-gross) var(--radius-gross);
    padding: 4px 16px 18px;
    margin: 0 calc(var(--pad) * -1) 26px;
}
/* Flush against the header, so nothing of the page colour shows between them.
   Beats.main >:first-child, which gives every other screen its top air. */
.main > .panel--getoent:first-child { margin-top: 0; }
/* The page title inside the block. No side padding for a back arrow here, there
   is none on this screen, and 44px on both sides would push the two words into
   two lines on a small phone. */
.getoent-titel { padding: 0; margin-bottom: 18px; }
/* Heading, its line and the confirmation centred. The rows
   below stay left aligned: a list is read down its left edge. */
.panel--getoent > .panel-title,
.panel--getoent > .panel-sub,
.panel--getoent > .banner { text-align: center; }
/* balance instead of pretty: pretty only pulls the last word down when a line
   would otherwise be left alone, and here it left "für dich" standing on its own
   . balance spreads the sentence evenly over both lines. */
.panel--getoent > .panel-sub { text-wrap: balance; }

/* The opened bike stands out from the closed ones (an open
   bike should be more clearly marked). Same tint as the leasing panel above, so
   the screen says one thing consistently: what is tinted is what you are working
   on. Air around it as well, otherwise the surface would touch the neighbouring
   rows and the list would look like it had broken apart.
   Only.ak-eintrag, not.doku or.such-zeile: those never open. */
.akkordeon .ak-eintrag[open] {
    background: var(--surface-2);
    border: 1px solid var(--line);
    border-radius: var(--radius-gross);
    padding: 0 12px;
    margin: 10px 0;
}
/* Its own line would double up with the border it now has */
.akkordeon .ak-eintrag[open] + .ak-eintrag { border-top: 0; }
.akkordeon .ak-eintrag[open] .ak-kopf { padding: 12px 2px; }

/* List rows: one line between entries, none after the last. --line-strong, not
   --line: without a card around them the hairline is the only thing holding the
   list together, and the quiet one measured 1.26:1 in dark mode, which is
   invisible on a phone in daylight. */
.ak-eintrag,
.doku,
.such-zeile,
.card {
    border-bottom: 1px solid var(--line-strong);
    border-radius: 0;
}
.akkordeon { gap: 0; }
/* Leasing und Versicherung standen hier als aufklappbare Zeile ueber der
   Radliste, erst mit 4px Abstand, dann mit kraeftiger Trennlinie. Seit dem
    ist der Block offen und getoent (.panel--getoent in raeder.php),
   die Regeln dafuer sind mit ihm weggefallen. */
.ak-eintrag:last-child,
.doku:last-child,
.such-zeile:last-child { border-bottom: 0; }
.ak-eintrag[open] { border-color: var(--line); }
.ak-kopf { padding: 14px 2px; }
.ak-koerper { padding: 0 2px 18px; }
.doku, .such-zeile { padding: 14px 2px; }

/* No line under the last block on a screen: it would fence off empty space
   . Same for the quick access row when nothing follows it. */
.termin { padding: 16px 0; border-top: 1px solid var(--line); border-bottom: 1px solid var(--line); }
.termin:last-child, .schnellzugriff:last-child { border-bottom: 0; }
.hero-features li { padding: 10px 2px; border-bottom: 1px solid var(--line); }
.skelett { padding: 16px 0; }

/* --- One edge, not two --------------------------------------------------
   Only for the few things that still carry a shadow: their edge comes from the
   1px ring inside it, a border on top would draw a second line.

   SHORTENED, and it was a real bug: this list used to include
   .ak-eintrag,.such-zeile and.termin. Since the boxes went away those rows get
   their structure from a hairline, and this rule sat further down the file and
   quietly turned every one of them transparent again. we saw no separators on
   the bike and receipt lists, and he was right, the rule above was being
   overruled by an older one of mine. */
.app-kopf-aktion,
.suchleiste,
.tab-karte-knopf,
.slide:not(.slide--marke) {
    border-color: transparent;
}

/* --- Visible keyboard focus, required by DESIGN-SPEC section 12 ---------
   summary added with the voucher button on the customer card. Every
   fold in this app is a summary (bike list, older posts, voucher), and they were
   left with whatever outline the browser happened to draw, which is not the ring
   the rest of the app uses. */
a:focus-visible, button:focus-visible, input:focus-visible, select:focus-visible,
summary:focus-visible {
    outline: 3px solid var(--accent);
    outline-offset: 2px;
    border-radius: 4px;
}

/* --- Account menu -------------------------------------------------------
   The account used to be one long page with every form unfolded at once
   (picture, data, mail, password, documents, display).
   build it like the apps people already use, one line per topic that leads
   to its own screen. For an audience over 50 that is far less to take in,
   and every screen then has room for a proper explanation.

   Left aligned on purpose, one of the named exceptions to principle 7: this
   is a list you scan, not prose you read. */
/* No card around any of this (we, 06.08.: "dieses Box Model mag ich visuell
   nicht so sehr", with a shopping app as the example). Rows sit straight on the
   page background, no frame, no shadow, no disc behind the icon. What separates
   them is space, not lines. That is quieter than nine boxed rows and it makes the
   screen read as one list instead of three parcels. */
.konto-kopf {
    display: flex; align-items: center; gap: 16px;
    padding: 4px 4px 18px;
    text-decoration: none; color: inherit;
    transition: opacity 0.12s ease;
}
.konto-kopf:active { opacity: 0.6; }
.konto-kopf-text { display: flex; flex-direction: column; min-width: 0; flex: 1; }
.konto-kopf-name {
    font-family: "Futura FCH", "Futura", "Avenir Next", sans-serif;
    font-size: 24px; overflow-wrap: anywhere;
}
.konto-kopf-sub { color: var(--ink-soft); font-size: 14px; }
/* The chevron at the end of a row points the way, it is not the message: quiet
   colour, and it never shrinks when the label gets long. */
.konto-kopf > .icon, .menue-zeile > .icon { color: var(--ink-soft); flex: none; }

.menue-gruppe { margin-bottom: 22px; }
.menue-titel {
    font-family: "Futura FCH", "Futura", "Avenir Next", sans-serif;
    font-size: 15px; color: var(--ink-soft); margin: 0 4px 8px;
}
.menue { display: block; }
/* Works as <a> and as <button>. The button reset is not cosmetic: without it the
   row keeps the browser's own button colours, and measured that
   put the sub line at 1.91:1 in dark mode while looking fine in light mode. */
.menue-zeile {
    display: flex; align-items: center; gap: 16px;
    width: 100%; min-height: 60px; padding: 10px 4px;
    text-decoration: none; color: inherit;
    background: none; font: inherit; text-align: left; cursor: pointer;
    border: 0;
    border-radius: var(--radius);
    transition: background 0.12s ease;
}
/* The press feedback needs a hair of inset now that there is no card edge. */
.menue-zeile:active { background: var(--surface-2); }
/* Bare icon, no disc: the account is a list to scan, and a coloured disc per row
   would turn nine quiet lines into nine badges. Colour lives on the start tiles. */
.menue-icon {
    display: flex; align-items: center; justify-content: center; flex: none;
    width: 26px; color: var(--ink-soft);
}
.menue-text { flex: 1; min-width: 0; }
.menue-label { display: block; overflow-wrap: anywhere; }
.menue-sub { display: block; color: var(--ink-soft); font-size: 14px; }
/* Marks a line that is waiting for the customer, e.g. an unconfirmed address. */
.menue-zeile .tag { flex: none; }
.menue-zeile--warnung .menue-icon { color: var(--warn-ink); }
/* Short values sit on the right, the way a shopping list app shows counts. Long
   ones stay as a sub line, an email address on the right would break the row. */
.menue-wert { flex: none; color: var(--ink-soft); font-size: 15px; text-align: right; }

/* ===== Display mode switch ===== */
.darstellung { display: flex; gap: 8px; }
.darstellung-option {
    flex: 1;
    min-height: 48px;
    font: inherit;
    font-size: 15px;
    color: var(--ink);
    background: var(--surface-2);
    border: 1.5px solid var(--line);
    border-radius: var(--radius);
    cursor: pointer;
    transition: border-color 0.2s ease;
}
.darstellung-option--aktiv {
    border-color: var(--accent);
    background: var(--accent);
    color: var(--accent-ink);
    font-weight: 500;
}

/* ===== Dev: state catalogue ===== */
.kat-screen { padding: 14px 0; border-bottom: 1px solid var(--line); }
.kat-screen:last-child { border-bottom: none; padding-bottom: 0; }
.kat-screen-kopf { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; margin-bottom: 8px; }
.kat-screen-titel { font-weight: 500; }
.kat-datei { font-size: 13px; color: var(--ink-soft); font-family: ui-monospace, Menlo, monospace; }
.kat-liste { list-style: none; display: grid; gap: 4px; }
.kat-zustand { display: flex; flex-wrap: wrap; align-items: baseline; gap: 8px;
    padding: 7px 0; border-bottom: 1px solid var(--line); }
.kat-zustand:last-child { border-bottom: none; }
.kat-link { color: var(--accent-text); font-weight: 500; font-size: 15px; min-height: 24px; }
.kat-label { font-size: 15px; color: var(--ink); }
.kat-hinweis { font-size: 13px; color: var(--warn-ink); }
.kat-bauen { font-size: 13px; color: var(--ink-soft); flex-basis: 100%; }

/* ===== Dev box ===== */
.devbox {
    margin-top: 24px;
    border: 1px dashed var(--accent);
    border-radius: var(--radius);
    padding: 14px;
    background: var(--surface-2);
}
.devbox-label { font-size: 12px; color: var(--ink-soft); margin-bottom: 8px; }
.devbox-row { display: flex; gap: 8px; }
.devbox-row input {
    flex: 1;
    /* Without min-width:0 a flex item cannot shrink below the input's intrinsic
       width, which pushed the button off screen on narrow phones. */
    min-width: 0;
    font: inherit;
    padding: 10px 12px;
    border: 1px solid var(--line);
    border-radius: var(--radius);
    background: var(--surface);
}
.devbox-row .btn { min-height: 44px; }

/* --- Install screen -----------------------------------------------------
   Every variant is in the HTML, the matching one is revealed through
   data-zustand on the wrapper. The voucher screen brings that attribute along
   from the server, the install screen gets it from JavaScript, because only the
   browser knows which phone it is sitting on.

   Hidden by default. Before that the variants started out
   visible and were switched off once data-zustand appeared, so the install
   screen showed every set of instructions at once until the deferred script ran.
   Without JavaScript all of them stay visible, see the noscript block in
   views/layout.php: longer, but nobody is left without instructions. */
.install-fall { display: none; padding-bottom: 8px; }
.install[data-zustand="installiert"] [data-fall="installiert"],
.install[data-zustand="android-chrome"] [data-fall="android-chrome"],
.install[data-zustand="android-samsung"] [data-fall="android-samsung"],
.install[data-zustand="android-firefox"] [data-fall="android-firefox"],
.install[data-zustand="ios-safari"] [data-fall="ios-safari"],
.install[data-zustand="ios-anderer"] [data-fall="ios-anderer"],
.install[data-zustand="desktop"] [data-fall="desktop"] { display: block; }

/* Notifications, same mechanism as above: every case is in the
   markup, the script reveals the one that applies. Nothing shows until it has
   decided, so nobody is offered a button that cannot work on their phone; the
   noscript block in views/layout.php reveals the "not possible here" case,
   which is the truth without JavaScript. */
.push-fall { display: none; text-align: center; }
.push[data-zustand="an"] [data-fall="an"],
.push[data-zustand="aus"] [data-fall="aus"],
.push[data-zustand="verweigert"] [data-fall="verweigert"],
.push[data-zustand="ios-installieren"] [data-fall="ios-installieren"],
.push[data-zustand="unmoeglich"] [data-fall="unmoeglich"] { display: block; }
/* .btn is a flex block and ignores text-align, hence the auto margins.
   24px on top, NOT 14: adjacent block margins collapse against the choice
   block's 16px, so anything below that number changed nothing (measured;
   "steht zu eng"). */
.push .btn { margin-top: 24px; margin-inline: auto; }
/* Air between the state block and the what-we-send choice below it; measured, the button sat right on the heading. */
.push { margin-bottom: 28px; }

/* Numbered steps, big enough to follow while holding the phone in the other
   hand. The number sits in its own column so the text lines up under itself. */
.install-schritte { list-style: none; counter-reset: schritt; margin: 18px 0 0; padding: 0; }
.install-schritte li {
    counter-increment: schritt;
    position: relative;
    padding: 0 0 18px 44px;
    line-height: 1.5;
}
.install-schritte li::before {
    content: counter(schritt);
    position: absolute; left: 0; top: -2px;
    display: flex; align-items: center; justify-content: center;
    width: 30px; height: 30px; border-radius: 50%;
    background: var(--ton-neutral-bg); color: var(--ink-stark);
    font-size: 15px; font-variant-numeric: tabular-nums;
}
.install-gross {
    font-family: "Futura FCH", "Futura", "Avenir Next", sans-serif;
    font-size: 24px; line-height: 1.22; letter-spacing: -0.015em; margin-bottom: 8px;
    /* Der ganze Screen ist zentriert, nur "Alles erledigt" stand links, weil
       diese Zeile als einzige keine eigene Ausrichtung hatte. */
    text-align: center;
}

/* Anything that asks for the install disappears once it has happened. The class
   on <html> is set by app.js, both when the app already runs standalone and the
   moment the browser confirms the install. */
html.ist-installiert .nur-ohne-installation { display: none; }

/* The nudge on the start screen. A quiet banner, not a pop-up over the content:
   people over 50 close overlays reflexively without reading them, and this is
   the one message in the app that must not be reflex-closed. */
/* Text above, button below. Side by side the text was
   squeezed into a narrow column and ran over four lines; across the full width
   it needs two, so stacking costs nothing in height and reads calmer.
   This banner is the one message the app cannot afford to have ignored. */
/* One row instead of a stack. Stacked it took 113px, which was
   most of the reason the start screen did not fit on a phone (everything
   should fit, scrolling only when there is really more). It is a nudge, not a
   section, and a nudge may be one line high. */
.install-hinweis {
    position: relative;
    display: flex; align-items: center; gap: 12px;
    /* The right padding keeps the 44px tap area of the close button off the tap
       area of the install button. 34px was two pixels short of that. */
    padding: 6px 38px 8px 2px;
    border-bottom: 1px solid var(--line-strong);
}
/* Left aligned text in a centred block, so it sits symmetrically over the button
   below it.
   No padding on the right any more. It was meant to keep the text clear of the
   close button, but it pushed the visible text 25px left of the button while the
   box itself still measured as centred. Measured instead: the longest line ends
   at 277px and the button starts at 317, so they never meet. The box now hugs
   the text, which puts text and button on the same centre line. */
.install-hinweis-text {
    font-size: 15px; line-height: 1.35;
    text-align: left;
    /* Takes the room that is left beside the button now that the two sit in one
       row. The measured 25ch below only applied to the stacked version, where
       the text had the full width and had to be narrowed by hand. */
    flex: 1; min-width: 0;
    /* 25ch is measured, not chosen. CSS cannot shrink a box to the longest
       WRAPPED line: fit-content resolves to the unwrapped width and therefore to
       max-width, so a left aligned paragraph always sits left inside a box that
       is wider than its lines. At 30ch that was a visible 25px offset against
       the button. Measured on the German sentence: 30ch left 25px, 27ch 11px,
       25ch 2px, and below that it breaks into three lines and drifts again.
       If this sentence or the amount changes, measure again. */
    text-wrap: pretty;
}
.install-hinweis .btn { min-height: 44px; padding: 0 14px; flex: none; white-space: nowrap; }
.install-hinweis-zu {
    position: absolute; top: 50%; right: -8px; transform: translateY(-50%);
    width: 44px; height: 44px; border: 0; background: none;
    color: var(--ink-soft); font-size: 20px; line-height: 1; cursor: pointer;
}

/* --- Welcome voucher ----------------------------------------------------
   Looks like something you hold out at a till, which is exactly what happens
   with it. The dashed edge is the one place in the app where a border is
   decoration, and it earns that: it says "voucher" before a single word is read. */
.gutschein {
    border: 2px dashed var(--line-strong);
    border-radius: var(--radius-gross);
    padding: 26px 18px;
    text-align: center;
    margin-bottom: 18px;
}
.gutschein-betrag {
    font-family: "Futura FCH", "Futura", "Avenir Next", sans-serif;
    font-size: 44px; line-height: 1.1; letter-spacing: -0.02em;
    color: var(--ink-stark);
}
.gutschein-code {
    font-size: 26px; letter-spacing: 0.12em; margin: 14px 0 12px;
    color: var(--accent-text);
}
/* Centred like everything else on the card (we; until
   then it was a left aligned block in a centred column, which read as "not
   centred" next to amount and code). */
.gutschein-text {
    color: var(--ink-soft); font-size: 15px; line-height: 1.45;
    text-align: center;
    max-width: 30ch;
    margin-inline: auto;
}
/* Centred display texts break EVENLY (no lonely one or two
   word last lines like "im Laden."). balance, not pretty: pretty only rescues
   a single orphan word and leaves two-word rumps standing; for centred blocks
   the even distribution IS the symmetry he asked for. The old measurement
   against balance (09.08.) was made on LEFT-aligned text, where ragged right
   is correct; these are all centred. Browsers without balance simply wrap
   normally. */
.gutschein-text, .gs-gross-zweck, .gs-gross-bedingungen, .gs-kachel-sub,
.termin-hinweis, .entwerten-text, .speicher-status, .panel-sub,
.gs-info-zweck { text-wrap: balance; }
/* Everything a voucher owns lives INSIDE the dashed card
   : validity, conditions and the till button used to dangle below the
   border and looked like loose parts. */
.gutschein .footnote { margin: 10px 0 0; }
.gutschein .entwerten { margin-top: 16px; }
.gutschein .btn { margin-top: 14px; }
/* Locked: the amount stays readable but clearly out of reach, so the offer is
   visible while the step to get it is the obvious next move. */
.gutschein--gesperrt .gutschein-betrag { color: var(--ink-soft); }
.gutschein--eingeloest { border-style: solid; border-color: var(--line); }

/* Vouchers as square tiles, drawn after the shop's own mockup: a
   FILLED light blue rounded square, the value big and dark in it, a short
   line under it. The blue stays fixed in both modes, like the white barcode
   field: it is a coupon, not a surface, and dark text on this blue measures
   comfortably above 7:1. Tapping opens the enlargement below.

   Layout rule: a single voucher takes the full row, two
   share it, more continue below in the same pattern. And while the fold is
   open the whole thing is ONE card: button row and tiles inside one border,
   nothing floats outside the box. */
.gs[open] { background: var(--surface); border: 1px solid var(--line); border-radius: var(--radius); }
.gs[open] .gs-knopf { border: 0; background: none; }
.gs[open] .gs-koerper { padding: 0 16px 16px; }
.gs-kacheln { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 12px; }
.gs-kachel {
    position: relative;
    aspect-ratio: 1 / 1;
    display: flex;
    /* A quiet gradient and a breath of depth instead of a flat sticker
       : light falls from above, the inset
       line is the top highlight, the shadow lifts the tile off the card. */
    background: linear-gradient(180deg, #92D3EE 0%, #7CC3E6 100%);
    border-radius: 24px;
    color: #17171B;
    box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.4),
        0 1px 2px rgba(17, 17, 20, 0.06), 0 6px 16px rgba(17, 17, 20, 0.08);
    /* Press feedback: the tile gives slightly under the finger. */
    transition: transform 0.15s ease;
}
/* The dashed inner frame, the app's voucher grammar: the big card and the
   ticket page carry the same line, so the tile reads "Gutschein" before a
   single word does. */
.gs-kachel::before {
    content: ''; position: absolute; inset: 8px;
    border: 1.5px dashed rgba(23, 23, 27, 0.22);
    border-radius: 16px;
    pointer-events: none;
}
.gs-kachel:has(.gs-kachel-flaeche:active) { transform: scale(0.97); }
.gs-kacheln > .gs-kachel:only-child { grid-column: 1 / -1; aspect-ratio: auto; min-height: 150px; }
.gs-kachel-flaeche {
    flex: 1; min-width: 0;
    display: flex; flex-direction: column; align-items: center; justify-content: center;
    gap: 6px; padding: 12px; text-align: center;
    text-decoration: none; color: inherit; border-radius: inherit;
}
.gs-kachel-wert {
    font-family: "Futura FCH", "Futura", "Avenir Next", sans-serif;
    font-size: 38px; line-height: 1.05; letter-spacing: -0.02em;
    color: #17171B; margin: 0;
    overflow-wrap: anywhere;
}
.gs-kachel-sub {
    font-size: 13px; line-height: 1.35; color: rgba(23, 23, 27, 0.72); margin: 0; font-weight: 500;
    display: -webkit-box; -webkit-line-clamp: 3; -webkit-box-orient: vertical; overflow: hidden;
    /* German compounds ("Geburtstagsgutschein") must hyphenate instead of
       running edge to edge across the dashed inner line;
       the padding keeps even unbroken lines inside that line (inset 8px). */
    hyphens: auto; overflow-wrap: break-word;
    padding-inline: 10px; max-width: 100%; box-sizing: border-box;
}
.gs-kachel--eingeloest { background: var(--ok-bg); box-shadow: none; }
.gs-kachel--eingeloest::before { border-color: var(--ok-line); }
.gs-kachel--eingeloest .gs-kachel-wert { color: var(--ok-ink); }
.gs-kachel--eingeloest .gs-kachel-sub { color: var(--ok-ink); font-weight: 600; }
.gs-gross-bedingungen { margin: 0; font-size: 16px; line-height: 1.5; color: var(--ink); }

/* The enlargement, like an image lightbox, driven purely by:target so it
   needs no JavaScript: tile links to #gs-N, backdrop and Schließen link back
   to #gutscheine. The barcode field stays WHITE in both modes, the till
   scanner does not care about dark mode (same rule as the customer card). */
.gs-gross { display: none; }
.gs-gross:target {
    display: flex; position: fixed; inset: 0; z-index: 60;
    align-items: center; justify-content: center; padding: 16px;
}
.gs-gross-zu { position: absolute; inset: 0; background: rgba(0, 0, 0, 0.55); }
/* The enlargement ARRIVES instead of popping ("wirkt
   plump"): the veil fades, the card rises and settles, like a sheet on iOS.
   Closing stays instant, a tap away wants to feel done. The global
   reduced-motion block above disarms both. */
@keyframes gs-schleier-auf { from { opacity: 0; } }
@keyframes gs-karte-auf { from { opacity: 0; transform: translateY(16px) scale(0.94); } }
.gs-gross:target .gs-gross-zu { animation: gs-schleier-auf 0.22s ease-out; }
.gs-gross:target .gs-gross-karte { animation: gs-karte-auf 0.32s cubic-bezier(0.2, 0.9, 0.3, 1); }
/* While an enlargement is open, the page behind it holds still.:has falls
   back gracefully where unsupported. */
html:has(.gs-gross:target) { overflow: hidden; }
.gs-gross-karte {
    position: relative; width: 100%; max-width: 380px; overflow-y: auto;
    /* dvh, not vh: in a Safari TAB (before installing) 88vh measures the big
       viewport and the buttons at the card's foot sat under the toolbar. */
    max-height: 88vh;
    max-height: min(88dvh, calc(100dvh - 32px));
    overscroll-behavior: contain;
    background: var(--surface); border-radius: var(--radius-gross);
    padding: 22px 18px; text-align: center;
    box-shadow: 0 12px 40px rgba(0, 0, 0, 0.35);
    display: grid; gap: 10px; justify-items: center;
}
.gs-gross-karte > * { width: 100%; }
.gs-gross-zweck { margin: 0; font-size: 16px; color: var(--ink); }
/* The conditions page as a ticket: the
   head band repeats the tapped tile (same blue, value, subtitle), the body
   carries the conditions under a quiet heading, and a dashed rule in the
   voucher grammar separates them from code and buttons. */
.gs-gross-karte--info { padding: 0; overflow: hidden; gap: 0; }
.gs-info-kopf {
    background: #85CCEA; color: #17171B;
    padding: 26px 18px 22px; text-align: center;
}
.gs-info-kopf .gutschein-betrag { color: #17171B; font-weight: 600; }
.gs-info-zweck {
    margin: 4px 0 0; font-size: 15px; font-weight: 500;
    color: rgba(23, 23, 27, 0.72);
}
.gs-info-koerper {
    padding: 20px 18px 22px;
    display: grid; gap: 12px; justify-items: center;
}
.gs-info-koerper > * { width: 100%; }
.gs-info-titel {
    margin: 0; font-size: 12px; letter-spacing: 0.09em; text-transform: uppercase;
    font-weight: 500; color: var(--ink-soft);
}
.gs-info-trenn { border-top: 2px dashed var(--line-strong); margin: 8px 0 2px; }
.gs-scan { background: #FFFFFF; border-radius: 12px; padding: 12px 10px 8px; }
.gs-scan .barcode-svg { display: block; width: 100%; height: 82px; }
.gs-scan-nummer { margin: 6px 0 0; font-size: 14px; color: #444448; text-align: center; }
/* Names the big number under the barcode when it is the till number, so
   nobody types it into the team screen, which only knows FCH codes. */
.gs-scan-beschriftung { margin: 4px 0 0; font-size: 12px; letter-spacing: 0.04em; color: var(--ink-soft); text-align: center; text-transform: uppercase; }
.gs-gross-karte .entwerten { margin-top: 4px; }

/* --- Vouchers under the customer card ------------------
   "as a button, with a small round number in the corner, and it unfolds when
   you press it". A summary, so it opens without JavaScript, and it keeps its
   surface: NO BOXES exempts controls, and this is one.

   It sits after the NO BOXES block on purpose. Every earlier attempt at a
   surface in this app was quietly flattened by that block 200 lines later. */
.gs { width: 100%; }
.gs-knopf {
    /* 16px, not the 12 this started with: the badge reaches past the icon's
       right edge and ate the gap down to a measured 3px against the label. */
    display: flex; align-items: center; gap: 16px;
    min-height: 56px; padding: 0 16px;
    background: var(--surface);
    border: 1px solid var(--line); border-radius: var(--radius);
    font-weight: 500; cursor: pointer;
    /* Both lines needed: Safari draws its own triangle through the second one,
       every other browser through the first. */
    list-style: none;
}
.gs-knopf::-webkit-details-marker { display: none; }
.gs-knopf-icon { position: relative; display: flex; flex: none; }
.gs-knopf-text { flex: 1; min-width: 0; }
.gs-pfeil { display: flex; flex: none; color: var(--ink-soft); transition: transform 0.2s ease; }
.gs[open] .gs-pfeil { transform: rotate(180deg); }
/* Same badge as the tab bar, down to the colours, so the app counts things in
   one visual language. Only the ring differs: it has to match whatever sits
   behind it, and here that is the button, not the bar. */
.gs-punkt {
    position: absolute; top: -7px; right: -6px;
    min-width: 18px; height: 18px; padding: 0 5px;
    display: flex; align-items: center; justify-content: center;
    border-radius: 9px;
    background: var(--accent); color: var(--accent-ink);
    font-size: 11px; font-weight: 500; line-height: 1;
    font-variant-numeric: tabular-nums;
    box-shadow: 0 0 0 2px var(--surface);
}
/* The voucher itself already carries 18px below it, so the body only needs air
   above. Its last child is a button in the locked state, and a second gap under
   that would push the card off centre for no reason. */
.gs-koerper { padding-top: 18px; }
.gs-koerper .install-fall { padding-bottom: 0; }

/* ==========================================================================
   TEAM AREA SHELL. Desktop first per our call: the counter PC
   is the primary device. Same tokens as the customer app, own frame: sidebar,
   header, content. Written desktop-first with the project's fixed breakpoint
   set (1024/768/480) doing only LAYOUT switches, sizes stay fluid.

   Red in the ADMIN means "waits for you" (our wish: a red counter when
   something arrived). The customer app keeps red = error; the two surfaces
   never mix,.adm-* classes appear only under layout_admin.php.
   ========================================================================== */
:root { --adm-rot: #C62828; }          /* white on it: 5.7:1, works in both modes */
.adm { display: flex; min-height: 100dvh; background: var(--bg); }
.adm-nav {
    width: 250px; flex: none;
    display: flex; flex-direction: column; gap: 2px;
    padding: 20px 14px 16px;
    background: var(--band);
    position: sticky; top: 0; height: 100dvh; overflow-y: auto;
}
.adm-marke { display: flex; align-items: center; gap: 10px; padding: 0 10px 16px; }
.adm-marke-logo { width: 40px; height: 40px; flex: none; }
.adm-marke-text { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
.adm-marke-name {
    font-family: "Futura FCH", "Futura", "Avenir Next", sans-serif;
    font-size: 17px; line-height: 1.25; color: var(--ink-stark);
}
.adm-marke-sub { font-size: 13px; color: var(--ink-soft); }
.adm-gruppe-titel {
    font-size: 12px; letter-spacing: 0.07em; text-transform: uppercase;
    color: var(--ink-soft); padding: 16px 10px 6px;
}
.adm-link {
    display: flex; align-items: center; gap: 8px;
    min-height: 40px; padding: 8px 10px;
    border-radius: var(--radius);
    text-decoration: none; color: var(--ink); font-size: 15px;
}
.adm-link:hover { background: var(--bg); }
.adm-link--aktiv { background: var(--bg); font-weight: 500; color: var(--ink-stark); }
/* Counter pill in the nav and on dashboard rows. The red variant is the "this
   arrived, look at it" marker, the quiet one is plain information. */
.adm-zahl {
    margin-left: auto; flex: none;
    min-width: 22px; height: 22px; padding: 0 7px;
    display: inline-flex; align-items: center; justify-content: center;
    border-radius: 11px; font-size: 12px; font-weight: 500;
    font-variant-numeric: tabular-nums;
    background: var(--bg); color: var(--ink-soft);
}
.adm-zahl--rot { background: var(--adm-rot); color: #FFFFFF; }
/* Shop-style paging under the distribute result list. */
.adm-blaettern { display: flex; align-items: center; justify-content: center; gap: 14px; }
.adm-blaettern-stand { font-size: 14px; color: var(--ink-soft); }

/* Theme switch sits on its own row above the who/sign-out row; it takes the
   margin-top:auto so both rows stick to the sidebar bottom together. */
.adm-darstellung { margin-top: auto; padding: 0 10px; }
.adm-darstellung .btn--text {
    min-height: 40px; padding: 0 8px;
    display: inline-flex; align-items: center; gap: 8px;
}
.adm-fuss {
    margin-top: auto; padding: 14px 10px 0;
    display: flex; align-items: center; justify-content: space-between; gap: 8px;
    border-top: 1px solid var(--line);
}
.adm-fuss-name { font-size: 14px; color: var(--ink-soft); }
.adm-fuss .btn--text { min-height: 40px; padding: 0 8px; }

.adm-rest { flex: 1; min-width: 0; display: flex; flex-direction: column; }
.adm-kopf {
    display: flex; align-items: center; justify-content: space-between; gap: 16px;
    padding: 22px 36px 10px;
}
.adm-titel {
    font-family: "Futura FCH", "Futura", "Avenir Next", sans-serif;
    font-size: clamp(22px, 18px + 1vw, 30px); line-height: 1.15; font-weight: 400;
}
/* The content column sits in the MIDDLE of the space next to the sidebar
   since;
   the text inside stays left aligned. */
.adm-main { flex: 1; width: 100%; max-width: 1100px; margin-inline: auto; padding: 8px 36px 48px; }
/* Existing admin views bring.flow /.kopf markup; inside the shell the column
   aligns left and forms keep a readable width. */
.adm-main .flow { max-width: 720px; margin-inline: auto; }
.adm-main .flow-form { max-width: 560px; }
.adm-main .kopf { display: none; }     /* the shell header carries the title */

/* The dropdown replaces the sidebar below 1024px. Plain details, no script. */
.adm-menue { display: none; position: relative; }
.adm-menue summary { list-style: none; cursor: pointer; }
.adm-menue summary::-webkit-details-marker { display: none; }
.adm-menue-liste {
    position: absolute; right: 0; top: calc(100% + 6px); z-index: 40;
    min-width: 260px; padding: 10px;
    background: var(--bg); border: 1px solid var(--line-strong);
    border-radius: var(--radius-gross);
    box-shadow: 0 8px 28px rgba(17, 17, 20, 0.18);
    display: flex; flex-direction: column; gap: 2px;
}

/* Dashboard */
/* Three fixed columns: six tiles make two clean rows. auto-fit broke 5+1 here,
   and a lone tile in the second row read as a mistake. minmax(0,1fr) so a long
   number can never inflate its column. */
.adm-kacheln {
    display: grid; grid-template-columns: repeat(3, minmax(0, 1fr));
    gap: 14px; margin-bottom: 8px;
}
/* Four period tiles on the voucher statistics; narrow screens fall back to
   the shared two-column rule further down. */
.adm-kacheln--vier { grid-template-columns: repeat(4, minmax(0, 1fr)); }
.adm-kachel { background: var(--band); border-radius: var(--radius-gross); padding: 16px 18px 14px; }
.adm-kachel-zahl {
    font-family: "Futura FCH", "Futura", "Avenir Next", sans-serif;
    font-size: clamp(26px, 22px + 1vw, 36px); line-height: 1.1;
    font-variant-numeric: tabular-nums; color: var(--ink-stark);
}
.adm-kachel-label { font-size: 14px; color: var(--ink); margin-top: 4px; }
.adm-kachel-sub { font-size: 13px; color: var(--ink-soft); margin-top: 1px; }
.adm-abschnitt {
    font-size: 13px; letter-spacing: 0.07em; text-transform: uppercase;
    font-weight: 500; color: var(--ink-soft);
    margin: 30px 0 10px;
}
.adm-liste { display: flex; flex-direction: column; }
.adm-zeile {
    display: flex; align-items: center; gap: 12px;
    padding: 12px 2px; min-height: 52px;
    border-bottom: 1px solid var(--line);
    text-decoration: none; color: var(--ink);
}
.adm-liste .adm-zeile:last-child { border-bottom: 0; }
.adm-zeile-text { display: flex; flex-direction: column; gap: 1px; min-width: 0; flex: 1; }
.adm-zeile-titel { color: var(--ink-stark); }
.adm-zeile-sub { font-size: 13px; color: var(--ink-soft); }
.adm-wert { flex: none; font-size: 14px; color: var(--ink-soft); font-variant-numeric: tabular-nums; }
.adm-wert--warn { color: var(--adm-rot); font-weight: 500; }
/* The status dot never stands alone, the word next to it carries the meaning. */
.adm-status { flex: none; width: 10px; height: 10px; border-radius: 50%; background: var(--ok-line, #7fae8f); }
.adm-status--warn { background: var(--adm-rot); }
.adm-leer { color: var(--ink-soft); font-size: 15px; padding: 6px 2px 0; }
.adm-fussnote { font-size: 13px; color: var(--ink-soft); margin-top: 22px; max-width: 66ch; }
/* Distribution pick list: up to 400 pre-ticked rows, so the list scrolls in
   its own frame instead of stretching the page. */
.adm-liste--auswahl {
    max-height: 460px; overflow-y: auto;
    border: 1px solid var(--line); border-radius: 12px;
    padding: 2px 14px; margin: 14px 0 18px;
    background: var(--surface-2);
}
.adm-zeile--wahl { cursor: pointer; }
.adm-zeile--wahl input[type="checkbox"] { flex: none; width: 18px; height: 18px; accent-color: var(--accent); }
.adm-zeile--wahl:has(input:not(:checked)) .adm-zeile-titel,
.adm-zeile--wahl:has(input:not(:checked)) .adm-zeile-sub { color: var(--ink-soft); text-decoration: line-through; }
/* Banner cards: the picture on top shows the tile the way the app crops it. */
.adm-banner-bild {
    display: block; width: 100%; height: auto; aspect-ratio: 16 / 9;
    object-fit: cover; border-radius: 12px; margin-bottom: 14px;
    border: 1px solid var(--line);
}
.adm-banner-bild--leer {
    display: flex; align-items: center; justify-content: center;
    padding: 18px; text-align: center;
    font-size: 14px; color: var(--ink-soft); background: var(--surface-2);
}
.adm-banner-aktionen { display: flex; flex-wrap: wrap; gap: 4px 14px; align-items: start; margin-top: 10px; }
.adm-banner-entfernen summary { list-style: none; cursor: pointer; }
.adm-banner-entfernen summary::-webkit-details-marker { display: none; }
.adm-banner-entfernen[open] { flex-basis: 100%; }
.adm-banner-entfernen form { margin-top: 8px; display: grid; gap: 8px; justify-items: start; }
/* The watchdog URL: monospace and breakable, it must be copyable, not pretty. */
.adm-code { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 13px; word-break: break-all; user-select: all; }
/* A single action under a centred entry sits in the middle too (we,
   , Rahmennummern). */
/* The active what-may-we-send choice at the switch-on moment.
   Radios stay native for accessibility; accent colour ties them to the app. */
.push-wahl {
    border: 0; padding: 0; margin: 2px 0 16px;
    display: grid; gap: 10px; justify-content: center;
}
.push-wahl-titel {
    font-size: 15px; font-weight: 600; color: var(--ink-stark);
    margin-bottom: 4px; text-align: center;
}
.push-wahl-zeile {
    display: flex; align-items: flex-start; gap: 12px;
    text-align: left; cursor: pointer; max-width: 340px;
}
.push-wahl-zeile input[type="radio"] {
    margin-top: 3px; width: 20px; height: 20px; accent-color: var(--ink-stark);
}
.push-wahl-text { display: grid; gap: 1px; }
.push-wahl-label { font-size: 15px; font-weight: 600; color: var(--ink); }
.push-wahl-sub { font-size: 13px; color: var(--ink-soft); line-height: 1.35; }

/* Pre-permission push nudge, same grammar as.install-hinweis:
   text, quiet button, optional x. app.js reveals it and sets
   data-zustand="ok" after a successful subscribe; the strip then turns into
   the green confirmation and its controls disappear. */
.push-hinweis {
    position: relative;
    display: flex; align-items: center; gap: 12px;
    padding: 8px 38px 8px 2px;
    border-bottom: 1px solid var(--line-strong);
}
.push-hinweis-text { flex: 1; min-width: 0; font-size: 15px; line-height: 1.4; color: var(--ink); }
.push-hinweis-zu {
    position: absolute; top: 50%; right: -6px; transform: translateY(-50%);
    width: 44px; height: 44px; display: grid; place-items: center;
    background: none; border: 0; color: var(--ink-soft); font-size: 22px; cursor: pointer;
}
.push-hinweis[data-zustand="ok"] {
    background: var(--ok-bg); border: 1px solid var(--ok-line); border-radius: var(--radius);
    padding: 12px 14px;
}
.push-hinweis[data-zustand="ok"] .push-hinweis-text { color: var(--ok-ink); font-weight: 600; }
.push-hinweis[data-zustand="ok"] .btn,
.push-hinweis[data-zustand="ok"] .push-hinweis-zu { display: none; }
/* On the customer card the strip sits inside the voucher fold: quieter frame. */
.gs-koerper .push-hinweis { border-bottom: 0; padding-left: 6px; margin-top: 2px; }

/* Notifications, rebuilt the ON state is a green stamp like a
   redeemed voucher, the OFF state sells the benefit in three tight rows. */
.push-stempel {
    display: inline-flex; align-items: center; gap: 10px;
    background: var(--ok-bg); border: 1px solid var(--ok-line); color: var(--ok-ink);
    border-radius: 999px; padding: 12px 22px; font-weight: 600; font-size: 17px;
    margin: 4px 0 6px;
}
.push-nutzen { display: grid; gap: 14px; justify-content: center; margin: 8px 0 20px; }
.push-nutzen-zeile {
    display: flex; align-items: center; gap: 14px; margin: 0;
    font-size: 16px; line-height: 1.4; color: var(--ink); text-align: left;
}
.push-nutzen-icon {
    flex: 0 0 auto; display: grid; place-items: center;
    width: 40px; height: 40px; border-radius: 12px;
    background: var(--band); color: var(--ink-stark);
}

/* Quiet legal footer on the welcome screen. */
.recht-fuss { margin-top: 26px; text-align: center; font-size: 14px; color: var(--ink-soft); }
.recht-fuss a { color: inherit; text-decoration: underline; text-underline-offset: 3px; }

/* Centres a lone form button under centred text;.btn is a flex block and
   ignores the section's text-align. Shared by admin queues and the customer
   appointment screen. */
.form-mittig, .adm-form-mittig { display: flex; justify-content: center; }

/* Pages built from cards (voucher types, team accounts) may use the full
   content width on a desktop, reading text stays capped. */
/* Wide pages run as a centred 1040px column (the
   content block belongs in the middle of the screen, text stays left
   aligned). */
.adm-main .flow.flow--breit { max-width: 1040px; margin-inline: auto; }
.adm-main .flow--breit .flow-sub { max-width: 66ch; }
/* Centred column for the till screens: redeeming and
   distributing happen on a big desktop, a column hugging the left edge looked
   lost there. Same trick for single forms on otherwise wide pages. */
.adm-main .flow--mittig { margin-inline: auto; }
.adm-main .flow--mittig .flow-form { margin-inline: auto; width: 100%; }
.adm-main .flow-form--mittig { margin-inline: auto; width: 100%; }
/* Wide card pages, centred: two card
   columns need more than the 720px column, but hugging the left edge of a
   big screen looked lost. */
.adm-main .flow--breit.flow--mittig { max-width: 1040px; margin-inline: auto; }

/* Team avatars. Same picture pipeline as the customers, only shown here. */
.adm-avatar {
    display: inline-flex; align-items: center; justify-content: center;
    width: 32px; height: 32px; border-radius: 50%; overflow: hidden;
    background: var(--surface-2); border: 1px solid var(--line);
    font-size: 15px; color: var(--ink-soft); vertical-align: middle; flex: none;
}
.adm-avatar img { width: 100%; height: 100%; object-fit: cover; display: block; }
.adm-avatar--gross { width: 72px; height: 72px; font-size: 28px; }
.adm-bild-zeile { display: flex; align-items: flex-start; gap: 18px; flex-wrap: wrap; }
.adm-fuss-wer { display: flex; align-items: center; gap: 8px; min-width: 0; }

/* Cards for voucher types and team accounts: one thing per box, the box says
   everything about it. Two columns on a desktop. */
.adm-typen {
    display: grid; grid-template-columns: repeat(2, minmax(0, 1fr));
    gap: 16px; align-items: start;
}
.adm-typ { background: var(--band); border-radius: var(--radius-gross); padding: 18px 20px 16px; }
/* The customer tile previewed inside a type card: the SAME
   classes the app uses, only sized down, so what the shop sees here is what the
   customer gets. The i is decoration in the preview, not a link. */
.adm-typ-vorschau { display: flex; align-items: center; gap: 16px; margin: 14px 0 6px; }
.adm-typ-vorschau .gs-kachel { width: 140px; aspect-ratio: 1 / 1; flex: none; }
.adm-typ-vorschau .gs-kachel-wert { font-size: 26px; }
.adm-typ-vorschau-text {
    margin: 0; font-size: 13px; color: var(--ink-soft);
    max-width: 34ch; overflow-wrap: anywhere;
}
.adm-typ--neu { background: none; border: 2px dashed var(--line-strong); }
.adm-typ--aus { opacity: 0.72; }
.adm-typ-name {
    font-family: "Futura FCH", "Futura", "Avenir Next", sans-serif;
    font-size: 20px; color: var(--ink-stark);
}
.adm-typ-sub { font-size: 13px; color: var(--ink-soft); margin: 3px 0 14px; }
.adm-typ-form { display: flex; flex-direction: column; gap: 2px; }
/* Inputs sit on the tinted card, so they need their own ground to read as
   fields; the customer app never puts fields on --band. */
.adm-typ input, .adm-typ textarea, .adm-typ select { background: var(--bg); }

/* The appointment calendar. The number in a day is "commitments/places"; the
   cell is the way INTO the day, where the list and the day's rules live. */
.adm-kal-nav { display: flex; align-items: center; gap: 14px; margin-bottom: 12px; }
.adm-kal-nav .btn { min-height: 44px; padding: 0 16px; }
.adm-kal-titel {
    font-family: "Futura FCH", "Futura", "Avenir Next", sans-serif;
    font-size: 21px; min-width: 190px; text-align: center;
}
.adm-kal-woche {
    display: grid; grid-template-columns: repeat(7, minmax(0, 1fr)); gap: 6px;
    font-size: 12px; color: var(--ink-soft); text-align: right;
    padding: 0 8px 4px 0; margin-top: 4px;
}
.adm-kal-woche span { padding-right: 4px; }
.adm-kal { display: grid; grid-template-columns: repeat(7, minmax(0, 1fr)); gap: 6px; }
.adm-kal-tag {
    min-height: 76px; padding: 8px 10px;
    display: flex; flex-direction: column; align-items: flex-end; gap: 4px;
    background: var(--band); border-radius: var(--radius);
    text-decoration: none; color: var(--ink);
}
.adm-kal-tag:hover { background: var(--surface-2); outline: 1px solid var(--line-strong); }
.adm-kal-tag--leer { background: none; pointer-events: none; }
.adm-kal-tag--zu { background: none; border: 1px dashed var(--line); color: var(--ink-soft); }
.adm-kal-tag--heute .adm-kal-datum {
    background: var(--accent); color: var(--accent-ink);
    min-width: 24px; height: 24px; border-radius: 12px;
    display: inline-flex; align-items: center; justify-content: center;
}
.adm-kal-tag--gewaehlt { outline: 2px solid var(--accent-text); }
.adm-kal-datum { font-size: 13px; color: var(--ink-soft); line-height: 24px; }
.adm-kal-zahl {
    margin-top: auto; font-variant-numeric: tabular-nums;
    font-size: 15px; font-weight: 500; color: var(--ink-stark);
}
.adm-kal-zahl--voll { color: var(--adm-rot); }
.adm-kal-info { margin-top: auto; font-size: 13px; }
/* The day's own rules under the list: small forms side by side. */
.adm-tag-regeln { display: flex; flex-wrap: wrap; align-items: flex-end; gap: 14px; margin-top: 18px; }
.adm-tag-regeln .field { margin-bottom: 0; }
.adm-tag-regeln input[type="number"] { width: 110px; }

/* Layout switches only, sizes are fluid above. */
@media (max-width: 1024px) {
    .adm { display: block; }
    .adm-nav { display: none; }
    .adm-menue { display: block; }
    .adm-kopf { padding: 16px var(--pad) 6px; }
    .adm-main { padding: 6px var(--pad) 36px; }
}
@media (max-width: 768px) {
    .adm-typen { grid-template-columns: minmax(0, 1fr); }
    .adm-kal-tag { min-height: 58px; padding: 6px 8px; }
}
@media (max-width: 480px) {
    .adm-kacheln { grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 10px; }
    .adm-kachel { padding: 13px 14px 12px; }
    .adm-wert { max-width: 45%; text-align: right; white-space: normal; }
    .adm-kal { gap: 4px; }
    .adm-kal-tag { min-height: 46px; padding: 4px 6px; }
    .adm-kal-zahl { font-size: 12px; }
    .adm-kal-info { font-size: 11px; }
}

/* The reward line on the install screen. Not a banner: it is good news, not a
   warning, and this app does not shout. */
.install-belohnung {
    display: flex; align-items: flex-start; gap: 10px;
    margin: 14px 0 4px; color: var(--accent-text);
    font-size: 15px; line-height: 1.45;
}
.install-belohnung .icon { flex: none; margin-top: 2px; }

/* Keyed on data-gutschein, not on.install: the same four
   states also hang under the customer card, where the wrapper is a details
   element and carries no.install class. The voucher screen still matches, it
   has both. */
[data-gutschein][data-zustand="gesperrt"] [data-fall="gesperrt"],
[data-gutschein][data-zustand="offen"] [data-fall="offen"],
[data-gutschein][data-zustand="eingeloest"] [data-fall="eingeloest"],
[data-gutschein][data-zustand="beendet"] [data-fall="beendet"] { display: block; }

/* Placeholder-Farbe ausdruecklich setzen.
   Ohne diese Regel malt der Browser seinen eigenen Platzhalterton, im
   Dunkelmodus gemessen 3,4:1 und damit unter AA. --ink-soft ist pro Modus schon
   umgeschaltet und liefert 5,95:1 hell und 5,61:1 dunkel, deshalb braucht es
   hier keinen zweiten Media-Block. opacity: 1 ist Pflicht: Firefox setzt von
   sich aus eine Deckkraft unter 1 und wuerde den Wert sonst wieder druecken. */
input::placeholder,
textarea::placeholder {
    color: var(--ink-soft);
    opacity: 1;
}
