/* =====================================================================
   Yumas Internal Platform — design tokens and base styles
   The single place colour, type and spacing are defined.

   THE COLOUR RULE, IN ONE LINE:
   dark and muted is who we are; bright is what is happening right now.

   Terracotta and Moosgrün carry the colour across the app. Neither can be
   used as TEXT on the dark background — they measure 3.5:1 and 1.7:1,
   below the 4.5:1 body-text floor. They are FILLS, with white lettering on
   top, which measures 4.8:1 and 10.1:1 respectively. That is also how the
   webshop already uses them.

   Pink is the third colour and yellow the fourth: reached for only when a
   third or fourth distinct colour is genuinely needed, never as default
   decoration.

   The three state colours are reserved for status and nothing else. If
   green becomes decoration, "green means confirmed" stops carrying
   information — and roughly 1 in 12 men cannot separate red from green in
   the first place, which is why every state also carries an icon and a
   text label (§8.2).
   ===================================================================== */

/* =====================================================================
   THEMES

   Two sets of values for the same variables. Nothing outside this block
   should ever name a colour — if a rule needs one that is not here, the
   variable is missing, not the rule.

   HOW THE THREE MODES WORK:
     no data-theme      → follow the device (the default, and what
                          "Automatisch" means)
     data-theme="light" → forced light, whatever the device says
     data-theme="dark"  → forced dark

   §8.1's contrast figures were all measured against #111111 and only
   hold there. Every state colour therefore has a DIFFERENT hex in light
   mode: #fbbf24 is 11.5:1 on black and 1.7:1 on white, so an "attention"
   label carried straight over would be invisible — and §8.2 is explicit
   that these carry meaning, not decoration.

   THE LIGHT VALUES APPEAR TWICE, once per selector, because CSS has no
   way to share a block between a media query and an attribute selector.
   tools/verify-theme.php asserts the two copies are identical, so they
   cannot drift.
   ===================================================================== */

:root {
    color-scheme: dark;

    /* --- surfaces and text (§8.1) --- */
    --bg:          #111111;
    --bg-raised:   #181818;   /* cards, message bubbles, modals */
    --bg-input:    #1e1e1e;
    --border:      #272727;
    --text:        #f0f0f0;
    --text-muted:  #888888;   /* 5.9:1 — fine for secondary, not for body */

    /* --- brand: fills only, always with white text --- */
    --terracotta:      #975b4f;
    /* The ONE green. RAL 6005, §8.1. Hover states brighten this rather
       than introducing a second hex, so there is nothing to drift. */
    --moosgruen:       #114232;

    /* --- third and fourth colours, used sparingly --- */
    --pink:   #ec65c8;   /* 6.6:1 — the one brand colour safe as text */
    --yellow: #f3be00;   /* RAL 1021 Rapsgelb */

    /* --- state: status only, never decoration (§8.2) --- */
    --urgent:    #f87171;   /* 6.9:1  */
    --attention: #fbbf24;   /* 11.5:1 */
    --urgent-fill: #dc2626; /* badge fill, bold white text only */

    /* AMBER AS A FILL, which is not the same amber as amber-as-text.
       The light theme darkens --attention to #8a5a00 so it can be read
       AS TEXT on white — and anything that then used it as a background
       with --on-light on top was putting #111 on #8a5a00, about 1.6:1.
       .pill-order-partial had been doing exactly that.

       A fill's contrast is against its own lettering, not against the
       page, so this one colour works in both themes: 11.5:1 with
       --on-light. Split the same way --urgent and --urgent-fill already
       are, and defined once in the base so neither theme has to
       remember to keep them in step. */
    --attention-fill: #fbbf24;

    /* LETTERING ON A LIGHT FILL. Terracotta, Moosgrün and the red badge
       all take white on top; amber is the one brand-ish fill that is
       itself light, so white on it is unreadable and it needs the
       opposite.

       Goes with --attention-fill, NOT with --attention. This comment
       used to say "amber is light in BOTH themes", which was true of
       the dark palette and false of the light one, where --attention is
       #8a5a00 — and every rule that paired the two was unreadable there.
       See the note on --attention-fill above. */
    --on-light: #111111;
    /* "Done / confirmed" has NO separate colour. The bright green that
       §8.2 originally specified was dropped so the palette holds exactly
       one green. Confirmation uses Moosgrün as a FILL with white lettering
       (10.1:1), never as a border or text — at 1.45:1 a thin green line on
       the dark background cannot be seen. See .msg-success. */

    /* --- floor usability (§8.4) --- */
    --touch-min: 44px;
    --radius: 10px;

    /* Height of the sticky header. app.js measures the real thing and
       overwrites these on load, after a resize, and once the webfonts
       have landed — these are the fallback for when it cannot.
       Declared here rather than on .app-header because the sticky date
       separator lives in a sibling subtree, and custom properties
       inherit downwards, not sideways.

       The context fallback assumes the tab strip is there, which makes
       that row taller. A DM has no tabs, so without JavaScript its pill
       floats slightly lower than it needs to — which looks a little
       odd, where the opposite error puts the pill on top of the tabs
       and makes them unreachable. */
    --header-h: 62px;            /* one row: the channel list */
    --header-h-context: 124px;   /* two: the channel name and its tabs */

    /* --- everything that used to be a hardcoded hex ---------------- */
    /* An unread channel needs its own surface. It used to borrow
       --bg-input, which happens to differ from --bg-raised on a dark
       page and does not on a light one — so the single most important
       cue in that list came down to a coincidence. */
    --bg-unread: #1e1e1e;
    /* The tint behind an @mention. Its own variable rather than a
       translucent terracotta, so light and dark can differ — a wash that
       reads as a highlight on #121212 is nearly invisible on white. */
    --mention-bg: #3a2a26;
    /* The bars beside a video that does not fill its box. Black in BOTH
       themes on purpose: it is not a surface colour, it is the neutral
       backdrop video is presented on everywhere. Named anyway, so the
       theme check can see it is a decision and not an oversight. */
    --video-letterbox: #000000;
    /* Dark surfaces separate by being LIGHTER than the page; nothing
       more is needed, and a drop shadow on near-black is invisible
       anyway. Light surfaces need the shadow to lift at all. */
    --card-shadow: none;

    --surface-sunken: #1a1a1a;              /* the PDF viewer's frame */
    --scrollbar:       #3a3a3a;
    --scrollbar-hover: #4d4d4d;
    --code-bg:   rgba(0, 0, 0, 0.35);
    --tint-soft: rgba(255, 255, 255, 0.02); /* alternating table rows */
    --tint-firm: rgba(255, 255, 255, 0.12); /* controls over a photo */
    --shadow-sm: 0 2px 8px rgba(0, 0, 0, 0.5);
    --shadow-md: 0 4px 14px rgba(0, 0, 0, 0.5);
    --shadow-lg: 0 8px 28px rgba(0, 0, 0, 0.6);
    --scrim:        rgba(0, 0, 0, 0.6);     /* behind a bottom sheet */
    --lightbox:     rgba(0, 0, 0, 0.9);     /* a photo, full screen */
    --lightbox-bar: rgba(0, 0, 0, 0.72);
}

/* --- LIGHT: following the device ----------------------------------- */
@media (prefers-color-scheme: light) {
    :root:not([data-theme="dark"]) {
        color-scheme: light;

        /* ELEVATION IS THE OTHER WAY ROUND HERE, and that was a real
           readability bug: a #f6f6f6 card on a #ffffff page is 1.05:1,
           a darker rectangle on white, which reads as a smudge rather
           than a surface. Light interfaces lift by going TOWARDS white
           and casting a shadow — so the canvas is grey and the surfaces
           are white. */
        --bg:          #f1f1f3;
        --bg-raised:   #ffffff;
        --bg-input:    #ffffff;
        --border:      #d6d6da;
        --text:        #1a1a1a;
        /* A faint wash of the same green as the unread marker. */
        --bg-unread:   #e9f1ec;
        --mention-bg:  #f0dcd6;
        --video-letterbox: #000000;
        --card-shadow: 0 1px 2px rgba(0, 0, 0, 0.07);
        /* 6.4:1 on white. #888888 was 5.9:1 on black and only 3.5:1
           here, which fails for anything anyone has to read. */
        --text-muted:  #5c5c5c;

        /* Brand fills are unchanged: white lettering on terracotta is
           4.9:1 and on Moosgrün 12.6:1, both fine on either page. */
        --terracotta:  #975b4f;
        --moosgruen:   #114232;

        /* Third and fourth colours, darkened until they can be read.
           The originals are 2.9:1 and 1.9:1 on white. */
        --pink:   #a32b8c;
        --yellow: #7a6100;

        /* State (§8.2). Same meanings, legible hexes. */
        --urgent:      #b3261e;
        --attention:   #8a5a00;
        --urgent-fill: #c62828;

        --surface-sunken: #ececec;
        --scrollbar:       #c4c4c4;
        --scrollbar-hover: #a8a8a8;
        --code-bg:   rgba(0, 0, 0, 0.07);
        --tint-soft: rgba(0, 0, 0, 0.025);
        --tint-firm: rgba(0, 0, 0, 0.10);
        /* Softer: a heavy drop shadow on white reads as dirt. */
        --shadow-sm: 0 1px 4px rgba(0, 0, 0, 0.12);
        --shadow-md: 0 2px 10px rgba(0, 0, 0, 0.14);
        --shadow-lg: 0 8px 28px rgba(0, 0, 0, 0.18);
        --scrim: rgba(0, 0, 0, 0.45);
        /* A photo still gets a dark backdrop in light mode — a picture
           is judged against dark, not against the page. */
        --lightbox:     rgba(0, 0, 0, 0.9);
        --lightbox-bar: rgba(0, 0, 0, 0.72);
    }
}

/* --- LIGHT: chosen deliberately ------------------------------------ */
/* Identical to the block above; kept in step by tools/verify-theme.php. */
:root[data-theme="light"] {
    color-scheme: light;

    --bg:          #f1f1f3;
    --bg-raised:   #ffffff;
    --bg-input:    #ffffff;
    --border:      #d6d6da;
    --text:        #1a1a1a;
    --bg-unread:   #e9f1ec;
    --mention-bg:  #f0dcd6;
    --video-letterbox: #000000;
    --card-shadow: 0 1px 2px rgba(0, 0, 0, 0.07);
    --text-muted:  #5c5c5c;

    --terracotta:  #975b4f;
    --moosgruen:   #114232;

    --pink:   #a32b8c;
    --yellow: #7a6100;

    --urgent:      #b3261e;
    --attention:   #8a5a00;
    --urgent-fill: #c62828;

    --surface-sunken: #ececec;
    --scrollbar:       #c4c4c4;
    --scrollbar-hover: #a8a8a8;
    --code-bg:   rgba(0, 0, 0, 0.07);
    --tint-soft: rgba(0, 0, 0, 0.025);
    --tint-firm: rgba(0, 0, 0, 0.10);
    --shadow-sm: 0 1px 4px rgba(0, 0, 0, 0.12);
    --shadow-md: 0 2px 10px rgba(0, 0, 0, 0.14);
    --shadow-lg: 0 8px 28px rgba(0, 0, 0, 0.18);
    --scrim: rgba(0, 0, 0, 0.45);
    --lightbox:     rgba(0, 0, 0, 0.9);
    --lightbox-bar: rgba(0, 0, 0, 0.72);
}

* { box-sizing: border-box; }

body {
    margin: 0;
    /* THE VISIBLE HEIGHT, NOT THE THEORETICAL ONE.
       `vh` is the viewport with the browser's own chrome collapsed, which
       is not what anybody can actually see: measured in a 899px viewport,
       100vh resolved to 812px. On iOS the error runs the other way and is
       larger — Safari counts the toolbar as absent — so a page told to be
       100vh tall is taller than the screen, leaving dead space below the
       last thing on it and giving a sticky footer somewhere to sit that
       is not the bottom edge.
       `dvh` is the same number the browser actually shows. The `vh` line
       stays first as the fallback for anything too old to know `dvh`. */
    min-height: 100vh;
    min-height: 100dvh;
    background: var(--bg);
    color: var(--text);
    /* Montserrat for all body, UI, labels and buttons (§8.3), self-hosted
       — see fonts.css. */
    font-family: Montserrat, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
    font-size: 16px;   /* §8.3 floor: read at arm's length in dim light */
    line-height: 1.5;
    -webkit-text-size-adjust: 100%;
    /* Fills the notch area on iPhones when installed to the home screen,
       instead of leaving a white band above the dark app. */
    padding: env(safe-area-inset-top) env(safe-area-inset-right)
             env(safe-area-inset-bottom) env(safe-area-inset-left);
}

/* THE STATUS BAR WAS EATING THE TOP OF THE STICKY HEADER.
   The inset above is padding on <body>, which scrolls away with
   everything else. A sticky header with `top: 0` then comes to rest at
   the top of the SCROLLPORT — which on a home-screen iPhone is behind
   the clock and the battery. So "Kanäle" ended up under the clock and
   "Konto" behind the battery icon the moment you scrolled. Reported
   2026-08-11 with a screenshot.

   The header has to carry the inset itself, so the space it reserves
   travels with it. Where a page has a header, <body> gives its top
   inset up to avoid reserving the room twice.

   :has() is Safari 15.4 and up. Older browsers keep both paddings,
   which costs a little space above the header and breaks nothing —
   the header still paints under the status bar because the padding is
   its own. */
body:has(.app-header) { padding-top: 0; }

.app-header { padding-top: env(safe-area-inset-top); }

/* Railroad Gothic is a condensed display face, headings and titles ONLY.
   §8.3 is explicit that it is illegible at small sizes, so it never
   reaches body text, labels or buttons — and the sizes below keep it
   comfortably above that threshold. */
h1, h2, h3 {
    margin: 0 0 12px;
    font-family: 'Railroad Gothic', Montserrat, sans-serif;
    font-weight: 400;
    line-height: 1.2;
    /* Condensed faces need a little air between letters at display size. */
    letter-spacing: 0.03em;
    text-transform: uppercase;
}
h1 { font-size: 2rem; }
h2 { font-size: 1.5rem; }
h3 { font-size: 1.25rem; }

a { color: var(--pink); }
a:focus-visible, button:focus-visible, input:focus-visible {
    /* Never rely on colour alone, and never on hover — §8.4 forbids
       hover-dependent interaction, and half these screens are touch. */
    outline: 3px solid var(--moosgruen);
    outline-offset: 2px;
}

/* --- layout ------------------------------------------------------- */

.center-page {
    /* Same reasoning as body — see there. */
    min-height: 100vh;
    min-height: 100dvh;
    display: flex;
    align-items: center;
    justify-content: center;
    padding: 24px;
}

.card {
    width: 100%;
    max-width: 26rem;
    background: var(--bg-raised);
    border: 1px solid var(--border);
    border-radius: var(--radius);
    box-shadow: var(--card-shadow);
    padding: 28px;
    /* A long word — a German compound, a filename, a URL — must break
       rather than push the card wider than its container. */
    overflow-wrap: anywhere;
}

/* 26rem is right for a login form and far too narrow for a table.
   Reported 2026-08-08: text overflowing several cards on the storage
   page. The admin screens are read on a desktop (§B3), so they take the
   full width of the page instead. */
.card-wide { max-width: none; }

/* A table that still cannot fit scrolls INSIDE its card rather than
   making the whole page scroll sideways — which on a phone breaks every
   other screen too. */
.table-wrap {
    overflow-x: auto;
    -webkit-overflow-scrolling: touch;
}

.card h1 {
    padding-bottom: 12px;
    border-bottom: 2px solid var(--terracotta);
}

.muted { color: var(--text-muted); font-size: 0.9rem; }

/* --- forms -------------------------------------------------------- */

label {
    display: block;
    margin: 18px 0 6px;
    font-weight: 500;
}

input[type="email"],
input[type="password"],
input[type="text"] {
    width: 100%;
    min-height: var(--touch-min);
    padding: 10px 12px;
    background: var(--bg-input);
    border: 1px solid var(--border);
    border-radius: 8px;
    color: var(--text);
    font: inherit;
    font-size: 16px;   /* below 16px, iOS Safari zooms on focus */
}

/* Focus uses the brand green exactly (RAL 6005, chosen by the owner).
   It measures 1.45:1 against the field background, so the colour alone is
   a weak signal in a dim room — the border also DOUBLES in width, giving a
   change of shape that does not depend on being able to see the hue. That
   is the same principle §8.2 applies to state: colour is never the only
   signal. Inputs are border-box, so nothing shifts when it thickens. */
input:focus {
    border-color: var(--moosgruen);
    border-width: 2px;
}

/* --- show/hide password ------------------------------------------- */
/* The wrapper and button are created by app.js, so none of this exists
   unless the toggle actually works. */

.field-with-reveal { position: relative; }

.field-with-reveal input {
    /* Room for the button, so a long password never runs underneath it. */
    padding-right: calc(var(--touch-min) + 4px);
}

.reveal-btn {
    position: absolute;
    top: 0;
    right: 0;
    width: var(--touch-min);
    height: 100%;
    min-height: var(--touch-min);   /* §8.4: 44px, thumb-sized */
    display: flex;
    align-items: center;
    justify-content: center;
    padding: 0;
    border: 0;
    background: transparent;
    /* Not --text-muted: at 5.9:1 this is an interactive control, and it
       has to be findable in a dim room without hunting for it. */
    color: var(--text);
    cursor: pointer;
}

.reveal-btn:active { color: var(--moosgruen); }

/* --- buttons ------------------------------------------------------ */

.btn {
    display: block;
    width: 100%;
    min-height: var(--touch-min);
    margin-top: 24px;
    padding: 12px 16px;
    border: 0;
    border-radius: 8px;
    background: var(--terracotta);
    color: #ffffff;    /* 4.8:1 on terracotta — the whole reason it is a fill */
    font: inherit;
    font-size: 1rem;
    font-weight: 600;
    cursor: pointer;
}

.btn:hover { filter: brightness(1.15); }   /* the same terracotta, lifted */

.btn-confirm { background: var(--moosgruen); }        /* 10.1:1 with white */
.btn-confirm:hover { filter: brightness(1.45); }      /* the same green, lifted */

.btn-quiet {
    background: transparent;
    border: 1px solid var(--border);
    color: var(--text);
}

/* --- messages ----------------------------------------------------- */
/* Every one carries a word, not just a colour. A red border is not a
   message; "Anmeldung fehlgeschlagen" is. */

.msg {
    margin: 18px 0 0;
    padding: 12px 14px;
    border-radius: 8px;
    border-left: 4px solid var(--border);
    background: var(--bg-input);
    font-size: 0.95rem;
}

.msg-error   { border-left-color: var(--urgent); }
.msg-warn    { border-left-color: var(--attention); }

/* Filled rather than outlined, because the only green in the palette is
   too dark to read as a border. White on Moosgrün is 10.1:1, and a solid
   block is a stronger "this worked" than a hairline anyone might miss
   mid-shift. */
.msg-success {
    background: var(--moosgruen);
    border-left-color: var(--moosgruen);
    color: #ffffff;
}

/* A STATEMENT OF FACT, not a status. "This recipe is in the archive" is
   neither good news nor a warning, so it takes none of §8.2's state
   colours — just the panel and a quiet edge. */
.msg-info {
    background: var(--bg-input);
    border-left-color: var(--text-muted);
}

/* --- the archive and the bin (A42) ---------------------------------- */

.archive-list { list-style: none; margin: 12px 0 0; padding: 0; }

.archive-row {
    display: flex;
    align-items: center;
    gap: 12px;
    flex-wrap: wrap;
    padding: 10px 0;
    border-top: 1px solid var(--border);
}

.archive-row img {
    width: 64px;
    height: 48px;
    object-fit: contain;
    background: var(--bg-input);
    border-radius: 6px;
}

/* Takes the slack so the button sits at the right-hand end. */
.archive-body { flex: 1 1 200px; min-width: 0; }
.archive-body > * { display: block; }
.archive-body .muted { font-size: 0.8rem; margin-top: 2px; }

.msg strong { display: block; margin-bottom: 2px; }

/* --- utility ------------------------------------------------------ */

.stack > * + * { margin-top: 16px; }
.text-center { text-align: center; }
.mt-24 { margin-top: 24px; }
.mt-10 { margin-top: 10px; }

/* A message in the brand colour rather than a state colour — used when
   something is informational and must NOT be mistaken for a status. */
.msg-brand { border-left-color: var(--terracotta); }

/* Several rules below set display, which would otherwise beat the browser
   default for [hidden] and show elements meant to stay out of sight. */
[hidden] { display: none !important; }

.install-steps {
    margin: 12px 0 0;
    padding-left: 20px;
}

.install-steps li {
    margin-bottom: 8px;
    /* Read while holding the phone in the other hand, following along. */
    line-height: 1.45;
}

.install-steps li:last-child { margin-bottom: 0; }

/* --- application chrome ------------------------------------------- */

/* The header carries the page's subject, so it is two rows deep on a
   channel and one on the channel list. The terracotta rule sits under
   the whole block rather than between the rows — the line marks where
   the chrome ends and the conversation begins.
   Its height is set by its content; --header-h and --header-h-context in
   :root record that height for things that must position against it. */
.app-header {
    position: sticky;
    top: 0;
    z-index: 10;
    background: var(--bg-raised);
    border-bottom: 2px solid var(--terracotta);
}

.app-bar {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 12px;
    padding: 8px 16px;
}

.app-context {
    display: flex;
    align-items: center;
    gap: 8px;
    /* 4px at the foot, not 10: the tab row sits here now and its current
       tab draws a terracotta underline that must not touch the header's
       terracotta rule. */
    padding: 0 12px 4px;
    /* Below the desktop breakpoint the tabs drop to their own line —
       see .channel-tabs. */
    flex-wrap: wrap;
}

.context-title {
    /* Not an <h1> visually shouting — it is a label on the bar. */
    margin: 0;
    font-size: 1.35rem;
    line-height: 1.15;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
    /* Yields to the tabs when the name is long. A channel called
       "Yumas Bahnhofsviertel" must not push Medien off a phone. */
    flex: 0 1 auto;
    min-width: 3rem;
}

/* "Thread" / "Medien" — says which view this is without stealing the
   channel name's place in the bar. */
/* The channel name as the way to its members. Inherits the heading's
   size and colour — it should read as the title it is, with only the
   little figure after it saying that pressing does something. The
   ellipsis rule above lives on the h1, so the link must not introduce a
   second one: it stays inline and lets the parent do the truncating. */
.context-link {
    display: inline-flex;
    align-items: center;
    gap: 7px;
    max-width: 100%;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
    color: inherit;
    text-decoration: none;
}

.context-link svg {
    flex: 0 0 auto;
    opacity: 0.55;
}

.context-link:hover svg,
.context-link:focus-visible svg { opacity: 1; }

/* --- who is in this channel ------------------------------------------ */
.member-list {
    list-style: none;
    margin: 0;
    padding: 0;
}

.member-row {
    display: flex;
    align-items: center;
    gap: 10px;
    min-height: var(--touch-min);
    padding: 6px 2px;
    border-bottom: 1px solid var(--border);
}

.member-row:last-child { border-bottom: 0; }

.member-name {
    flex: 1 1 auto;
    min-width: 0;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.member-self { font-size: 0.85rem; }

.context-note {
    margin: 0 0 12px;
    text-transform: uppercase;
    letter-spacing: 0.08em;
    font-size: 0.72rem;
}

/* THE DESTINATIONS, AS MANY LINES AS THE PHONE NEEDS.
   Kanäle, Bestellungen, Aufgaben and Inventar want about 410px between
   them at full length — more in Spanish, where they are Canales,
   Pedidos, Tareas and Inventario. They used to share whatever was left
   after the menu button and shrink together, so a 375px phone showed
   "CANA… PEDID… TAREAS … INVENTA…" and you could not tell one from the
   next (reported 2026-09-23 from a manager's phone).

   A word cut down to four letters is not navigation. So the row wraps
   instead: the phones that have the room keep the bar one line high, and
   the ones that do not get a second line with whole words on it. The
   menu button stays where it is, on the right of the first line. */
.app-places {
    display: flex;
    align-items: center;
    flex-wrap: wrap;
    /* The gap does what .app-title-alt's margin-left did, so a wrapped
       line starts at the edge rather than indented by it. */
    column-gap: 14px;
    row-gap: 2px;
    flex: 1 1 auto;
    min-width: 0;
}

.app-places .app-title-alt { margin-left: 0; }

/* Whole words. They no longer compete for one line, so there is nothing
   to shrink into — only a single word wider than the whole bar would
   still be cut, and there is none. */
.app-places .app-title { flex: 0 0 auto; }

.app-title {
    font-family: 'Railroad Gothic', Montserrat, sans-serif;
    /* Smaller than the channel name below it: on a channel page the
       channel is what you are looking at, and the product name is just
       the way home. */
    font-size: 1.05rem;
    letter-spacing: 0.03em;
    text-transform: uppercase;
    /* Full strength. This is not a wordmark any more, it is the way back
       to the channels — dimming the only route home made it read as
       decoration. Reported 2026-08-05. */
    color: var(--text);
    text-decoration: none;
    /* The one thing in the bar that may be squeezed. Everything to the
       right is somewhere to go; this is a word people already know. */
    flex: 0 1 auto;
    min-width: 0;
    overflow: hidden;
    white-space: nowrap;
    text-overflow: ellipsis;
}

/* NEVER shrinks. On a 375px phone the bar holds the product name, the
   requests link, Admin and a display name — more than fits. Without
   this the links are what gave way, and a shift request you cannot
   reach is the one failure this app is not allowed to have. */
.app-bar-right {
    display: flex;
    align-items: center;
    flex: 0 0 auto;
    gap: 4px;
}

/* Rendered only for admins, so it is never a link somebody cannot
   follow. Quiet: it is a door, not the main action on the screen. */
.app-admin {
    display: flex;
    align-items: center;
    min-height: var(--touch-min);
    padding: 0 10px;
    border-radius: 8px;
    color: var(--text);
    font-size: 0.85rem;
    font-weight: 600;
    text-decoration: none;
    /* NO opacity. These are the app's four destinations; quietening them
       to 0.75 made the navigation look disabled next to the content it
       leads to. Weight and size already keep them from shouting. */
}

.app-admin:active { background: var(--bg-input); }

/* A request waiting on you is the one thing in the header worth
   interrupting for, so it stops being a dimmed link and starts being
   terracotta. Colour is not doing this alone — the count is next to it. */
.app-admin.has-waiting { opacity: 1; color: var(--terracotta); }
.app-admin .badge { margin-left: 6px; }

/* The bar carries up to four destinations plus a name. It cannot
   overflow — .app-title shrinks first — but on a 375px phone that would
   squeeze the title to nothing, so everything gets tighter instead.

   If this bar grows once more it wants a bottom tab row rather than more
   compression: §8.4 puts the phone first, and the thumb is at the bottom
   of the screen, not the top. */
@media (max-width: 480px) {
    .app-bar { gap: 6px; padding: 8px 10px; }
    .app-admin { padding: 0 5px; font-size: 0.78rem; }
    .app-admin .badge { margin-left: 4px; }
    .app-me { max-width: 4.5rem; font-size: 0.82rem; }
}

/* The editor that opens inside the conversation, in place of the message.
   Sized like the composer so it reads as the same control, but flush with
   the message it replaces rather than floating in a card. */
.inline-edit { margin: 4px 0 2px; }
.inline-edit .rich-input, .inline-edit textarea { min-height: 62px; }
.inline-edit .composer-row { margin-top: 8px; gap: 14px; }
.inline-edit .composer-row .btn { flex: 0 0 auto; padding: 0 20px; }

/* ---- editing and deleting ------------------------------------------ */

/* Quiet and inline. It must be visible — silently rewriting a message
   someone acted on is how a shift gets missed — but it is a footnote,
   not an accusation. */
.edited {
    margin-left: 6px;
    color: var(--text-muted);
    font-size: 0.75rem;
    font-style: italic;
    cursor: help;
}

/* The only destructive icon in the app. Terracotta on hover only, so a
   row of actions does not read as a row of warnings. */
.action-danger:hover, .action-danger:active { color: var(--terracotta); }

.btn-danger {
    background: var(--terracotta);
    border-color: var(--terracotta);
    color: #ffffff;
}

/* NEITHER FINISHED NOR FAILED. Asked for 2026-09-05 so the three
   delivery buttons read at a glance: green says it all came, amber says
   some of it did, terracotta says it is off. Dark lettering, because
   amber is the one fill in this palette that is itself light. */
.btn-warn {
    background: var(--attention-fill);
    border-color: var(--attention-fill);
    color: var(--on-light);
}

/* The message shown back on the confirmation page. Not interactive —
   it is there to be read, so that the thing being deleted is the thing
   that was meant. */
.message-quote {
    margin: 16px 0 0;
    padding: 12px 14px;
    background: var(--bg);
    border: 1px solid var(--border);
    border-left: 3px solid var(--border);
    border-radius: var(--radius);
}

/* ---- the privacy notice -------------------------------------------- */
/* Prose, not interface: wider line height and real paragraph spacing,
   because this is the one page in the app somebody actually reads rather
   than scans. */

.legal .card { max-width: 44rem; }
.legal p { margin: 0 0 14px; line-height: 1.75; }
.legal em { font-style: normal; font-weight: 600; color: var(--text); }
.legal .section-heading { margin-top: 28px; }

.quiet-link { color: var(--text-muted); font-size: 0.9rem; }

/* ---- language ------------------------------------------------------ */
/* Three buttons rather than a dropdown: a dropdown costs two taps and
   asks you to understand what the current value means, and somebody who
   cannot read the page is exactly who needs this unambiguous. */

.lang-switch {
    display: flex;
    flex-wrap: wrap;
    gap: 8px;
    margin: 12px 0 0;
}

.lang-btn {
    min-height: var(--touch-min);
    padding: 0 16px;
    background: transparent;
    border: 1px solid var(--border);
    border-radius: 8px;
    color: var(--text-muted);
    font: inherit;
    font-size: 0.9rem;
    cursor: pointer;
}

.lang-btn.is-current {
    background: var(--moosgruen);
    border-color: var(--moosgruen);
    color: #ffffff;
    font-weight: 600;
}

/* ---- statistics ---------------------------------------------------- */

.stats-filters { margin-top: 16px; }

.filter-row {
    display: flex;
    flex-wrap: wrap;
    gap: 14px;
    margin-bottom: 12px;
}

.filter-row > span {
    display: flex;
    flex-direction: column;
    gap: 4px;
    flex: 1 1 11rem;
}

.filter-row label { margin: 0; font-size: 0.8rem; }
.filter-row input, .filter-row select { min-height: var(--touch-min); width: 100%; }

/* A table is the one thing that legitimately wants to be wider than a
   phone, so it scrolls inside its own box rather than making the page
   scroll sideways. */
.table-scroll { overflow-x: auto; margin-top: 12px; }

/* --- storage ---------------------------------------------------------- */

/* A native <progress>, which needs its appearance reset and then three
   sets of rules — the bar, WebKit's value pseudo-element and Firefox's.
   Verbose, but it is the only way to carry a percentage without an
   inline style, which the CSP forbids. */
.quota-bar {
    -webkit-appearance: none;
    appearance: none;
    display: block;
    width: 100%;
    height: 14px;
    background: var(--bg-input);
    border: 1px solid var(--border);
    border-radius: 999px;
    overflow: hidden;
}

.quota-bar::-webkit-progress-bar { background: transparent; }
.quota-bar::-webkit-progress-value { background: var(--terracotta); }
.quota-bar::-moz-progress-bar { background: var(--terracotta); }

/* The colour is the second signal, never the only one — the sentence
   under the bar changes too, so it still reads correctly to somebody
   who cannot tell these apart. */
.quota-warn::-webkit-progress-value { background: var(--attention); }
.quota-warn::-moz-progress-bar      { background: var(--attention); }
.quota-bad::-webkit-progress-value  { background: var(--urgent); }
.quota-bad::-moz-progress-bar       { background: var(--urgent); }

/* The sentence sits above the bar — it is what people read; the bar is
   the shape of it. */
.quota-line { margin: 0 0 10px; }

.stats-table { width: 100%; border-collapse: collapse; font-size: 0.92rem; }

.stats-table th, .stats-table td {
    padding: 10px 12px;
    border-bottom: 1px solid var(--border);
    text-align: left;
    /* Numbers and dates must not break mid-value. Prose columns opt
       back into wrapping with .wrap — see below. */
    white-space: nowrap;
}

/* For the columns that hold a sentence rather than a figure. */
.stats-table th.wrap, .stats-table td.wrap { white-space: normal; }

.stats-table thead th {
    font-size: 0.75rem;
    text-transform: uppercase;
    letter-spacing: 0.05em;
    color: var(--text-muted);
}

.stats-table tbody th { font-weight: 600; }
.stats-table .num { text-align: right; font-variant-numeric: tabular-nums; }

/* Weight, not colour. §8.2 reserves colour for state, and a count is not
   a state — and #114232 as text on a dark background is unreadable
   anyway. Bold is enough to make the column you came for stand out. */
.stats-table .yes { font-weight: 700; }

/* Quiet, at the foot of a list. Not a destination people need, but one
   they must be able to find when they go looking for something old. */
/* A button, not a sentence. The way to a place should look like the way
   to a place — and quiet, because the archive is somewhere you go
   looking for something, never somewhere you are being sent. */
.archive-link {
    margin: 24px 0 0;
    padding-top: 16px;
    border-top: 1px solid var(--border);
    font-size: 0.85rem;
    line-height: 1.5;
}

.archive-link .btn { display: inline-block; width: auto; margin: 0; }

/* ---- the PDF viewer page ------------------------------------------ */

.pdf-page {
    /* Fills what is left under the header so the document gets the whole
       screen. The back arrow stays in the header above it, which is the
       entire reason this page exists. */
    display: flex;
    flex-direction: column;
    /* The context variant: this page always shows the file name as its
       context row, so the header is the taller of the two. */
    height: calc(100vh - var(--header-h-context));
    height: calc(100dvh - var(--header-h-context));
    padding: 0;
}

.pdf-frame {
    flex: 1 1 auto;
    width: 100%;
    min-height: 0;
    border: 0;
    background: var(--surface-sunken);
}

.pdf-fallback {
    display: flex;
    flex-direction: column;
    align-items: flex-start;
    gap: 10px;
    padding: 24px 16px;
}

.pdf-actions {
    flex: 0 0 auto;
    margin: 0;
    padding: 10px 16px calc(10px + env(safe-area-inset-bottom));
    background: var(--bg-raised);
    border-top: 1px solid var(--border);
    font-size: 0.85rem;
    text-align: center;
}

.pdf-actions a { color: var(--text-muted); }

/* A SPREADSHEET OR TEXT FILE, SHOWN (2026-09-17). Same page frame as a
   PDF — header with the back arrow, the file filling what is left, the
   save bar at the foot — with a table that scrolls both ways inside it.
   Row numbers and column letters stay put while it scrolls, so "row 14,
   column D" can be said out loud. */
.sheet-tabs {
    flex: 0 0 auto;
    display: flex;
    gap: 4px;
    overflow-x: auto;
    padding: 8px 12px 0;
    border-bottom: 1px solid var(--border);
}
.sheet-tab {
    flex: 0 0 auto;
    padding: 8px 14px;
    border: 1px solid var(--border);
    border-bottom: 0;
    border-radius: 8px 8px 0 0;
    color: var(--text-muted);
    text-decoration: none;
    white-space: nowrap;
}
.sheet-tab.is-on { background: var(--bg-raised); color: var(--text); font-weight: 600; }

.sheet-scroll {
    flex: 1 1 auto;
    min-height: 0;
    overflow: auto;
    -webkit-overflow-scrolling: touch;
    background: var(--surface-sunken);
}
.sheet-table {
    border-collapse: separate;
    border-spacing: 0;
    font-size: 0.85rem;
    font-variant-numeric: tabular-nums;
}
.sheet-table th,
.sheet-table td {
    padding: 4px 8px;
    border-right: 1px solid var(--border);
    border-bottom: 1px solid var(--border);
    white-space: nowrap;
    max-width: 28rem;
    overflow: hidden;
    text-overflow: ellipsis;
    vertical-align: top;
    text-align: left;
}
.sheet-table td { background: var(--bg-raised); color: var(--text); }
.sheet-table td.num { text-align: right; }
.sheet-table thead th {
    position: sticky;
    top: 0;
    z-index: 2;
    background: var(--bg-input);
    color: var(--text-muted);
    font-weight: 600;
    text-align: center;
}
.sheet-table tbody th {
    position: sticky;
    left: 0;
    z-index: 1;
    background: var(--bg-input);
    color: var(--text-muted);
    font-weight: 500;
    text-align: right;
}
.sheet-table .sheet-corner { left: 0; z-index: 3; }
.sheet-empty, .sheet-note { margin: 0; padding: 10px 16px; font-size: 0.85rem; }
.text-preview {
    margin: 0;
    padding: 14px 16px;
    white-space: pre-wrap;
    word-break: break-word;
    font-size: 0.9rem;
    color: var(--text);
}

/* Under a photo in a message. Quiet on purpose: the picture is what
   people came for, and a save link that competes with it is furniture on
   every photo ever posted. */
.attachment-actions {
    margin: 4px 0 0;
    font-size: 0.8rem;
}

.attachment-actions a { color: var(--text-muted); }
.attachment-actions a:hover { color: var(--text); }
.attachment-actions span { color: var(--text-muted); margin: 0 6px; }

/* --- sharing a file into another channel ---------------------------- */

.share-preview {
    margin: 0 0 18px;
    padding: 12px;
    border: 1px solid var(--border);
    border-radius: var(--radius);
    background: var(--bg-raised);
    text-align: center;
}

.share-preview img {
    max-width: 100%;
    max-height: 220px;
    border-radius: 6px;
}

.share-preview p { margin: 8px 0 0; font-size: 0.85rem; }

/* The share screen centres a filename; a card is not a filename. */
.share-preview .recipe-embed { text-align: left; }

/* --- a recipe shared into a conversation (A42) ---------------------- */

/* A CARD INSIDE A BUBBLE, so it needs its own edge to read as a
   different object from the message that carries it — but a quiet one.
   The terracotta rule from §8.1 holds: a fill with white text or
   nothing, never a thin coloured line. This gets a plain border. */
.recipe-embed {
    display: flex;
    gap: 10px;
    align-items: flex-start;
    /* CAPPED, because a card stretched across a 900px bubble stops
       reading as a card and starts reading as a banner. On a phone the
       bubble is narrower than this and the cap never applies. */
    max-width: 400px;
    margin-top: 8px;
    padding: 10px;
    border: 1px solid var(--border);
    border-radius: var(--radius);
    background: var(--bg);
    color: var(--text);
    text-decoration: none;
}

.recipe-embed:hover { border-color: var(--text-muted); }

.recipe-embed-photo {
    flex: 0 0 auto;
    width: 64px;
    height: 64px;
    border-radius: 6px;
    overflow: hidden;
}

.recipe-embed-photo img { width: 100%; height: 100%; object-fit: cover; }

/* No photo yet — the glass shape says more than a grey rectangle. */
.recipe-embed-photo-empty {
    display: flex;
    align-items: center;
    justify-content: center;
    background: var(--bg-raised);
    color: var(--text-muted);
}

/* min-width:0 or a long ingredient line pushes the card wider than the
   bubble instead of wrapping inside it. */
.recipe-embed-body { display: block; min-width: 0; }

.recipe-embed-name { display: block; font-weight: 600; }
.recipe-embed-sub  { display: block; font-size: 0.85rem; }

.recipe-embed-glass {
    display: flex;
    align-items: center;
    gap: 5px;
    margin-top: 2px;
    font-size: 0.8rem;
}

/* THE MEASURES ARE THE POINT — this is what people argue about, so it
   is the only part of the recipe the card carries. */
.recipe-embed-lines {
    display: block;
    margin-top: 6px;
    font-size: 0.85rem;
    line-height: 1.5;
}

.recipe-embed-lines span { display: block; }

.recipe-embed-more {
    display: block;
    margin-top: 6px;
    font-size: 0.8rem;
    color: var(--pink);
}

/* Soft-deleted: still readable, because the thread about it still is. */
.recipe-embed.is-gone { opacity: 0.65; }

.recipe-embed-gone {
    margin-top: 8px;
    padding: 10px;
    border: 1px dashed var(--border);
    border-radius: var(--radius);
    font-size: 0.85rem;
}

/* A quiet marker, not a status. Used for "Gruppe" beside a name. */
.pill {
    display: inline-block;
    margin-left: 6px;
    padding: 1px 7px;
    border: 1px solid var(--border);
    border-radius: 999px;
    color: var(--text-muted);
    font-size: 0.7rem;
    font-weight: 600;
    vertical-align: middle;
}

.admin-nav {
    display: flex;
    gap: 4px;
    max-width: 74rem;
    margin: 0 auto;
    padding: 12px 16px 0;
}

.admin-tab {
    display: flex;
    align-items: center;
    min-height: 38px;
    padding: 0 14px;
    background: var(--bg-raised);
    border: 1px solid var(--border);
    border-radius: 999px;
    color: var(--text);
    font-size: 0.9rem;
    text-decoration: none;
}

.admin-tab.is-current {
    background: var(--terracotta);
    border-color: var(--terracotta);
    color: #ffffff;
    font-weight: 600;
}

.app-me {
    /* 44px tall so it is a real target, not a word to be poked at. */
    display: flex;
    align-items: center;
    min-height: var(--touch-min);
    padding: 0 4px;
    color: var(--text);
    font-size: 0.9rem;
    text-decoration: none;
    /* "Bahnhofsviertel-Aushilfe Nikolas" must not push the links off the
       screen. Truncated, because you know your own name. */
    max-width: 7.5rem;
    overflow: hidden;
    white-space: nowrap;
    text-overflow: ellipsis;
}

.page {
    max-width: 44rem;
    margin: 0 auto;
    padding: 16px;
}

/* A SCREEN THAT IS A TABLE RATHER THAN A TEXT. 44rem is a reading
   measure — right for a message, a to-do, an order — and wrong for the
   stock catalogue, which is seven columns somebody scans across.

   ONLY WHERE THERE IS ROOM: below 62rem this is the ordinary page, so
   nothing changes on a phone. Added 2026-09-09 for the kitchen's iPad,
   where the narrow column with one wide table in it was reported as the
   screen looking skewed.

   THE FORMS KEEP THE READING MEASURE. A text field stretched across
   74rem is a worse field, and the 44rem was always for them: a name and
   a unit do not get easier to type by being three feet wide. */
@media (min-width: 62rem) {
    .page-wide { max-width: min(74rem, 100vw - 3rem); }

    .page-wide form,
    .page-wide .page-help { max-width: 44rem; }

    /* …except the ones that ARE a row of the table — an area's rename
       line and the "put an article here" line sit inside the wide part
       and would jump back to the narrow column mid-list. */
    .page-wide .inv-area-row,
    .page-wide .inv-area-add,
    .page-wide .inv-walk form { max-width: none; }

    /* …and a form that holds the prep grid (2026-09-17): the 44rem made
       the table scroll sideways on a desktop with room to spare. */
    .page-wide form:has(.prep-grid) { max-width: none; }
}

/* --- the desktop channel rail --------------------------------------- */
/* Hidden by default. On a phone the screen belongs to the conversation,
   and the channel list is one tap away through the back arrow — a
   permanently visible sidebar would cost the width the messages need. */

.channel-rail { display: none; }

@media (min-width: 900px) {
    .app-layout {
        display: grid;
        grid-template-columns: 232px minmax(0, 1fr);
        gap: 20px;
        max-width: 74rem;
        margin: 0 auto;
        padding: 16px 16px 0;
        align-items: start;
    }

    /* THE HEADER'S ROWS LINE UP WITH THE COLUMNS UNDER THEM. The header
       spans the window — it has to, it carries the terracotta rule — but
       its contents used to start at the window's edge while everything
       below started at the channel list. Two left edges a few hundred
       pixels apart, for no reason anyone could see. Same box as
       .app-layout, so "Kanäle" and "← Allgemein" sit directly above the
       channel list. Asked for 2026-08-12. */
    .app-bar,
    .app-context {
        max-width: 74rem;
        margin: 0 auto;
        padding-left: 16px;
        padding-right: 16px;
    }

    /* The desktop shape of the title row is NOT here — it sits with
       .channel-tabs further down, because a media query adds no
       specificity and the base rule comes later in this file. Written
       here first, it lost, and the channel name was cut to "Al…" on a
       1500px screen. */

    /* The second column: the tab strip, the conversation, everything
       that is not the rail. min-width: 0 because a grid track otherwise
       refuses to shrink below its content, and one long word or a wide
       table would push the whole layout sideways. */
    .app-main {
        min-width: 0;
    }

    /* The conversation is already centred inside its own column, so the
       page must not re-centre against the whole window. */
    .app-layout .page {
        margin: 0;
        padding: 0;
        max-width: none;
    }

    .channel-rail {
        /* FLEX HERE AND NOWHERE ELSE. The fold-out and the Neu group
           need this a column so the lists can be reordered, and it was
           first written next to .home-lists further down the file — at
           top level, where it beat the display:none above and put the
           whole channel list on top of every page on a phone. */
        display: flex;
        flex-direction: column;
        position: sticky;
        /* Sits under the header, and scrolls independently once there
           are more channels than fit. */
        top: calc(var(--header-h-context) + 16px);
        max-height: calc(100vh - var(--header-h-context) - 32px);
        overflow-y: auto;
        scrollbar-width: thin;
        scrollbar-color: var(--scrollbar) transparent;
    }

    .channel-rail ul {
        list-style: none;
        margin: 0;
        padding: 0;
    }

    /* The rail shows channels in a channel and people in a conversation,
       so it says which. Quiet — it is a label, not a destination. */
    .rail-heading {
        margin: 0 0 8px;
        padding: 0 12px;
        color: var(--text-muted);
        font-size: 0.72rem;
        font-weight: 600;
        letter-spacing: 0.08em;
        text-transform: uppercase;
    }

    /* The way from "people I am talking to" to everyone else. */
    .rail-more {
        margin: 10px 0 0;
        padding: 0 12px;
        font-size: 0.85rem;
    }

    .rail-more a { color: var(--text-muted); }
    .rail-more a:hover { color: var(--text); }

    /* A thread beside the conversation rather than instead of it.
       Only here, inside the desktop breakpoint: on a phone there is no
       room and the full page is the right answer. */
    .app-layout:has(.thread-panel) {
        grid-template-columns: 232px minmax(0, 1fr) minmax(0, 26rem);
    }

    .thread-panel {
        position: sticky;
        top: calc(var(--header-h-context) + 16px);
        max-height: calc(100vh - var(--header-h-context) - 32px);
        overflow-y: auto;
        background: var(--bg-raised);
        border: 1px solid var(--border);
        border-radius: var(--radius);
        scrollbar-width: thin;
        scrollbar-color: var(--scrollbar) transparent;
    }

    .thread-panel-bar {
        position: sticky;
        top: 0;
        z-index: 1;
        display: flex;
        align-items: center;
        gap: 10px;
        padding: 8px 12px;
        background: var(--bg-raised);
        border-bottom: 1px solid var(--border);
    }

    .thread-panel-close {
        min-width: 32px;
        min-height: 32px;
        padding: 0;
        background: none;
        border: 0;
        color: var(--text-muted);
        font-size: 1.3rem;
        line-height: 1;
        cursor: pointer;
    }

    .thread-panel-close:hover { color: var(--text); }

    .thread-panel-full { font-size: 0.8rem; color: var(--text-muted); }
    .thread-panel-full:hover { color: var(--text); }

    /* The fragment brings its own <main class="page">, which is centred
       and padded for a full screen. Inside the panel it is the panel. */
    .thread-panel-body .page {
        max-width: none;
        margin: 0;
        padding: 12px;
    }

    /* Nachrichten, under the last channel. THE LINE IS ABOVE IT: a rule
       under the final item reads as the end of the list rather than as
       a separator between two kinds of thing. Asked for in exactly
       those terms on 2026-08-12. */
    .rail-plan { margin: 0 0 14px; list-style: none; padding: 0; }

    .rail-dms {
        margin: 10px 0 0;
        padding-top: 6px;
        border-top: 1px solid var(--border);
    }

    .rail-dms > summary {
        display: flex;
        align-items: center;
        gap: 8px;
        min-height: 34px;
        padding: 0 12px;
        color: var(--text-muted);
        font-size: 0.72rem;
        font-weight: 700;
        letter-spacing: 0.08em;
        text-transform: uppercase;
        cursor: pointer;
        list-style: none;
    }

    .rail-dms > summary::-webkit-details-marker { display: none; }

    .rail-dms > summary::before {
        content: '';
        width: 0;
        height: 0;
        border-left: 5px solid currentColor;
        border-top: 4px solid transparent;
        border-bottom: 4px solid transparent;
        transition: transform 120ms ease;
    }

    .rail-dms[open] > summary::before { transform: rotate(90deg); }

    .rail-dms > summary:hover { color: var(--text); }

    /* THE CHANNEL DRAWER IN THE RAIL, dressed exactly like the
       conversations' one above. Reported 2026-09-02: the fold worked on
       the overview and not here — and when it was added, it was added
       without these, so it had no arrow and no pointer. The rules live
       here rather than being shared with .channel-foldout because the
       rail runs at its own smaller size: 34px rows, 0.72rem, tighter
       padding. */
    .rail-channels > summary {
        display: flex;
        align-items: center;
        gap: 8px;
        min-height: 34px;
        padding: 0 12px;
        color: var(--text-muted);
        font-size: 0.72rem;
        font-weight: 700;
        letter-spacing: 0.08em;
        text-transform: uppercase;
        cursor: pointer;
        list-style: none;
    }

    .rail-channels > summary::-webkit-details-marker { display: none; }

    .rail-channels > summary::before {
        content: '';
        width: 0;
        height: 0;
        border-left: 5px solid currentColor;
        border-top: 4px solid transparent;
        border-bottom: 4px solid transparent;
        transition: transform 120ms ease;
    }

    .rail-channels[open] > summary::before { transform: rotate(90deg); }
    .rail-channels > summary:hover { color: var(--text); }

    /* THE RULE BETWEEN THE TWO LISTS BELONGS TO WHICHEVER IS SECOND.
       It hangs off the conversations, which is right while they are
       underneath — and a stray line across the top of the rail once
       they move above. So it follows them. */
    .rail-dms.dm-first { border-top: 0; margin-top: 0; padding-top: 0; }

    .rail-dms.dm-first ~ .rail-channels {
        margin-top: 10px;
        padding-top: 6px;
        border-top: 1px solid var(--border);
    }

    /* Shorter than the home screen's: the rail row is 34px, not a
       touch target, and this is a pointer surface. */
    .rail-dms .dm-add {
        width: 26px;
        min-height: 26px;
        margin-right: -4px;
        font-size: 1.15rem;
    }

    .rail-dms ul { list-style: none; margin: 0; padding: 0; }
    .rail-dms .rail-item { margin: 0; }
    .rail-dms .rail-more { margin: 6px 0 0; }

    .rail-item {
        display: flex;
        align-items: center;
        justify-content: space-between;
        gap: 8px;
        min-height: 40px;
        padding: 8px 12px;
        margin-bottom: 4px;
        border-radius: 8px;
        border-left: 3px solid transparent;
        color: var(--text);
        text-decoration: none;
        font-size: 0.92rem;
    }

    .rail-item:hover { background: var(--bg-raised); }

    /* Where you are. Terracotta fill rather than a green edge: green is
       doing unread duty two lines down, and one colour cannot mean both
       "you are here" and "something new". */
    .rail-item.is-current {
        background: var(--terracotta);
        border-left-color: var(--terracotta);
        color: #ffffff;
        font-weight: 600;
    }

    .rail-item.is-current:hover { filter: brightness(1.15); background: var(--terracotta); }

    .rail-item.has-unread { font-weight: 700; }
    .rail-item.has-unread:not(.is-current) { border-left-color: var(--moosgruen); }

    .rail-name {
        overflow: hidden;
        text-overflow: ellipsis;
        white-space: nowrap;
    }

    /* A group in the rail is two lines: its name, and who is in it.
       min-width:0 on both, or a flex child refuses to shrink below its
       content and the ellipsis never appears — the column widens
       instead and pushes the conversation sideways. */
    .rail-stack {
        display: flex;
        flex-direction: column;
        gap: 1px;
        min-width: 0;
        flex: 1 1 auto;
    }

    .rail-item-two { align-items: flex-start; padding-top: 6px; padding-bottom: 6px; }

    .rail-people {
        overflow: hidden;
        text-overflow: ellipsis;
        white-space: nowrap;
        font-size: 0.72rem;
        font-weight: 400;
        color: var(--text-muted);
    }

    /* The current conversation is a filled terracotta row, where muted
       grey is unreadable. */
    .rail-item.is-current .rail-people { color: rgba(255, 255, 255, 0.85); }

    .channel-rail::-webkit-scrollbar { width: 8px; }
    .channel-rail::-webkit-scrollbar-track { background: transparent; }
    .channel-rail::-webkit-scrollbar-thumb {
        background: var(--scrollbar);
        border-radius: 999px;
        border: 2px solid var(--bg);
        background-clip: padding-box;
    }
}

/* --- channel list -------------------------------------------------- */

.channel-list {
    list-style: none;
    margin: 0;
    padding: 0;
}

/* Where "needs you" ends and the address book begins. Quiet on purpose:
   it is a divider, not a section anybody navigates to. */
.list-heading {
    margin: 22px 0 10px;
    color: var(--text-muted);
    font-size: 0.78rem;
    font-weight: 600;
    letter-spacing: 0.06em;
    text-transform: uppercase;
}

.directory-filter {
    width: 100%;
    min-height: var(--touch-min);
    margin-bottom: 16px;
    padding: 10px 14px;
    background: var(--bg-input);
    color: var(--text);
    border: 1px solid var(--border);
    border-radius: 10px;
    font: inherit;
}

.directory-filter:focus-visible {
    border-color: var(--terracotta);
    outline: none;
}


/* --- channel tabs ----------------------------------------------------- */

/* Nachrichten · the channel's pages · Medien. Scrolls sideways rather
   than wrapping: on a phone four tabs will not fit, and a strip that
   wraps to two lines eats the conversation's space permanently while a
   strip that scrolls costs nothing until you use it.

   THE STRIP SHARES THE CHANNEL TITLE'S ROW, INSIDE THE HEADER. It used
   to be the first thing in the scrolling column, which meant it only
   existed at the very top of a channel: scroll down to read and the way
   to the Passwörter page went with it, half-covered by the sticky
   header on the way out. It also sat below the terracotta rule, on the
   conversation's side of a line that divides chrome from content.

   Then it had a row to itself and was reported "too far to the left" —
   correctly: a strip pinned to the window's edge belongs to the window,
   while a strip beside "← Allgemein" belongs to Allgemein. Sharing the
   row also gives the conversation a line of screen back. Both reported
   2026-08-11.

   No border and no padding of its own: the header's rule is the
   divider, and .app-context supplies the spacing. */
.channel-tabs {
    display: flex;
    align-items: center;
    gap: 4px;
    /* ON A PHONE THE STRIP TAKES A LINE OF ITS OWN. Sharing the title's
       row is right on a desktop and unusable on a 390px screen: the
       channel name shortened to "Yumas E…", which is the half that all
       three restaurants share, and the tabs still arrived cut — "chten"
       and "Mec". Two things fighting over one row and both losing.
       Reported 2026-08-12 with a screenshot.

       flex-basis 100% forces the wrap; the title above it then has the
       full width and stops eliding. */
    flex: 1 0 100%;
    min-width: 0;
    overflow-x: auto;
    scrollbar-width: none;
}

.channel-tabs::-webkit-scrollbar { display: none; }

/* The tabs were briefly draggable. They are not any more: the strip
   lives in a sticky header, the tabs are links, and a browser would
   rather drag a link out as a URL than move it. Reordering is a proper
   list under Bearbeiten, which is where the channel list already does
   it and where it already works. */

/* On a desktop the tabs go back beside the title. This block has to
   live AFTER the base rule above: a media query adds no specificity, so
   between two rules of equal weight the later one wins wherever both
   apply. Written higher up it lost, and 100% flex-basis then squeezed
   the channel name down to "Al…" on a 1500px screen. */
@media (min-width: 900px) {
    .app-context { flex-wrap: nowrap; }

    .channel-tabs {
        flex: 1 1 auto;
        margin-left: 4px;
    }

    /* The name keeps its full width and the tabs take what is left.
       A channel is identified by its name — "Yumas E…" is the half all
       three restaurants share, so eliding it is worse than useless. */
    .context-title {
        flex: 0 0 auto;
        min-width: 0;
    }
}

.channel-tab {
    flex: 0 0 auto;
    display: inline-flex;
    align-items: center;
    min-height: 40px;
    padding: 0 12px;
    border-bottom: 2px solid transparent;
    color: var(--text-muted);
    font-size: 0.9rem;
    text-decoration: none;
    white-space: nowrap;
}

.channel-tab:hover { color: var(--text); }

.channel-tab.is-current {
    color: var(--text);
    border-bottom-color: var(--terracotta);
    font-weight: 600;
}

/* The name field only takes room once it has focus — an always-visible
   text box in a row of tabs reads as clutter to the fifty people who
   will never add a page. */
.channel-tab-add {
    display: flex;
    align-items: center;
    gap: 4px;
    flex: 0 0 auto;
}

.channel-tab-add input {
    width: 0;
    min-width: 0;
    padding: 0;
    border: 0;
    background: none;
    color: var(--text);
    font: inherit;
    /* Never under 16px, or iOS zooms the page the moment this opens —
       and opening it is the whole point of the control. */
    font-size: max(16px, 0.9rem);
    transition: width 0.12s ease, padding 0.12s ease;
}

.channel-tab-add:focus-within input {
    width: 9rem;
    padding: 6px 10px;
    background: var(--bg-input);
    border: 1px solid var(--border);
    border-radius: 8px;
}

.channel-tab-plus {
    background: none;
    border: 0;
    border-bottom: 2px solid transparent;
    color: var(--text-muted);
    font-size: 1.2rem;
    cursor: pointer;
    padding: 0 10px;
}

.channel-tab-plus:hover { color: var(--text); }

/* The strip belongs to the header, so the sticky date pill and the
   scroll-to-bottom maths must clear it too. */
.page-actions {
    display: flex;
    flex-wrap: wrap;
    gap: 8px;
    align-items: center;
    margin-top: 20px;
}

/* --- uploading -------------------------------------------------------- */

/* A <progress>, same as the storage bar and for the same reason: the CSP
   forbids the inline width an ordinary div would need. Without a value
   it renders indeterminate, which is exactly what the server-side
   processing step should look like. */
.upload-bar {
    -webkit-appearance: none;
    appearance: none;
    display: block;
    width: 100%;
    height: 6px;
    margin-top: 10px;
    background: var(--bg-input);
    border: 0;
    border-radius: 999px;
    overflow: hidden;
}

.upload-bar::-webkit-progress-bar   { background: var(--bg-input); }
.upload-bar::-webkit-progress-value { background: var(--terracotta); }
.upload-bar::-moz-progress-bar      { background: var(--terracotta); }

.upload-label { margin: 6px 0 0; }

/* --- video ------------------------------------------------------------ */

.attachment-video {
    display: block;
    width: 100%;
    max-width: 460px;
    max-height: 70vh;
    margin-top: 8px;
    background: var(--video-letterbox);
    border-radius: 10px;
}

/* A video tile in the media grid needs a stacking context for the play
   badge sitting on top of it. */
.media-video { position: relative; display: block; }

.media-play {
    position: absolute;
    inset: 0;
    display: flex;
    align-items: center;
    justify-content: center;
    color: #fff;
    /* Never swallows the tap meant for the tile underneath. */
    pointer-events: none;
    text-shadow: 0 1px 6px rgba(0, 0, 0, 0.7);
}

/* When the browser could not draw a poster on upload — a plain tile
   rather than a broken image icon. */
.media-blank {
    display: block;
    width: 100%;
    aspect-ratio: 1;
    background: var(--bg-input);
}

/* --- @mentions -------------------------------------------------------- */

/* Drawn only where the server decided to notify somebody — see
   Mentions. A typed name that matched nobody stays plain text on
   purpose, so this highlight always means "they were told". */
.mention {
    padding: 1px 5px;
    background: var(--mention-bg);
    border-radius: 5px;
    color: var(--text);
    font-weight: 600;
}

/* A MENTION IS A DOOR TO THAT PERSON (2026-09-01). "fragt gerne
   @Viviana" is an instruction, and tapping her name opens the
   conversation where you can actually ask her.

   No underline: forty of them down a busy channel would look like a
   page of links rather than a conversation. The pill already reads as
   an object, and the pointer plus the hover say the rest. */
a.mention {
    text-decoration: none;
    cursor: pointer;
}

a.mention:hover,
a.mention:focus-visible {
    background: var(--moosgruen);
    color: var(--text);
}

/* Being mentioned yourself should be findable while scrolling past
   forty messages, so it is louder than a mention of somebody else. */
.message-mentions-me {
    border-left: 3px solid var(--terracotta);
}

/* .composer is sticky and .composer-static is relative (below), so both
   are a containing block for this. Anchored ABOVE the box rather than
   below it: the composer sits at the bottom of the screen on a phone,
   and a menu under it would open off the edge. */
.mention-menu {
    position: absolute;
    left: 0;
    bottom: 100%;
    z-index: 20;
    min-width: 210px;
    max-width: min(90vw, 320px);
    margin: 4px 0 0;
    padding: 4px;
    list-style: none;
    background: var(--bg-raised);
    border: 1px solid var(--border);
    border-radius: 10px;
    box-shadow: var(--card-shadow);
}

.mention-option {
    display: block;
    width: 100%;
    min-height: var(--touch-min);
    padding: 8px 12px;
    background: none;
    border: 0;
    border-radius: 7px;
    color: var(--text);
    font: inherit;
    text-align: left;
    cursor: pointer;
}

.mention-option.is-active,
.mention-option:hover {
    background: var(--bg-input);
}

/* --- search ---------------------------------------------------------- */

.app-search {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    min-width: var(--touch-min);
    min-height: var(--touch-min);
    color: var(--text-muted);
}

.app-search:hover { color: var(--text); }

.search-row {
    display: flex;
    gap: 10px;
}

.search-row input[type="search"] {
    flex: 1;
    min-width: 0;
    min-height: var(--touch-min);
    padding: 10px 14px;
    background: var(--bg-input);
    color: var(--text);
    border: 1px solid var(--border);
    border-radius: 10px;
    font: inherit;
}

.search-row .btn { width: auto; margin: 0; }

.search-filters { margin-top: 12px; }

.search-filters summary {
    color: var(--text-muted);
    font-size: 0.88rem;
    cursor: pointer;
    padding: 6px 0;
}

.search-grid {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(min(100%, 190px), 1fr));
    gap: 12px;
    margin: 10px 0;
}

.search-grid label {
    display: block;
    margin-bottom: 4px;
    font-size: 0.82rem;
    color: var(--text-muted);
}

.search-grid select,
.search-grid input[type="date"] {
    width: 100%;
    min-height: var(--touch-min);
    padding: 8px 10px;
    background: var(--bg-input);
    color: var(--text);
    border: 1px solid var(--border);
    border-radius: 8px;
    font: inherit;
}

.search-count { margin: 20px 0 10px; }

.search-results {
    list-style: none;
    margin: 0;
    padding: 0;
}

.search-hit {
    display: block;
    padding: 12px 14px;
    margin-bottom: 8px;
    background: var(--bg-raised);
    border: 1px solid var(--border);
    border-left: 3px solid var(--terracotta);
    border-radius: 10px;
    color: var(--text);
    text-decoration: none;
}

.search-hit:hover { border-color: var(--text-muted); }

.search-meta {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 8px;
    margin-bottom: 4px;
    font-size: 0.8rem;
    color: var(--text-muted);
}

.search-meta strong { color: var(--text); font-weight: 600; }

.search-where {
    padding: 1px 8px;
    border: 1px solid var(--border);
    border-radius: 999px;
}

.search-snippet {
    display: block;
    line-height: 1.5;
}

/* The match itself. Readable in both themes rather than the browser's
   default yellow, which disappears against a dark card. */
.search-snippet mark {
    padding: 0 2px;
    background: var(--terracotta);
    color: #fff;
    border-radius: 3px;
}

.pager {
    display: flex;
    align-items: center;
    justify-content: center;
    gap: 14px;
    margin: 20px 0;
}

.pager .btn { width: auto; margin: 0; }

.channel {
    display: block;
    /* Comfortably above the 44px floor: this is the main navigation of
       the whole app, tapped one-handed mid-shift. */
    min-height: 60px;
    padding: 14px 16px;
    margin-bottom: 10px;
    background: var(--bg-raised);
    border: 1px solid var(--border);
    border-left: 4px solid var(--terracotta);
    border-radius: var(--radius);
    box-shadow: var(--card-shadow);
    color: var(--text);
    text-decoration: none;
}

.channel-name {
    display: block;
    font-weight: 600;
    font-size: 1.05rem;
}

.channel-topic {
    display: block;
    margin-top: 2px;
    color: var(--text-muted);
    font-size: 0.9rem;
}

/* --- channel view -------------------------------------------------- */

.channel-head {
    display: flex;
    align-items: flex-start;
    gap: 12px;
    margin-bottom: 16px;
}

.back {
    display: flex;
    align-items: center;
    justify-content: center;
    min-width: var(--touch-min);
    min-height: var(--touch-min);
    margin-left: -8px;
    color: var(--text);
    font-size: 1.5rem;
    text-decoration: none;
}

/* Pushed to the right of the channel title. */
.head-action {
    display: flex;
    align-items: center;
    justify-content: center;
    min-width: var(--touch-min);
    min-height: var(--touch-min);
    margin-left: auto;
    flex: 0 0 auto;
    color: var(--text);
    opacity: 0.7;
    text-decoration: none;
}

.head-action:active { opacity: 1; }

/* A worded action rather than an icon: "Passwörter" is not a thing with
   a symbol anyone would recognise, and guessing at one costs more than
   the space the word takes. */
.head-action-text {
    min-width: 0;
    padding: 0 10px;
    font-size: 0.85rem;
    font-weight: 600;
    white-space: nowrap;
}

/* --- channel pages --------------------------------------------------- */

.page-content { margin-top: 4px; }

.page-body {
    margin: 0;
    /* Codes and passwords are read character by character, so the text
       must not be squeezed. */
    line-height: 1.6;
    overflow-wrap: anywhere;
}

/* --- formatting produced by Markup ---------------------------------- */

.message-body code,
.page-body code {
    padding: 1px 5px;
    background: var(--bg-input);
    border: 1px solid var(--border);
    border-radius: 4px;
    /* Monospace so a password is read character by character — the whole
       reason code spans exist here. */
    font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
    font-size: 0.92em;
    overflow-wrap: anywhere;
}

.page-body h2, .page-body h3, .page-body h4 { margin: 20px 0 8px; }
.page-body h2 { font-size: 1.4rem; }
.page-body h3 { font-size: 1.15rem; }
.page-body h4 { font-size: 1rem; }
.page-body > :first-child { margin-top: 0; }
.page-body p { margin: 0 0 12px; }

.md-list { margin: 0 0 12px; padding-left: 20px; }
.md-list li { margin-bottom: 4px; }

/* A table must not widen the page on a phone: it scrolls inside its own
   box instead. */
.md-table-wrap {
    overflow-x: auto;
    margin: 0 0 16px;
    -webkit-overflow-scrolling: touch;
}

.md-table {
    border-collapse: collapse;
    width: 100%;
    min-width: 320px;
    font-size: 0.95rem;
}

.md-table th,
.md-table td {
    padding: 8px 12px;
    border: 1px solid var(--border);
    text-align: left;
    vertical-align: top;

    /* `anywhere`, WITH A FLOOR UNDER THE COLUMN — and the floor is what
       makes it safe.
       Both values break a word that will not fit; only `anywhere` also
       counts the broken pieces when the browser decides how narrow a
       column may be. That is why it used to shatter short words:
       "OpenTable" as "Ope / nTa / ble" (2026-08-20). But `break-word`
       bought that safety by keeping every column at least as wide as its
       longest word — and one 80-character login URL then forces a table
       far wider than the screen, which is the scrollbar reported on
       2026-09-01.
       min-width says the thing that was actually meant: a column may not
       be narrower than about one short word. Under that floor `anywhere`
       breaks the URL and leaves "OpenTable" alone. */
    overflow-wrap: anywhere;
    min-width: 7rem;
}

.md-table th {
    background: var(--bg-input);
    font-weight: 600;
    white-space: nowrap;
}

/* A WIDE TABLE MAY USE THE ROOM THE LAYOUT IS NOT USING.
   The app is centred inside 74rem, which on a large monitor leaves a few
   hundred pixels idle down each side while a password table scrolls
   sideways in the middle of it — "there would still be plenty of space
   to display quite a bit more of text in width" (2026-09-01).

   So a table, and only a table, grows rightwards into that slack. Prose
   keeps its comfortable measure, which is the reason for the 74rem in
   the first place; nothing moves leftwards, because the channel rail is
   there. Below 78rem there is no slack to take and this does nothing. */
@media (min-width: 78rem) {
    .app-layout .md-table-wrap {
        width: min(
            calc(100% + (100vw - 74rem) / 2 - 20px),
            96rem
        );
    }
}

.md-table tbody tr:nth-child(even) { background: var(--tint-soft); }

/* ---------------------------------------------------------------------
   A WIDE TABLE ON A PHONE BECOMES A LIST OF ENTRIES.

   Four columns in 375px is about 80px each, which is narrower than the
   words going into them, so the browser breaks inside the words:
   "OpenTable" arrives as "Ope / nTa / ble". Reported 2026-08-20 on the
   Passwörter page.

   THE ALTERNATIVE WAS HORIZONTAL SCROLLING, and it was rejected on
   purpose. It keeps the grid, but reading one password then means
   dragging sideways and losing sight of which app you were on — on the
   one device where this page is actually used, standing in a restaurant.
   The question somebody brings to this page is "what is the login for
   OpenTable", which is one entry at a time, and that is what this shape
   answers.

   Each cell carries its column's name (data-label, set in Markup.php)
   and CSS draws it beside the value. Nothing is hidden and nothing is
   truncated — the whole table is still there, in a shape that fits.

   TWO-COLUMN TABLES ARE LEFT AS TABLES: they already fit, and stacking
   them would turn a tidy pair into two lines for no gain. Markup.php
   decides, because it knows the column count and CSS cannot count.
   --------------------------------------------------------------------- */
@media (max-width: 560px) {
    .md-stack,
    .md-stack tbody,
    .md-stack tr,
    .md-stack td {
        display: block;
        width: auto;
    }

    /* The header row's job is done by the per-cell labels below. */
    .md-stack thead {
        position: absolute;
        width: 1px;
        height: 1px;
        overflow: hidden;
        clip-path: inset(50%);
        white-space: nowrap;
    }

    .md-stack { min-width: 0; border-collapse: separate; }

    /* One entry, one card. */
    .md-stack tbody tr {
        margin: 0 0 10px;
        padding: 10px 12px;
        border: 1px solid var(--border);
        border-radius: var(--radius);
        background: var(--bg-raised);
    }

    /* The striping meant "which row am I on"; separate cards say it
       better, and a tinted every-other card would just look arbitrary. */
    .md-stack tbody tr:nth-child(even) { background: var(--bg-raised); }

    /* GRID, NOT FLEX, and minmax(0, …) is the point of the change.
       As a flex row the value was an anonymous item, which refuses to
       shrink below its content — so a long login URL ran straight out of
       the card and off the right of the screen (2026-09-01). A grid
       track declared minmax(0, 1fr) is allowed to be narrower than what
       is in it, and the break below then does the rest.

       `anywhere` is safe here in a way it is not in the desktop table:
       a card has no columns to squeeze, so there is nothing for a short
       word to shatter against. */
    .md-stack td {
        display: grid;
        grid-template-columns: 38% minmax(0, 1fr);
        gap: 10px;
        padding: 3px 0;
        border: 0;
        min-width: 0;
        overflow-wrap: anywhere;
    }

    .md-stack td::before {
        content: attr(data-label);
        color: var(--text-muted);
        font-size: 0.85rem;
    }

    /* THE FIRST COLUMN NAMES THE ENTRY, so it is the card's heading
       rather than another labelled field: "App: OpenTable" reads as a
       form, "OpenTable" reads as the thing you were looking for. */
    .md-stack td:first-child {
        display: block;
        margin-bottom: 6px;
        font-weight: 600;
    }

    .md-stack td:first-child::before { content: none; }

    /* An empty cell — the Info column on most rows — has nothing to say,
       and a label with a blank beside it is worse than silence. */
    .md-stack td:empty { display: none; }
}

/* --- editing a page -------------------------------------------------- */

.page-help {
    margin: 6px 0 0;
    font-size: 0.8rem;
    line-height: 1.6;
}

/* --- formatting buttons ---------------------------------------------- */

.format-bar {
    display: flex;
    flex-wrap: wrap;
    gap: 4px;
    margin-bottom: 6px;
}

.format-btn {
    min-width: 38px;
    height: 38px;
    padding: 0 10px;
    background: var(--bg-input);
    border: 1px solid var(--border);
    border-radius: 8px;
    color: var(--text);
    font: inherit;
    font-size: 0.9rem;
    line-height: 1;
    cursor: pointer;
}

/* A formatting button shows what it does: F is bold, U is underlined, S
   is struck through, ‹› is monospace. These were inline style attributes
   until 2026-08-19, which this app's own CSP blocks — so they rendered
   plain and the console collected violations nobody reads.

   AFTER .format-btn, NOT BEFORE IT. Both are single-class selectors, so
   whichever comes last wins — and .format-btn sets `font: inherit`, a
   shorthand that resets font-weight along with everything else. Written
   above it, U and S worked and F was not bold, which is the same
   ordering trap the channel tabs hit in August. */
.fmt-bold      { font-weight: 700; }
.fmt-underline { text-decoration: underline; }
.fmt-strike    { text-decoration: line-through; }
.fmt-code      { font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; }

.format-btn:active { border-color: var(--moosgruen); border-width: 2px; }

/* On, because the caret is inside text of that style. Filled rather than
   merely outlined: at a glance, on a phone, in a dim room, a border
   change is not enough of a difference to read (§8.4). */
.format-btn.is-active {
    background: var(--moosgruen);
    border-color: var(--moosgruen);
    color: #ffffff;
}

/* Sits above the box when open; [hidden] takes it out of the flow
   entirely so nothing is left behind when it is closed. */
.format-bar-collapsible[hidden] { display: none; }

/* Same size and shape as the "+" beside it — two controls that do the
   same kind of job should not look like different species. */
.format-toggle {
    display: flex;
    align-items: center;
    justify-content: center;
    flex: 0 0 auto;
    width: 44px;
    height: 44px;
    background: transparent;
    border: 1px solid var(--border);
    border-radius: 8px;
    color: var(--text-muted);
    font: inherit;
    font-size: 0.95rem;
    font-weight: 600;
    cursor: pointer;
}

.format-toggle.is-on {
    border-color: var(--moosgruen);
    color: var(--text);
}

/* Holds the Schichtanfrage and Wichtig switches. [hidden] alone is enough
   here — unlike the format bar, nothing sets a display on it. */
.send-options[hidden] { display: none; }
.send-options { margin-top: 8px; }

.send-toggle svg { display: block; }

/* --- management screens ---------------------------------------------- */

select {
    width: 100%;
    min-height: var(--touch-min);
    padding: 10px 12px;
    background: var(--bg-input);
    border: 1px solid var(--border);
    border-radius: 8px;
    color: var(--text);
    font: inherit;
    font-size: 16px;
}

.admin-title { margin-bottom: 2px; }

.admin-new { margin-top: 8px; }

.admin-new > summary {
    display: flex;
    align-items: center;
    min-height: var(--touch-min);
    padding: 0 14px;
    background: var(--moosgruen);
    border-radius: 8px;
    color: #ffffff;
    font-weight: 600;
    cursor: pointer;
    list-style: none;
}

.admin-new > summary::-webkit-details-marker { display: none; }
.admin-new[open] > summary { margin-bottom: 8px; }

.admin-list { list-style: none; margin: 0; padding: 0; }

.admin-row {
    display: flex;
    flex-direction: column;
    gap: 4px;
    padding: 12px 14px;
    margin-bottom: 8px;
    background: var(--bg-raised);
    border: 1px solid var(--border);
    border-left: 3px solid var(--terracotta);
    border-radius: var(--radius);
    color: var(--text);
    text-decoration: none;
}

/* Deactivated accounts stay listed but recede — they are history, not
   staff, and hiding them would make an ex-employee's access invisible
   rather than absent. */
.admin-row.is-inactive { opacity: 0.55; border-left-color: var(--border); }

.admin-row-main strong { display: block; }
.admin-row-main em { font-style: normal; font-size: 0.85rem; color: var(--text-muted); }

.admin-row-tags {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 6px;
    font-size: 0.8rem;
}

.tier-badge {
    padding: 1px 8px;
    border-radius: 999px;
    background: var(--bg-input);
    border: 1px solid var(--border);
    font-size: 0.72rem;
    text-transform: uppercase;
    letter-spacing: 0.04em;
}

.tier-master, .tier-admin { background: var(--terracotta); border-color: var(--terracotta); color: #fff; }
.tier-off { background: var(--urgent-fill); border-color: var(--urgent-fill); color: #fff; }

/* The activation checklist. Green means an urgent request would reach
   this person; amber means it would not, and the badge says why. */
.tier-ok   { background: var(--moosgruen); border-color: var(--moosgruen); color: #fff; }
.tier-warn { background: transparent; border-color: var(--terracotta); color: var(--terracotta); }

/* "gerade aktiv" — the only live thing on the page, so it should not
   look like the dates beside it. */
.pill-live {
    margin-left: 0;
    border-color: var(--moosgruen);
    color: var(--moosgruen);
}

.tag-group {
    margin: 18px 0 0;
    padding: 12px 14px;
    border: 1px solid var(--border);
    border-radius: var(--radius);
}

.tag-group legend {
    padding: 0 6px;
    font-size: 0.78rem;
    text-transform: uppercase;
    letter-spacing: 0.06em;
    color: var(--text-muted);
}

.tag-check {
    display: flex;
    align-items: center;
    gap: 10px;
    min-height: var(--touch-min);   /* §8.4 — tapped on a phone */
    margin: 0;
    font-weight: 400;
}

.tag-check input { width: 20px; height: 20px; accent-color: var(--moosgruen); }

/* Two columns on anything wider than a phone: fifty-five names in one
   column is a scroll nobody finishes. */
@media (min-width: 560px) {
    .tag-group, .person-picker { columns: 2; column-gap: 24px; }
    .tag-group legend, .person-picker summary { column-span: all; }
    .tag-group .page-help { column-span: all; }

    /* AND A NOTICE, which is about the whole fieldset and not about one
       column of it (2026-09-27). Without this the green box was
       fragmented down the middle: two half-sentences in two green boxes,
       staggered, because the columns end at different heights. Reported
       with a screenshot of the member screen — "this part looks a little
       bit funny". A fragment of a sentence in a coloured box reads as a
       rendering fault, which is what it was. */
    .tag-group .msg { column-span: all; }

    /* The search box belongs over both columns, not at the top of the
       left one, and so does the line that says it found nobody.

       DISPLAY:BLOCK IS NOT DECORATION (2026-09-27). column-span only
       applies to block-level boxes, and an <input> is inline-level — so
       without this the rule is quietly ignored and the box takes a slot
       in the left column, reading as the first name in the list. Which
       is exactly how it shipped: "the search bar has the same place as
       an individual name". The <p> below it is block already and did
       span; the input is the one that needs saying. */
    .tag-group .people-search, .tag-group .people-none,
    .person-picker .people-search, .person-picker .people-none {
        display: block;
        column-span: all;
    }

    .tag-check { break-inside: avoid; }
}

/* The foldout for naming individuals. Closed by default — most requests
   go to a tag and nothing else, and fifty-five checkboxes above the send
   button would make the common case feel like the rare one. */
.person-picker {
    margin: 18px 0 0;
    padding: 12px 14px;
    border: 1px solid var(--border);
    border-radius: var(--radius);
}

.person-picker summary {
    display: flex;
    align-items: center;
    min-height: var(--touch-min);
    cursor: pointer;
    font-size: 0.9rem;
    font-weight: 600;
}

.person-picker summary::marker { color: var(--moosgruen); }

/* ---- Directed shift requests ------------------------------------- */

.request-list { list-style: none; margin: 16px 0 0; padding: 0; }

.request-card {
    margin-bottom: 14px;
    padding: 14px 16px;
    background: var(--bg-raised);
    border: 1px solid var(--border);
    /* The same left edge the urgent messages carry in a channel, so a
       request looks like a request wherever it is seen. */
    border-left: 3px solid var(--terracotta);
    border-radius: var(--radius);
}

/* Settled ones stay readable but stop competing with the open ones. */
.request-list-done .request-card { border-left-color: var(--border); opacity: 0.7; }

/* ---- reported conversations --------------------------------------- */

.report-list { list-style: none; margin: 16px 0 0; padding: 0; }

.report {
    margin-bottom: 14px;
    padding: 14px 16px;
    background: var(--bg-raised);
    border: 1px solid var(--border);
    border-left: 3px solid var(--terracotta);
    border-radius: var(--radius);
}

.report-done { border-left-color: var(--border); opacity: 0.65; }

.report-actions {
    display: flex;
    align-items: center;
    gap: 14px;
    flex-wrap: wrap;
    margin-top: 10px;
}

/* ---- notification settings ---------------------------------------- */

.quiet-times { margin: 10px 0 0; }

.time-pair { display: flex; gap: 16px; flex-wrap: wrap; }
.time-pair > span { display: flex; flex-direction: column; gap: 4px; }

.time-pair input[type="time"] {
    /* Wide enough for the native clock control, which several browsers
       draw larger than a text field. */
    min-width: 8rem;
    min-height: var(--touch-min);
}

/* Sits at the very top of the home screen, so it does not get the
   breathing room a heading further down would want. */
.waiting { margin-bottom: 4px; }
.waiting .section-heading { margin-top: 2px; }

.section-heading {
    margin: 28px 0 0;
    font-size: 0.78rem;
    text-transform: uppercase;
    letter-spacing: 0.06em;
    color: var(--text-muted);
}

/* Shown once, so it has to be readable and selectable at a glance. */
.password-reveal { margin: 6px 0; }

.password-reveal code {
    display: inline-block;
    padding: 8px 14px;
    background: var(--code-bg);
    border-radius: 8px;
    font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
    font-size: 1.05rem;
    letter-spacing: 0.06em;
    user-select: all;
}

.order-list { list-style: none; margin: 16px 0 0; padding: 0; }

.order-row {
    display: flex;
    align-items: center;
    gap: 10px;
    padding: 8px 12px;
    margin-bottom: 6px;
    background: var(--bg-raised);
    border: 1px solid var(--border);
    border-radius: 8px;
}

.order-name { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.order-actions { display: flex; gap: 4px; }
.order-actions form { margin: 0; }

.order-btn {
    width: var(--touch-min);
    height: var(--touch-min);
    background: var(--bg-input);
    border: 1px solid var(--border);
    border-radius: 8px;
    color: var(--text);
    font-size: 1.1rem;
    cursor: pointer;
}

/* Same control, but a word rather than an arrow, so it needs its own
   width and has to sit its text on the middle line like the buttons do. */
.order-btn-text {
    display: inline-flex;
    align-items: center;
    width: auto;
    padding: 0 14px;
    font-size: 0.9rem;
    text-decoration: none;
    white-space: nowrap;
}

.order-btn-text:hover { border-color: var(--text-muted); }

.order-btn:disabled { opacity: 0.3; cursor: default; }

/* --- back to the newest message -------------------------------------- */

.to-bottom {
    position: fixed;
    /* Above the composer, and clear of the home indicator on an iPhone. */
    right: 16px;
    bottom: calc(96px + env(safe-area-inset-bottom));
    z-index: 30;
    display: flex;
    align-items: center;
    justify-content: center;
    width: var(--touch-min);
    height: var(--touch-min);
    padding: 0;
    border: 1px solid var(--border);
    border-radius: 50%;
    background: var(--bg-raised);
    color: var(--text);
    box-shadow: var(--shadow-md);
    cursor: pointer;
}

.to-bottom:active { background: var(--bg-input); }

.page-editor {
    width: 100%;
    padding: 10px 12px;
    background: var(--bg-input);
    border: 1px solid var(--border);
    border-radius: 8px;
    color: var(--text);
    font: inherit;
    font-size: 16px;
    line-height: 1.6;
    resize: vertical;
}

.page-editor:focus { border-color: var(--moosgruen); border-width: 2px; outline: none; }

/* --- the block editor: prose and tables, in order -------------------- */
/* A page is a heading, a table, a note, another table. The editor is
   that list rather than one box of pipe-delimited text, which is what
   was asked for on 2026-08-12 with a screenshot of Slack's canvas. */

.block-editor { margin: 0 0 12px; }

.block {
    position: relative;
    margin: 0 0 18px;
    padding: 12px 12px 8px;
    background: var(--bg-raised);
    border: 1px solid var(--border);
    border-radius: 10px;
}

/* Only a hint of a box: the blocks are scaffolding, and a page with
   four heavy frames on it reads as a form rather than as a page. */
.block-text { padding-bottom: 12px; }

/* SMALL AND QUIET, never .btn. That class is display:block, width:100%
   and a filled background — right for "Speichern" at the foot of a
   form, absurd for "Zeile". A screenshot on 2026-08-12 showed five of
   them stacked down the page like traffic lights. */
.block-remove,
.block-add-btn {
    min-height: 34px;
    padding: 0 12px;
    background: transparent;
    border: 1px solid var(--border);
    border-radius: 8px;
    color: var(--text-muted);
    font: inherit;
    font-size: 0.85rem;
    cursor: pointer;
}

.block-remove:hover,
.block-add-btn:hover {
    border-color: var(--text-muted);
    color: var(--text);
}

.block-add {
    display: flex;
    gap: 8px;
    margin: 0 0 18px;
}

.tbl-scroll {
    overflow-x: auto;
    -webkit-overflow-scrolling: touch;
}

.tbl-edit {
    border-collapse: collapse;
    width: 100%;
}

.tbl-edit td,
.tbl-edit th {
    padding: 0;
    border: 1px solid var(--border);
}

/* The two control lanes carry buttons, not data, so they get no grid. */
.tbl-edit .tbl-ctrl th,
.tbl-edit .tbl-rowctrl {
    border: 0;
    width: 32px;
    text-align: center;
}

.tbl-ctrl th { height: 26px; }

.tbl-cell {
    display: block;
    width: 100%;
    min-width: 8rem;
    padding: 10px 10px;
    background: var(--bg-input);
    border: 0;
    color: var(--text);
    font: inherit;
    /* 16px exactly: anything smaller and iOS zooms the page when the
       box takes focus, which on a table means losing your place. */
    font-size: 16px;
}

.tbl-cell:focus {
    outline: 2px solid var(--moosgruen);
    outline-offset: -2px;
}

.tbl-head-row .tbl-cell {
    background: var(--bg-raised);
    font-weight: 600;
}

/* Visible, not on hover. Half the people who use this are on a phone
   in a kitchen, and a control that only exists under a mouse pointer
   does not exist for them (§8.4). */
.tbl-x {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 26px;
    height: 26px;
    padding: 0;
    background: none;
    border: 0;
    border-radius: 6px;
    color: var(--text-muted);
    font-size: 1.1rem;
    line-height: 1;
    cursor: pointer;
}

.tbl-x:hover,
.tbl-x:focus {
    background: var(--bg-input);
    color: var(--text);
}

/* The add buttons run along the edges they act on: a thin column down
   the right side adds a column, a thin bar under the table adds a row.
   The shape is the label — which is why the glyph is only "+" and the
   name lives in the tooltip. */
.tbl-with-side {
    display: flex;
    align-items: stretch;
    gap: 6px;
}

.tbl-with-side .tbl-scroll { flex: 1 1 auto; min-width: 0; }

.tbl-add-col,
.tbl-add-row {
    background: transparent;
    border: 1px dashed var(--border);
    border-radius: 8px;
    color: var(--text-muted);
    font: inherit;
    font-size: 1rem;
    line-height: 1;
    cursor: pointer;
}

.tbl-add-col:hover,
.tbl-add-row:hover {
    border-style: solid;
    border-color: var(--moosgruen);
    color: var(--text);
}

.tbl-add-col {
    /* Wide enough for a thumb, narrow enough not to read as a column
       of the table itself. */
    width: 30px;
    flex: 0 0 30px;
    padding: 0;
}

.tbl-add-row {
    width: 100%;
    height: 30px;
    margin-top: 6px;
    /* Stops short of the right edge so it lines up with the table and
       not with the add-column strip beside it. */
    max-width: calc(100% - 36px);
    padding: 0;
}

.tbl-tools {
    display: flex;
    flex-wrap: wrap;
    gap: 8px;
    margin-top: 10px;
}

.tbl-tools .block-remove { margin-left: auto; }

.page-files {
    list-style: none;
    margin: 0;
    padding: 0;
    display: flex;
    flex-direction: column;
    gap: 8px;
}

.page-files li {
    display: flex;
    align-items: center;
    gap: 8px;
}

.page-files .attachment-doc { flex: 1; }
.page-files form { margin: 0; }

/* --- media overview -------------------------------------------------- */

.media-grid {
    list-style: none;
    margin: 0;
    padding: 0;
    display: grid;
    /* Square thumbnails that reflow: three across on a phone, more on a
       wide screen, without a breakpoint for each size. */
    grid-template-columns: repeat(auto-fill, minmax(104px, 1fr));
    gap: 6px;
}

.media-grid a {
    display: block;
    aspect-ratio: 1;
    border-radius: 8px;
    overflow: hidden;
    background: var(--bg-input);
}

.media-grid img {
    width: 100%;
    height: 100%;
    object-fit: cover;
}

.media-docs {
    list-style: none;
    margin: 0;
    padding: 0;
    display: flex;
    flex-direction: column;
    gap: 8px;
}

.media-docs .attachment-doc { width: 100%; }

.messages {
    list-style: none;
    margin: 0;
    padding: 0;
}

/* The date, stated once per day rather than on every message. Sticky so
   it stays legible while scrolling through a long day — you can always
   see which day you are looking at without scrolling back. */
/* NOT STICKY — and that was the bug. Every separator is a child of the
   same <ol>, so each one pinned at the same offset for the whole length
   of the list and they drew on top of each other: "Dienstag" over
   "Gestern" over "August". Sticky headers only hand over to one another
   when each lives in its own containing block, which a flat message list
   does not give them.

   The date that follows you down the page is now ONE floating pill
   (.day-float), driven by app.js. One element cannot overlap itself.
   These separators just sit in the flow where they belong. */
.day-separator {
    display: flex;
    align-items: center;
    gap: 12px;
    margin: 26px 0 14px;
}

/* The first separator does not need the full gap above it — nothing is
   scrolling past yet. */
.messages > .day-separator:first-child { margin-top: 8px; }

.day-separator::before,
.day-separator::after {
    content: '';
    flex: 1;
    height: 1px;
    background: var(--border);
}

.day-separator span,
.day-float span,
.day-jump {
    padding: 3px 12px;
    background: var(--bg-raised);
    border: 1px solid var(--border);
    border-radius: 999px;
    color: var(--text-muted);
    font-size: 0.8rem;
    white-space: nowrap;
}

/* ---------------------------------------------------------------------
   "Nuria ist dazugekommen".

   MOOSGRÜN AS A FILL, WHICH IS THE ONLY WAY THIS PALETTE ALLOWS IT. The
   owner asked for the moss green and it is the right choice — it is the
   app's one green (RAL 6005, §8.1), and arriving somewhere is the most
   quietly positive thing that happens in a channel. But green TEXT or a
   thin green RULE would sit at 1.45:1 against this background and be
   close to invisible; filled with white lettering it reads 10.1:1. The
   same rule .btn-confirm and .msg-success already follow.

   ON THE SEPARATOR'S ROW, NOT A MESSAGE'S. Nobody has to answer it,
   react to it, or come back to it, so it should not look like something
   that expects any of those.

   WRITTEN AS `.day-separator .join-chip` FOR ONE REASON: specificity.
   `.day-separator span` above already paints every chip on a separator
   row grey, and it scores 0-1-1 against a bare `.join-chip`'s 0-1-0 — so
   it wins wherever in this file the green sits, and the first version of
   this shipped grey. Two classes score 0-2-0 and settle it. Ordering
   would not have helped; this is not the equal-specificity trap that has
   caught this project three times, it is the other one.
   --------------------------------------------------------------------- */
.day-separator .join-chip {
    padding: 3px 12px;
    /* The grey rule sets a border too, and an unset one would leave a
       hairline outline around a filled chip. */
    border: 1px solid var(--moosgruen);
    border-radius: 999px;
    background: var(--moosgruen);
    color: #ffffff;
    font-size: 0.8rem;
    font-weight: 600;
    white-space: nowrap;
}

/* Tighter than a day change: a join is a smaller event than a new day,
   and several arriving at once should not push the conversation apart. */
.join-line { margin: 14px 0; }

/* The separator is also the way back to any other day. It stays looking
   exactly like the label it replaced — a date that suddenly shouted
   "CLICK ME" would be noise on every screen for the sake of something
   used once a month. It reveals itself on hover and on focus. */
.day-jump {
    font: inherit;
    font-size: 0.8rem;
    cursor: pointer;
    transition: color 0.12s ease, border-color 0.12s ease;
    /* The pill looked right at 24px tall and was a poor target for a
       thumb — §8.4 is one hand, mid-shift. The padding grows the hit
       area without making the date itself any louder. */
    padding: 8px 14px;
    min-height: 34px;
}

.day-jump:hover,
.day-jump:focus-visible {
    color: var(--text);
    border-color: var(--text-muted);
}

/* Loading more of the conversation, in either direction. */
.load-more {
    display: flex;
    justify-content: center;
    margin: 14px 0;
}

/* Where a jump to a date landed. Bright enough to find with your eye,
   gone before it becomes something to dismiss. */
@keyframes landed-fade {
    from { background: var(--bg-input); }
    to   { background: transparent; }
}

.message-landed {
    animation: landed-fade 2.4s ease-out;
    border-radius: 10px;
}

@media (prefers-reduced-motion: reduce) {
    .message-landed { animation: none; background: var(--bg-input); }
}

.to-present {
    margin: 8px 0 0;
    text-align: center;
}

/* The date picker. */
.date-dialog {
    padding: 22px;
    max-width: 22rem;
    background: var(--bg-raised);
    color: var(--text);
    border: 1px solid var(--border);
    border-radius: 14px;
}

.date-dialog::backdrop { background: var(--scrim); }

.date-dialog h2 { margin: 0 0 14px; font-size: 1.05rem; }

.date-dialog label {
    display: block;
    margin-bottom: 6px;
    font-size: 0.85rem;
    color: var(--text-muted);
}

/* Full width, and tall enough to be a comfortable target one-handed —
   this opens the OS date picker on a phone, which is the whole reason
   it is a native date input and not a hand-built calendar. */
.date-dialog input[type="date"] {
    width: 100%;
    min-height: 44px;
    padding: 8px 10px;
    background: var(--bg-input);
    color: var(--text);
    border: 1px solid var(--border);
    border-radius: 8px;
    font: inherit;
}

.dialog-actions {
    display: flex;
    justify-content: flex-end;
    gap: 10px;
    margin-top: 18px;
}

/* The date of whatever you are currently looking at, following you down.
   ONE element, sticky against the page rather than the list, so it is
   never in competition with another copy of itself.

   height: 0 keeps it out of the flow entirely — it hovers over the
   conversation without pushing a single message down. */
.day-float {
    position: sticky;
    top: calc(var(--header-h-context) + 14px);
    z-index: 6;
    height: 0;
    overflow: visible;
    text-align: center;
    /* The strip is full width and invisible, so it must never swallow a
       tap meant for the message underneath. The pill inside it takes
       taps again — it is the date people reach for. */
    pointer-events: none;
}

.day-float .day-jump { pointer-events: auto; }

.day-float span {
    display: inline-block;
    /* Reads over messages rather than through them. */
    background: var(--bg);
    box-shadow: var(--shadow-sm);
}

.day-float[hidden] { display: none; }

.message {
    padding: 12px 14px;
    margin-bottom: 10px;
    background: var(--bg-raised);
    border: 1px solid var(--border);
    border-radius: var(--radius);
    box-shadow: var(--card-shadow);
}

/* Your own messages lean on the brand green rather than being pushed to
   the right: a phone is narrow, and half-width bubbles waste the space
   the text actually needs. */
.message-mine { border-left: 4px solid var(--moosgruen); }

.message-meta {
    display: flex;
    justify-content: space-between;
    gap: 12px;
    margin-bottom: 4px;
    font-size: 0.85rem;
    color: var(--text-muted);
}

.message-author { font-weight: 600; color: var(--text); }

.message-body {
    margin: 0;
    /* Long words and pasted links must not push the page sideways. */
    overflow-wrap: anywhere;
}

/* --- the action row under a message -------------------------------- */
/* Always visible, never hover-revealed: §8.4 forbids hover-dependent
   interaction, and on a touch screen hover does not exist at all. Quiet
   rather than hidden — icons at 60% opacity until touched. */

.message-actions {
    display: flex;
    align-items: center;
    flex-wrap: wrap;
    gap: 4px;
    margin-top: 6px;
}

.message-actions .action,
.picker > summary {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    gap: 4px;
    /* Visually small, but a 36px target with the row's padding around it.
       Slightly under the 44px floor because these sit inside a message
       and are secondary — the composer and the channel links, which are
       the primary actions, stay at 44. */
    min-width: 36px;
    height: 36px;
    padding: 0 8px;
    border: 0;
    border-radius: 8px;
    background: transparent;
    color: var(--text);
    opacity: 0.6;
    cursor: pointer;
    text-decoration: none;
    font: inherit;
    font-size: 0.85rem;
    list-style: none;   /* kills the default <summary> triangle */
}

.picker > summary::-webkit-details-marker { display: none; }

.message-actions .action:active,
.picker[open] > summary,
.picker > summary:active { opacity: 1; background: var(--bg-input); }

/* AN ANSWER NOBODY HAS READ, and it has to be findable.
   Reported 2026-09-03: "its visibility is very bad, i had to look to
   actually find it". It was opacity 1 and bold against five siblings at
   0.6 — a slightly less grey grey, in a row of grey.

   A terracotta fill with white on it, which is the same move the copied
   state makes with Moosgrün and the only one §8.1 allows: a 16px
   outline stroked in a brand colour sits near 1.5:1 on this background
   and reads as nothing happening. White on terracotta is 5.35:1 and
   carries across a room. */
.message-actions .action.has-unread {
    opacity: 1;
    font-weight: 700;
    background: var(--terracotta);
    color: #ffffff;
}

/* The dot said "new" when the icon could not. On a filled pill carrying
   its own count it is one mark too many, so it keeps its label for a
   screen reader and stops competing for the eye. */
.message-actions .action.has-unread .dot {
    position: absolute;
    width: 1px;
    height: 1px;
    overflow: hidden;
    clip-path: inset(50%);
}

/* Copied — for about a second and a half.
   A FILL WITH A WHITE ICON, because §8.1 allows Moosgrün no other way:
   a 16px outline stroked in it would sit at 1.45:1 on this background
   and read as nothing happening, which is the one thing this state
   exists to rule out. */
.message-actions .action.is-copied {
    opacity: 1;
    background: var(--moosgruen);
    color: #ffffff;
}

.message-actions .count { font-size: 0.8rem; }

/* A small dot saying there is something new in this thread. Never the
   only signal — the link is also bolded above. */
.message-actions .dot {
    width: 7px;
    height: 7px;
    border-radius: 50%;
    background: var(--moosgruen);
    /* Moosgrün is 1.45:1 on the card, so it gets a light ring to be
       visible at all at this size. */
    box-shadow: 0 0 0 1px var(--text);
}

/* --- reaction chips ------------------------------------------------ */

.chip {
    display: inline-flex;
    align-items: center;
    gap: 4px;
    height: 30px;
    padding: 0 10px;
    background: var(--bg-input);
    border: 1px solid var(--border);
    border-radius: 999px;
    color: var(--text);
    font: inherit;
    font-size: 0.85rem;
    cursor: pointer;
}

/* Yours, so you know tapping it again takes it back.

   .chip.chip-mine, NOT .chip-mine. The date shortcuts (A45) brought a
   second, generic .chip rule further down this file — same specificity,
   later in the source, so it quietly won every declaration here and your
   own reaction on a channel message has looked exactly like everybody
   else's ever since. Found 2026-09-23 while building the comment row.
   Two classes on one element beats one, wherever the rule sits. */
.chip.chip-mine {
    border-color: var(--moosgruen);
    background: var(--moosgruen);
    color: #ffffff;
}

/* THE SAME ROW UNDER A COMMENT (2026-09-23). It carries .message-actions
   as well, so the chips, the picker and its anchoring are the channel's
   — this only puts it where a comment needs it: tight under the text,
   and indented with the rest of the comment's body. */
.comment-actions { margin-top: 2px; margin-left: -8px; }

/* A finished task keeps what people said and stops taking more. The
   chips stay readable rather than greying out to nothing: they are the
   record, not a control any more. */
.comment-actions .chip[disabled] { cursor: default; opacity: 0.85; }

/* --- reaction picker ----------------------------------------------- */
/* A <details> element: it opens without JavaScript, and each emoji is an
   ordinary submit button, so the whole thing works with scripting off. */

/* The panel is anchored to the ACTION ROW, not to the little face button.
   Anchoring it to the button meant a 340px panel starting wherever the
   button happened to sit — which is off the right edge as soon as a
   message has a reaction or two in front of it. The row spans the whole
   message, so aligning to its left edge always fits. */
.message-actions { position: relative; }

.picker-list {
    position: absolute;
    bottom: 100%;
    left: 0;
    z-index: 20;
    /* Wide enough that eight columns plus the scrollbar fit without a
       horizontal scroll. At 340px the vertical scrollbar stole ~15px and
       pushed the last column out of view, which is why a pointless
       sideways scroll appeared. */
    width: 400px;
    max-width: 100%;
    margin-bottom: 6px;
    padding: 10px;
    background: var(--bg-raised);
    border: 1px solid var(--border);
    border-radius: var(--radius);
    box-shadow: var(--shadow-lg);
}

/* --- phones: a bottom sheet, not a popover -------------------------
   On a narrow screen there is no good place to float a 340px panel: put
   it near the top and it covers the message, put it near the bottom and
   it fights the composer. Every phone app solves this the same way, and
   so does this one — it slides up from the bottom edge, full width, with
   a backdrop that dims what is behind and catches the tap that closes
   it. §8.4: one hand, thumb at the bottom of the screen. */
@media (max-width: 640px) {
    .picker-list {
        position: fixed;
        left: 0;
        right: 0;
        bottom: 0;
        width: auto;
        max-width: none;
        max-height: 60vh;
        overflow-y: auto;
        margin: 0;
        padding: 12px 12px calc(12px + env(safe-area-inset-bottom));
        border: 0;
        border-top: 1px solid var(--border);
        border-radius: var(--radius) var(--radius) 0 0;
        z-index: 60;
    }

    /* The scroll area no longer needs its own cap — the sheet scrolls. */
    .picker-scroll { max-height: none; overflow: visible; }

    /* Backdrop. Also the tap target that dismisses the sheet, which is
       what a thumb reaches for first. */
    .picker[open]::before {
        content: '';
        position: fixed;
        inset: 0;
        background: var(--scrim);
        z-index: 55;
    }

    /* Wider buttons on the phone: nine per row would be cramped. */
    .picker-grid { grid-template-columns: repeat(7, 1fr); }
    .picker-list button { width: 100%; height: 44px; }
    .picker-quick button { width: 44px; }
}

/* The six most-used, always on top, so the common case stays one tap. */
.picker-quick {
    display: flex;
    justify-content: space-between;
    padding-bottom: 8px;
    margin-bottom: 6px;
    border-bottom: 1px solid var(--border);
}

/* Everything else scrolls. Capped so the palette can never grow taller
   than the screen and push its own top out of view. */
.picker-scroll {
    max-height: 46vh;
    overflow-y: auto;
    overflow-x: hidden;
    /* Room for the scrollbar so it never sits on top of the last column. */
    padding-right: 4px;
    -webkit-overflow-scrolling: touch;

    /* The default scrollbar on a dark panel is a grey slab from another
       era. Firefox and the standard property: */
    scrollbar-width: thin;
    scrollbar-color: var(--scrollbar) transparent;
}

/* Chrome, Edge and Safari need their own vocabulary for the same thing. */
.picker-scroll::-webkit-scrollbar { width: 8px; }
.picker-scroll::-webkit-scrollbar-track { background: transparent; }

.picker-scroll::-webkit-scrollbar-thumb {
    background: var(--scrollbar);
    border-radius: 999px;
    /* Inset from the edge so the thumb reads as a floating pill rather
       than a bar welded to the side of the panel. */
    border: 2px solid var(--bg-raised);
    background-clip: padding-box;
}

.picker-scroll::-webkit-scrollbar-thumb:hover { background: var(--scrollbar-hover); background-clip: padding-box; }

.picker-group {
    margin: 6px 0 2px;
    font-size: 0.72rem;
    text-transform: uppercase;
    letter-spacing: 0.06em;
    color: var(--text-muted);
}

.picker-grid {
    display: grid;
    grid-template-columns: repeat(8, 1fr);
}

.picker-list button {
    /* Fills its grid column instead of being a fixed 40px. Fixed widths
       overflowed the moment a scrollbar appeared. */
    width: 100%;
    height: 40px;
    padding: 0;
    border: 0;
    border-radius: 8px;
    background: transparent;
    font-size: 1.2rem;
    line-height: 1;
    cursor: pointer;
}

/* The quick row is a flex row, so those need a width of their own. */
.picker-quick button { width: 44px; }

.picker-list button:active { background: var(--bg-input); }


/* --- shift requests --------------------------------------------------
   The one message type that must be impossible to scroll past. It gets a
   red edge AND a warning icon AND the words "Antwort erforderlich" —
   §8.2: roughly 1 in 12 men cannot separate red from green, and a shift
   request cannot depend on hue. */

.message-fill {
    border-left: 4px solid var(--urgent);
    background: var(--bg-input);
}

.fill { margin-top: 10px; }

.fill-state {
    display: flex;
    align-items: baseline;
    gap: 8px;
    margin: 0 0 10px;
    font-size: 0.95rem;
}

.fill-state-open { color: var(--urgent); }

/* Deliberately NOT green and NOT a tick on its own. "Available, decision
   open" is not "confirmed", and the whole point of the wording is that
   nobody reads it as settled and fails to turn up. */
.fill-state-waiting { color: var(--attention); }

.fill-state-done {
    padding: 8px 12px;
    background: var(--moosgruen);
    border-radius: 8px;
    color: #ffffff;
}

/* Two large, clearly separated buttons. No confirm dialog to mis-tap —
   §8.4 asks for exactly this. */
.fill-answer {
    display: flex;
    gap: 10px;
}

.fill-answer .btn {
    margin-top: 0;
    min-height: 52px;   /* larger than the usual floor: tapped in a hurry */
    font-size: 1.05rem;
}

.fill-change { margin-top: 8px; }

.link-button {
    padding: 8px 0;
    min-height: var(--touch-min);
    background: none;
    border: 0;
    color: var(--pink);
    font: inherit;
    font-size: 0.85rem;
    text-decoration: underline;
    cursor: pointer;
}

/* The one action on the recycle bin that cannot be undone. */
/* Terracotta, the palette's own warning colour (§8) — not a red invented
   for this one control. */
.link-danger { color: var(--terracotta); }

/* "Entwurf" on a channel you left something half-written in.

   MOOSGRÜN AS A FILL WITH WHITE LETTERING, which is the only way §8.1
   allows the green to be used: as text or as a thin border it sits at
   1.45:1 against the dark background and simply cannot be seen. Filled,
   it is 10.1:1 — the same treatment as .msg-success.

   Green rather than the urgent red is what keeps it honest. It says
   "you left something here", not "somebody is waiting for you", and
   those two must never look alike in a glance down the channel list. */
.draft-pill {
    margin-left: 6px;
    background: var(--moosgruen);
    border-color: var(--moosgruen);
    color: #ffffff;
}

/* --- earlier versions of a page, and the recycle bin ----------------- */
/* One row per version: when, who, and the way back. Deliberately plain —
   this is a screen somebody reaches while slightly panicking, and the
   only thing it has to do is be readable. */
.revision-list {
    list-style: none;
    margin: 8px 0 0;
    padding: 0;
}

.revision-row {
    display: flex;
    align-items: baseline;
    flex-wrap: wrap;
    gap: 4px 12px;
    padding: 8px 0;
    border-bottom: 1px solid var(--border);
}

.revision-row:last-child { border-bottom: 0; }

.revision-when { flex: 0 0 auto; }

.revision-who {
    flex: 1 1 12rem;
    min-width: 0;
    font-size: 0.85rem;
}

/* The button sits in its own form, which would otherwise take a whole
   line to itself in a flex row. */
.revision-row form { flex: 0 0 auto; margin: 0; }

.fill-decide {
    margin-top: 12px;
    padding-top: 12px;
    border-top: 1px solid var(--border);
}

.fill-counts { margin: 0 0 8px; font-size: 0.85rem; }

.fill-candidates {
    list-style: none;
    margin: 0;
    padding: 0;
    display: flex;
    flex-direction: column;
    gap: 6px;
}

.fill-candidates li {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 10px;
    padding: 8px 12px;
    background: var(--bg-raised);
    border: 1px solid var(--border);
    border-radius: 8px;
}

.fill-candidates form { margin: 0; }

/* WHEN BOTH ARE ASKED FOR, QUIET WINS — and this rule sits here, right
   above .btn-small, because that is where somebody looking for the
   reason will be standing.

   .btn-small sets a moosgrün fill and comes LATER in this file than
   .btn-quiet, so at equal specificity it wins and `btn btn-quiet
   btn-small` renders as a green slab. That has now cost three separate
   pieces of work: the calendar gave up and defined .cal-btn, the
   Aufgaben row restated the quiet look locally, and the file button on
   a to-do's thread hit it again on 2026-09-08.

   All eight places in the app that combine the two mean the quiet one —
   pagination, a cancel, a delete, "Datei". So it is settled once here
   rather than a fourth time somewhere else. */
.btn-small.btn-quiet {
    background: transparent;
    border: 1px solid var(--border);
    color: var(--text);
}

.btn-small {
    min-height: 38px;
    padding: 0 14px;
    background: var(--moosgruen);
    border: 0;
    border-radius: 8px;
    color: #ffffff;
    font: inherit;
    font-size: 0.85rem;
    font-weight: 600;
    cursor: pointer;
}

.fill-toggle {
    display: flex;
    align-items: center;
    gap: 10px;
    min-height: var(--touch-min);
    margin: 10px 0 0;
    padding: 8px 12px;
    background: var(--bg-input);
    border: 1px solid var(--border);
    border-left: 3px solid var(--urgent);
    border-radius: 8px;
    font-weight: 400;
    font-size: 0.9rem;
}

.fill-toggle input { width: 20px; height: 20px; accent-color: var(--urgent-fill); }

/* --- unread ------------------------------------------------------- */

/* An unread channel gets a green edge AND a count AND bolder text. Three
   signals, because §8.2 forbids colour as the only one — and green here
   is the brand fill, not the "done" state, which no longer exists. */
.channel-unread {
    border-left-color: var(--moosgruen);
    background: var(--bg-unread);
}

.channel-unread .channel-name { font-weight: 700; }

/* A CALENDAR ENTRY IS NOT A CHANNEL, and on the home screen it sat
   between two things that are, wearing the same terracotta edge. The
   app's text colour is the neutral that works in both themes: near-white
   on the dark ground the owner reported this against, near-black on the
   light one — one token, no second hex to keep in step. */
.channel-cal { border-left-color: var(--text); }

.badge {
    display: inline-block;
    min-width: 22px;
    padding: 1px 7px;
    margin-left: 6px;
    background: var(--moosgruen);
    border-radius: 999px;
    color: #ffffff;          /* 10.1:1 on Moosgrün */
    font-size: 0.75rem;
    font-weight: 600;
    text-align: center;
    vertical-align: 2px;
}

/* Where you left off. Terracotta rather than green: this is a place
   marker, not a status, and it must not read as "done". */
.new-separator span {
    background: var(--terracotta);
    border-color: var(--terracotta);
    color: #ffffff;
    font-weight: 600;
}

/* --- threads ------------------------------------------------------- */

.thread-link-unused {
    display: inline-flex;
    align-items: center;
    min-height: var(--touch-min);   /* §8.4 — a real target, not a hyperlink */
    margin-top: 6px;
    color: var(--pink);
    font-size: 0.9rem;
    text-decoration: none;
}

.thread-link.has-unread { font-weight: 700; }

.thread-parent {
    border-left: 4px solid var(--terracotta);
}

.thread-count {
    margin: 4px 0 12px;
    font-size: 0.85rem;
}

/* --- composer ------------------------------------------------------ */

.composer {
    position: sticky;
    bottom: 0;
    /* ABOVE THE MESSAGES IT SITS IN FRONT OF (2026-09-24 → fixed
       2026-09-27). Sticky with no z-index is z-index: auto, which does
       not lift anything: .message-tools is absolute at z-index 6, so on
       a desktop the little bar of icons on a hovered message scrolled
       straight over the red Senden button. Reported from the Aufbau
       channel: "it scrolls over the red send button".

       8, DELIBERATELY BETWEEN TWO THINGS. Above the message tools (6),
       which must go behind on every device — the newest message gets
       room instead, see .messages in the hover block. Below the emoji
       picker and the mention menu
       (20), which open upward out of a message or the box itself and
       have to stay whole, and below the round "back to newest" button
       (30), which floats over the composer on purpose. */
    z-index: 8;
    padding: 12px 0 4px;
    background: var(--bg);
}

/* WHICH CONVERSATION MOVED. Sits directly above the composer and is
   sticky with it, so it is in view the moment a channel opens rather
   than somewhere up the scroll — see the note in channel.php for why
   the top of the list was the wrong place.

   QUIET, NOT ALARMING. A thread that has moved is news, not a problem:
   a left edge in terracotta to catch the eye, and everything else the
   weight of an ordinary row. The pill is where the number goes, so the
   text can stay one line.

   IT IS SHORT BY CONSTRUCTION — one row per thread with something
   unread, and threads are read far more often than they are started.
   Capped anyway, because a fortnight away should not push the message
   box off the screen. */
.thread-jump {
    position: sticky;
    bottom: 0;
    max-height: 8.5rem;
    overflow-y: auto;
    margin: 0;
    padding-top: 8px;
    background: var(--bg);
}

.thread-jump-row {
    display: flex;
    align-items: center;
    gap: 10px;
    min-height: var(--touch-min);
    padding: 6px 12px;
    margin-bottom: 6px;
    border: 1px solid var(--border);
    border-left: 3px solid var(--terracotta);
    border-radius: var(--radius);
    background: var(--bg-raised);
    color: var(--text);
    text-decoration: none;
}

.thread-jump-row:active { background: var(--bg-input); }

/* The count, in the same shape the reply icon uses on a message. */
.thread-jump-count {
    flex: 0 0 auto;
    min-width: 22px;
    padding: 1px 6px;
    border-radius: 999px;
    background: var(--terracotta);
    color: #ffffff;      /* 5.35:1 — §8.1, terracotta is a fill only */
    font-size: 0.78rem;
    font-weight: 700;
    text-align: center;
}

/* min-width:0 so a long first line ellipses instead of pushing the
   arrow off the row. */
.thread-jump-text {
    flex: 1 1 auto;
    min-width: 0;
    display: flex;
    gap: 6px;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
    font-size: 0.85rem;
}

.thread-jump-text .muted { overflow: hidden; text-overflow: ellipsis; }

.thread-jump-go { flex: 0 0 auto; color: var(--text-muted); }

.composer textarea {
    width: 100%;
    padding: 10px 12px;
    background: var(--bg-input);
    border: 1px solid var(--border);
    border-radius: 8px;
    color: var(--text);
    font: inherit;
    font-size: 16px;   /* below 16px, iOS Safari zooms the page on focus */
    resize: vertical;
}

/* The contenteditable box that stands in for the textarea. Styled to be
   indistinguishable from it — it is the same control, just one that shows
   bold as bold. */
.rich-input {
    width: 100%;
    min-height: 78px;
    max-height: 40vh;
    overflow-y: auto;
    padding: 10px 12px;
    background: var(--bg-input);
    border: 1px solid var(--border);
    border-radius: 8px;
    color: var(--text);
    font: inherit;
    font-size: 16px;   /* below 16px, iOS Safari zooms the page on focus */
    line-height: 1.5;
    white-space: pre-wrap;
    word-break: break-word;
}

.rich-input:focus {
    border-color: var(--moosgruen);
    border-width: 2px;
    outline: none;
}

/* contenteditable has no placeholder of its own. :empty is false as soon
   as the browser drops a <br> in, so this also checks for that. */
.rich-input:empty::before {
    content: attr(data-placeholder);
    color: var(--text-muted);
    pointer-events: none;
}

.rich-input code {
    padding: 1px 5px;
    background: var(--code-bg);
    border-radius: 4px;
    font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
    font-size: 0.92em;
}

.composer textarea:focus {
    border-color: var(--moosgruen);
    border-width: 2px;
    outline: none;
}

.composer-row {
    display: flex;
    align-items: center;
    gap: 10px;
    margin-top: 10px;
}

.composer-row .btn { margin-top: 0; flex: 1; }

/* The edit page uses the composer so it looks like writing a message,
   but there is no conversation above it to stick to. */
.composer-static { position: relative; padding-top: 0; }

/* The "+" is a <label> for a hidden file input: browsers open the picker
   from a label with no JavaScript at all. */
.plus {
    display: flex;
    align-items: center;
    justify-content: center;
    width: var(--touch-min);
    height: var(--touch-min);
    flex: 0 0 auto;
    margin: 0;
    background: var(--bg-input);
    border: 1px solid var(--border);
    border-radius: 8px;
    color: var(--text);
    cursor: pointer;
}

.plus:active { border-color: var(--moosgruen); border-width: 2px; }

/* A screen whose content is a table rather than prose. 44rem is right
   for reading and wrong for six columns, which is why the activation
   list was scrolling sideways inside its own box. Admin only, so the
   phone case does not arise — those screens are used at a desk. */
.page-wide {
    max-width: 74rem;
}

/* Two fields, one control: a date, and a time that only exists in
   five-minute steps. A datetime-local could not promise that — its
   `step` decides what is VALID, not what the spinner offers. */
.plan-fields {
    display: flex;
    gap: 8px;
    flex-wrap: wrap;
}

.plan-fields input,
.plan-fields select {
    flex: 0 1 auto;
    min-height: var(--touch-min);
    padding: 6px 10px;
    background: var(--bg-input);
    border: 1px solid var(--border);
    border-radius: 8px;
    color: var(--text);
    font: inherit;
    font-size: 16px;   /* below this iOS zooms the page on focus */
}

/* Scheduled messages, above the channels. Its own styling rather than
   borrowing .rail-heading, which lives inside the desktop breakpoint
   and would have left this unstyled on the phone. */
.plan-heading {
    margin: 0 0 6px;
    padding: 0 2px;
    color: var(--text-muted);
    font-size: 0.72rem;
    font-weight: 700;
    letter-spacing: 0.08em;
    text-transform: uppercase;
}

.plan-list { margin-bottom: 18px; }

/* --- the install guide ----------------------------------------------- */
/* Numbered, generously spaced, and readable at arm's length: this is
   read on a phone WHILE following it, one hand, often in a corridor. */
.steps {
    margin: 10px 0 0;
    padding-left: 1.3em;
    line-height: 1.6;
}

.steps li { margin: 0 0 12px; }

.steps li::marker { color: var(--terracotta); font-weight: 700; }

/* --- Medien, by month, with searchable names ------------------------- */

.media-month {
    margin: 22px 0 8px;
    font-size: 0.78rem;
    font-weight: 700;
    letter-spacing: 0.08em;
    text-transform: uppercase;
    color: var(--text-muted);
}

.media-month:first-of-type { margin-top: 10px; }

/* The box IS the control: type a name, press Enter. A save button under
   every tile in a grid would be more furniture than photograph. */
.media-caption input {
    width: 100%;
    margin-top: 4px;
    padding: 4px 6px;
    background: transparent;
    border: 1px solid transparent;
    border-radius: 6px;
    color: var(--text-muted);
    font: inherit;
    /* 16px, or iOS zooms the whole grid when the box takes focus. */
    font-size: 16px;
}

.media-caption input:hover { border-color: var(--border); }

.media-caption input:focus {
    background: var(--bg-input);
    border-color: var(--moosgruen);
    color: var(--text);
    outline: none;
}

.media-caption-text {
    margin: 4px 0 0;
    font-size: 0.8rem;
    color: var(--text-muted);
}

/* --- private conversations, under the channels ----------------------- */
/* A fold-out rather than a flat list: channels are the shared thing and
   stay the first thing you see, and there can be a lot of conversations.
   It opens itself when something is waiting — a badge behind a closed
   drawer is a badge nobody reads. */
.dm-foldout {
    margin: 22px 0 0;
    border-top: 1px solid var(--border);
    padding-top: 6px;
}

.dm-foldout > summary {
    display: flex;
    align-items: center;
    gap: 8px;
    min-height: var(--touch-min);
    padding: 0 4px;
    color: var(--text-muted);
    font-size: 0.78rem;
    font-weight: 600;
    letter-spacing: 0.08em;
    text-transform: uppercase;
    cursor: pointer;
    list-style: none;
}

/* Safari draws its own triangle unless told twice. */
.dm-foldout > summary::-webkit-details-marker { display: none; }

/* One that follows the open state, in the app's own line weight rather
   than the browser's glyph. */
.dm-foldout > summary::before {
    content: '';
    width: 0;
    height: 0;
    border-left: 5px solid currentColor;
    border-top: 4px solid transparent;
    border-bottom: 4px solid transparent;
    transition: transform 120ms ease;
}

.dm-foldout[open] > summary::before { transform: rotate(90deg); }

@media (prefers-reduced-motion: reduce) {
    .dm-foldout > summary::before { transition: none; }
}

.dm-foldout > summary:hover { color: var(--text); }

.dm-summary-label { flex: 0 0 auto; }

/* The way to a new conversation, on the one row that is visible whether
   the drawer is open or shut. margin-left:auto puts it at the far end of
   the flex row, away from the label — so a thumb reaching for it does
   not open and close the drawer by accident.
   A full touch target: the glyph is small, the thing you press is not. */
.dm-add {
    margin-left: auto;
    display: flex;
    align-items: center;
    justify-content: center;
    width: var(--touch-min);
    min-height: var(--touch-min);
    margin-right: -8px;
    color: var(--text-muted);
    font-size: 1.35rem;
    font-weight: 400;
    line-height: 1;
    text-decoration: none;
}

.dm-add:hover,
.dm-add:focus-visible { color: var(--accent); }

.dm-empty,
.dm-all {
    margin: 8px 0 0;
    padding: 0 4px;
    font-size: 0.85rem;
}

/* --- send it later --------------------------------------------------- */
/* In the same drawer as Wichtig and Schichtanfrage, and shaped like
   them: an empty field means "now", the way an unticked box means no. */
.plan-field {
    display: flex;
    flex-direction: column;
    gap: 4px;
    margin: 10px 0 0;
    font-size: 0.9rem;
}

.plan-field input {
    width: 100%;
    max-width: 16rem;
    min-height: var(--touch-min);
    padding: 6px 10px;
    background: var(--bg-input);
    border: 1px solid var(--border);
    border-radius: 8px;
    color: var(--text);
    font: inherit;
    font-size: 16px;   /* below this iOS zooms the page on focus */
}

.plan-waiting {
    margin: 8px 0 0;
    font-size: 0.85rem;
}

/* --- the microphone ------------------------------------------------- */
/* Shaped like the "+" beside it, because they are the same kind of
   thing: two ways to put something other than words into a message. */
.mic {
    display: flex;
    align-items: center;
    justify-content: center;
    width: var(--touch-min);
    height: var(--touch-min);
    flex: 0 0 auto;
    margin: 0;
    padding: 0;
    background: var(--bg-input);
    border: 1px solid var(--border);
    border-radius: 8px;
    color: var(--text);
    cursor: pointer;
}

.mic:active { border-color: var(--moosgruen); border-width: 2px; }

/* RECORDING IS NOT A SUBTLE STATE. Somebody holding a phone in one hand
   in a loud kitchen has to be able to tell at a glance that it is still
   listening — this is the one place in the app where the colour is
   doing real work rather than decoration. */
.mic.is-recording {
    background: var(--terracotta);
    border-color: var(--terracotta);
    color: #ffffff;
    animation: mic-pulse 1.6s ease-in-out infinite;
}

@keyframes mic-pulse {
    0%, 100% { opacity: 1; }
    50%      { opacity: 0.62; }
}

@media (prefers-reduced-motion: reduce) {
    .mic.is-recording { animation: none; }
}

/* The running count, beside the button while it records. */
/* PAUSED IS STILL A RECORDING, and has to look like one that is merely
   held rather than one that has stopped: the pulse goes, the terracotta
   stays. Somebody who cannot tell "paused" from "finished" at a glance
   will press the microphone to check and end the note they were keeping. */
.mic.is-recording.is-paused {
    animation: none;
    opacity: 0.8;
}

.mic-time {
    flex: 0 0 auto;
    align-self: center;
    font-variant-numeric: tabular-nums;
    font-size: 0.9rem;
    color: var(--terracotta);
    font-weight: 600;
}

/* The clock stops climbing when paused; dimming it says so without
   needing a word next to it. */
.mic-time.is-paused { opacity: 0.6; }

/* --- getting out of a recording (2026-08-27) ------------------------- */

/* Only on screen while recording, and only then — see wireRecorders.
   Both are the same size as the microphone: this row is used one-handed,
   mid-shift, and a smaller target next to a bigger one is the one that
   gets missed. */
.mic-cancel,
.mic-pause {
    display: flex;
    align-items: center;
    justify-content: center;
    width: var(--touch-min);
    height: var(--touch-min);
    flex: 0 0 auto;
    margin: 0;
    padding: 0;
    background: var(--bg-input);
    border: 1px solid var(--border);
    border-radius: 8px;
    color: var(--text);
    cursor: pointer;
}

.mic-cancel:active,
.mic-pause:active { border-color: var(--moosgruen); border-width: 2px; }

/* QUIET, DELIBERATELY. Discarding is the destructive one, and the
   temptation is to paint it red — but red beside a pulsing terracotta
   microphone reads as another urgent thing rather than as a way out, and
   §8.1 keeps terracotta for the state that matters. An outlined ✕ next
   to a filled recording button is unmistakable without shouting. */
.mic-cancel { color: var(--text-muted); }

.mic-cancel:hover,
.mic-cancel:focus-visible { color: var(--terracotta); border-color: var(--terracotta); }

/* --- a voice note in the conversation -------------------------------- */

/* A microphone, then the label and the player. Without the icon it is a
   grey control strip that could be anything — the first one on the live
   site was reported as looking like a video. */
/* ONE ROW, not three. The first version stacked an icon, a caption and
   a player and took more vertical space than the message it belonged
   to — "i think it eats way too much space" (2026-08-12). The icon and
   the length sit beside the player now, and the whole thing is the
   height of one control strip. */
.attachment-audio {
    display: flex;
    align-items: center;
    gap: 8px;
    max-width: 24rem;
    margin: 6px 0 0;
    padding: 4px 8px;
    background: var(--bg-input);
    border: 1px solid var(--border);
    border-radius: 999px;
}

/* What the voice note says, under the player.
   Indented to the same left edge as the player above it so the two read
   as one thing.

   IT USED TO BE A SCROLLING BOX — max-height plus overflow-y — to stop a
   five-minute note pushing the conversation off the screen. Right
   intention, wrong mechanism: a scrolling box inside a scrolling page is
   miserable on a phone. A thumb meant for the conversation scrolls the
   transcript instead, and nothing on screen says there is more text, so
   a note that stops mid-sentence just looks broken (2026-08-28).
   It now collapses instead, with a button that says how to see the rest.
   NO CLAMP IN THE DEFAULT STATE: the collapsing is added by app.js only
   once it knows the text really is too long, so with no JavaScript the
   whole transcript is simply there. */
.attachment-transcript {
    max-width: 24rem;
    margin: 4px 0 0;
    padding: 6px 10px;
    background: var(--bg-input);
    border: 1px solid var(--border);
    border-radius: var(--radius);
}

/* Collapsed. The fade is the affordance — text that dissolves rather
   than stopping mid-word reads as "there is more", which a hard cut
   does not. */
.attachment-transcript.is-clipped .transcript-text {
    display: -webkit-box;
    -webkit-line-clamp: 5;
    line-clamp: 5;
    -webkit-box-orient: vertical;
    overflow: hidden;
    -webkit-mask-image: linear-gradient(to bottom, #000 70%, transparent 100%);
    mask-image: linear-gradient(to bottom, #000 70%, transparent 100%);
}

.transcript-more {
    display: flex;
    align-items: center;
    margin-top: 2px;
    /* THE FULL TOUCH TARGET. Wide because it sits under a wall of text
       and the thumb lands anywhere along it; tall because 27px of
       padding-sized button is exactly the control people jab at twice. */
    width: 100%;
    min-height: var(--touch-min);
    padding: 0;
    text-align: left;
    background: none;
    border: 0;
    color: var(--terracotta);
    font: inherit;
    font-size: 0.8rem;
    font-weight: 600;
    cursor: pointer;
}

.transcript-label {
    margin: 0 0 2px;
    font-size: 0.68rem;
    font-weight: 600;
    letter-spacing: 0.06em;
    text-transform: uppercase;
}

.transcript-text {
    margin: 0;
    font-size: 0.9rem;
    line-height: 1.45;
}

/* --- a message shown in your language -------------------------------- */
/* Quiet, and always present. It says the words are a machine's and which
   language they came from, and offers the original — the whole reason
   being that a shift request read wrong is a shift nobody covers. */
.translated-note {
    display: block;
    margin-top: 2px;
    color: var(--text-muted);
    font-size: 0.72rem;
}

.translated-toggle,
.translate-ask {
    margin-left: 8px;
    padding: 0;
    min-height: 0;
    font-size: 0.72rem;
}

.translate-ask { display: block; margin: 4px 0 0; }

.attachment-audio-icon {
    display: flex;
    align-items: center;
    justify-content: center;
    flex: 0 0 auto;
    width: 26px;
    height: 26px;
    border-radius: 50%;
    background: var(--terracotta);
    color: #ffffff;
}

.attachment-audio-body {
    display: flex;
    align-items: center;
    gap: 8px;
    min-width: 0;
    flex: 1 1 auto;
}

.attachment-audio audio {
    flex: 1 1 auto;
    min-width: 0;
    /* The browser's own control strip. Left native on purpose: it is
       already familiar, already accessible, and on iOS it hands the
       audio to the system the way people expect. */
    height: 32px;
}

/* The length only. The word "Sprachnachricht" is what the microphone
   already says, and repeating it in text was half the wasted room. */
.attachment-audio-meta {
    flex: 0 0 auto;
    font-size: 0.78rem;
    font-variant-numeric: tabular-nums;
    color: var(--text-muted);
}

/* --- what is about to be sent --------------------------------------- */

/* Square thumbnails in a row, each removable on its own. The previous
   version listed filenames in chips, which at four files was a wall of
   truncated "WhatsApp Image 2026-07-..." saying nothing about which
   picture was which. */
.file-preview {
    display: flex;
    flex-wrap: wrap;
    gap: 8px;
    margin-top: 10px;
}

.file-tile {
    position: relative;
    width: 74px;
    height: 74px;
    border-radius: 8px;
    overflow: hidden;
    background: var(--bg-input);
    border: 1px solid var(--border);
}

.file-tile img {
    width: 100%;
    height: 100%;
    object-fit: cover;
    display: block;
}

/* A PDF has no thumbnail worth showing; an <img> pointed at one just
   renders a broken-image icon. The extension and name say more. */
.file-tile-doc {
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: 2px;
    padding: 6px;
    text-align: center;
}

.file-tile-ext {
    font-size: 0.8rem;
    font-weight: 700;
    letter-spacing: 0.04em;
    color: var(--text);
}

.file-tile-name {
    font-size: 0.62rem;
    line-height: 1.2;
    color: var(--text-muted);
    /* Two lines, then ellipsis — enough to tell two files apart. */
    display: -webkit-box;
    -webkit-line-clamp: 2;
    -webkit-box-orient: vertical;
    overflow: hidden;
    overflow-wrap: anywhere;
}

.file-remove {
    position: absolute;
    top: 2px;
    right: 2px;
    width: 24px;
    height: 24px;
    padding: 0;
    border: 0;
    border-radius: 50%;
    background: var(--lightbox-bar);
    color: #ffffff;
    font-size: 1.1rem;
    line-height: 1;
    cursor: pointer;
}

.file-remove:active { background: var(--urgent-fill); }

/* --- viewing a photo full size -------------------------------------- */

.lightbox {
    position: fixed;
    inset: 0;
    z-index: 100;
    display: flex;
    align-items: center;
    justify-content: center;
    padding: 24px;
    background: var(--lightbox);
}

.lightbox {
    overflow: hidden;
    /* The photo handles pinch and drag itself (app.js); the browser must
       not scroll or zoom the page behind while it does. */
    touch-action: none;
}

.lightbox img {
    max-width: 100%;
    max-height: 100%;
    border-radius: 8px;
    /* The photo itself does NOT close the overlay — clicking left or
       right of it does. It zooms instead, and the cursor says so. */
    cursor: zoom-in;
    transform-origin: center center;
    transition: transform 120ms ease-out;
    user-select: none;
    -webkit-user-select: none;
    -webkit-touch-callout: none;
}

.lightbox img.is-zoomed { cursor: grab; border-radius: 0; }
.lightbox img.is-grabbing { cursor: grabbing; transition: none; }

/* A PDF needs the whole panel rather than being centred like a photo. */
.lightbox-pdf { padding: 12px; }

.lightbox iframe {
    width: 100%;
    height: 100%;
    border: 0;
    border-radius: 8px;
    background: #ffffff;
}

.lightbox-download {
    position: absolute;
    top: calc(12px + env(safe-area-inset-top));
    right: calc(12px + var(--touch-min) + 8px);
    display: flex;
    align-items: center;
    min-height: var(--touch-min);
    padding: 0 14px;
    border-radius: 999px;
    background: var(--tint-firm);
    color: #ffffff;
    font-size: 0.85rem;
    font-weight: 600;
    text-decoration: none;
}

.lightbox-close {
    position: absolute;
    top: calc(12px + env(safe-area-inset-top));
    right: 12px;
    width: var(--touch-min);
    height: var(--touch-min);
    padding: 0;
    border: 0;
    border-radius: 50%;
    background: var(--tint-firm);
    color: #ffffff;
    font-size: 1.5rem;
    line-height: 1;
    cursor: pointer;
}

/* --- sending ------------------------------------------------------- */

.btn.is-sending {
    /* Says something is happening without the button vanishing or
       resizing, which would move the layout under a thumb. */
    filter: none;
    opacity: 0.75;
    cursor: default;
}

/* --- attachments on a sent message ---------------------------------- */

.attachments {
    list-style: none;
    margin: 10px 0 0;
    padding: 0;
    display: flex;
    flex-wrap: wrap;
    gap: 8px;
}

.attachment-image {
    display: block;
    max-width: 100%;
}

.attachment-image img {
    display: block;
    width: auto;
    height: auto;
    /* Capped so one photo cannot take over the whole conversation, but
       big enough to read a delivery note without opening it. */
    max-width: min(100%, 320px);
    max-height: 320px;
    border-radius: 8px;
    background: var(--bg-input);
}

/* A PDF with a rendered first page. Shown as a page, because that is
   what tells you whether it is the right document. */
.attachment-pdf {
    display: block;
    max-width: min(100%, 240px);
    background: #ffffff;   /* paper, so a scan does not float on black */
    border: 1px solid var(--border);
    border-radius: 8px;
    overflow: hidden;
    text-decoration: none;
}

.attachment-pdf img {
    display: block;
    width: 100%;
    /* Enough of the page to recognise it, without one PDF taking over
       the conversation. */
    max-height: 260px;
    object-fit: cover;
    object-position: top;
}

.attachment-pdf-label {
    display: block;
    padding: 8px 10px;
    background: var(--bg-input);
    color: var(--text);
}

.attachment-pdf-label strong {
    display: block;
    font-size: 0.85rem;
    font-weight: 600;
    overflow-wrap: anywhere;
}

.attachment-pdf-label em {
    font-style: normal;
    font-size: 0.75rem;
    color: var(--text-muted);
}

.attachment-doc {
    display: inline-flex;
    align-items: center;
    gap: 10px;
    min-height: var(--touch-min);
    padding: 8px 12px;
    background: var(--bg-input);
    border: 1px solid var(--border);
    border-radius: 8px;
    color: var(--text);
    text-decoration: none;
}

.attachment-doc strong {
    display: block;
    font-weight: 600;
    font-size: 0.9rem;
    /* A long filename must wrap rather than widen the message. */
    overflow-wrap: anywhere;
}

.attachment-doc em {
    font-style: normal;
    font-size: 0.8rem;
    color: var(--text-muted);
}

/* Present for screen readers, invisible on screen — used where a visible
   label would only repeat what the placeholder already says. */
.visually-hidden {
    position: absolute;
    width: 1px;
    height: 1px;
    padding: 0;
    margin: -1px;
    overflow: hidden;
    clip: rect(0 0 0 0);
    white-space: nowrap;
    border: 0;
}

/* Definition list used on the landing page for the identity axes. */
.facts { margin: 24px 0 0; }
.facts dt { font-size: 0.9rem; color: var(--text-muted); }
.facts dd { margin: 2px 0 16px; }
.facts dd:last-of-type { margin-bottom: 0; }

/* A link that has to look and behave like a button. Everything here exists
   because <a> does not inherit the button reset. */
a.btn {
    text-decoration: none;
    text-align: center;
    line-height: 1.4;
}

/* NOTE: there are no style="..." attributes anywhere in the pages, and
   there must not be. The Content-Security-Policy sets style-src 'self',
   which blocks inline style attributes outright — an inline style does not
   degrade, it simply does not apply, and the layout silently breaks. */

/* The "put this away" link under a finished request. Quiet on purpose:
   tidying up is not what the screen is for, and it must never compete
   with "Ich kann". */
.request-archive { margin-top: 10px; }

/* ---- the channel ordering screen ---------------------------------- */

.order-row[draggable="true"] { cursor: grab; }
.order-row.is-dragging { opacity: 0.4; cursor: grabbing; }

/* Stillgelegt: still listed, so it can be brought back. A channel that
   vanished with no way to restore it is indistinguishable from one that
   was deleted. */
.order-row-off { opacity: 0.55; }

.order-name a { color: var(--text); text-decoration: none; font-weight: 600; }
.order-name a:hover { text-decoration: underline; }

.order-topic {
    display: block;
    margin-top: 2px;
    color: var(--text-muted);
    font-size: 0.85rem;
}

/* The list of names a channel's filters actually resolve to. Boxed,
   because it is the answer the screen exists to give. */
.audience-preview {
    padding: 14px 16px;
    background: var(--bg-input);
    border: 1px solid var(--border);
    border-radius: var(--radius);
}

.audience-preview .section-heading { margin-top: 0; }

/* --- checklists: one run (A43) ---------------------------------------
   §8.4 is the whole brief here: a dim room, one hand, possibly gloves,
   at 23:40. So the target is the entire row rather than a checkbox, the
   done state is unmistakable across a bar, and nothing depends on
   JavaScript — app.js only makes it instant. */

.work-progress-label {
    margin: 0 0 6px;
    font-size: 0.9rem;
    font-weight: 600;
}

.work-progress {
    height: 6px;
    margin: 0 0 16px;
    background: var(--bg-input);
    border-radius: 999px;
    overflow: hidden;
}

.work-progress span {
    display: block;
    height: 100%;
    background: var(--moosgruen);
    border-radius: 999px;
    transition: width 260ms ease;
}

/* In tenths, as classes: the obvious style="width:40%" is an inline
   style, which this app's CSP forbids — correctly. */
.w-0  { width: 0; }
.w-1  { width: 10%; }
.w-2  { width: 20%; }
.w-3  { width: 30%; }
.w-4  { width: 40%; }
.w-5  { width: 50%; }
.w-6  { width: 60%; }
.w-7  { width: 70%; }
.w-8  { width: 80%; }
.w-9  { width: 90%; }
.w-10 { width: 100%; }

@media (prefers-reduced-motion: reduce) {
    .work-progress span { transition: none; }
}

.work-steps { list-style: none; margin: 0; padding: 0; }

.work-step {
    position: relative;
    margin: 0 0 8px;
    border: 1px solid var(--border);
    border-radius: var(--radius);
    background: var(--bg-raised);
    overflow: hidden;
}

.work-tick-form { margin: 0; }

/* THE WHOLE ROW IS THE BUTTON. A checkbox is a 20px target for somebody
   holding a crate. */
.work-tick {
    display: flex;
    align-items: center;
    gap: 12px;
    width: 100%;
    min-height: 56px;
    padding: 10px 14px;
    background: none;
    border: 0;
    color: var(--text);
    font: inherit;
    font-size: 1rem;
    text-align: left;
    cursor: pointer;
}

.work-tick:disabled { cursor: default; opacity: 0.75; }

.work-box {
    display: flex;
    align-items: center;
    justify-content: center;
    flex: 0 0 auto;
    width: 28px;
    height: 28px;
    border: 2px solid var(--border);
    border-radius: 8px;
    color: transparent;
    transition: background-color 160ms ease, border-color 160ms ease, color 160ms ease;
}

.work-body { flex: 1 1 auto; }

/* DONE. Moosgrün as a fill with white on it — the only way §8.1 allows
   the green, and at a glance from two metres it is the state that has to
   be unmistakable. */
.work-step.is-done {
    border-color: var(--moosgruen);
    background: color-mix(in srgb, var(--moosgruen) 22%, var(--bg-raised));
}

.work-step.is-done .work-box {
    background: var(--moosgruen);
    border-color: var(--moosgruen);
    color: #ffffff;
}

.work-step.is-done .work-body {
    color: var(--text-muted);
    text-decoration: line-through;
}

/* The checkmark draws rather than appearing. 
   One small moment per item, and nothing that keeps moving afterwards —
   §UX-5 allows warmth, not a screen that fidgets. */
.work-step.is-done .work-box svg {
    stroke-dasharray: 24;
    stroke-dashoffset: 0;
    animation: work-check 220ms ease-out;
}

@keyframes work-check {
    from { stroke-dashoffset: 24; }
    to   { stroke-dashoffset: 0; }
}

/* Following a finger. app.js sets this while a row is being dragged; the
   row springs back if the drag was too short to count. */
.work-step[data-sliding] { transition: none; }
.work-step.is-settling   { transition: transform 180ms ease; }

@media (prefers-reduced-motion: reduce) {
    .work-box,
    .work-step.is-done .work-box svg,
    .work-step.is-settling { transition: none; animation: none; }
}

.work-meta {
    margin: 0;
    padding: 0 14px 8px 54px;
    font-size: 0.75rem;
}

.work-note {
    margin: 0;
    padding: 0 14px 10px 54px;
    font-size: 0.85rem;
}

.work-note-edit > summary {
    padding: 6px 14px 10px 54px;
    font-size: 0.75rem;
    color: var(--text-muted);
    cursor: pointer;
}

.work-note-edit form {
    display: flex;
    gap: 8px;
    padding: 0 14px 12px 54px;
}

.work-note-edit input[type="text"] { flex: 1 1 auto; }

/* A step still waiting for its photo. Held shut rather than refusing when
   pressed — a button that looks ready and then does nothing is the worst
   of both. The camera below is the way through, and it ticks the step. */
.work-step.needs-photo { border-style: dashed; }
.work-step.needs-photo .work-tick { opacity: 0.7; }

.work-photo-form {
    display: flex;
    gap: 8px;
    align-items: center;
    margin: 0;
    padding: 0 14px 12px 54px;
}

.work-photo-btn { margin: 0; cursor: pointer; }

.work-photo {
    margin: 0;
    padding: 0 14px 12px 54px;
}

.work-photo img {
    max-width: 160px;
    max-height: 160px;
    border-radius: var(--radius);
    border: 1px solid var(--border);
}

/* While a photo is on its way up. A phone picture is several megabytes
   and takes seconds; without this the camera closed and the row looked
   untouched, which reads as "it did not work". */
.work-step.is-uploading {
    border-style: solid;
    border-color: var(--moosgruen);
}

.work-step.is-uploading .work-photo-btn {
    opacity: 0.7;
    pointer-events: none;
}

.work-photo { display: flex; align-items: flex-start; gap: 12px; }
.work-photo form { margin: 0; }

/* --- the checklists of a channel ------------------------------------- */
.work-list { list-style: none; margin: 0; padding: 0; }

.work-list-row {
    padding: 12px 0;
    border-bottom: 1px solid var(--border);
}

.work-list-row:last-child { border-bottom: 0; }
.work-list-row.is-dormant { opacity: 0.6; }

.work-list-head {
    display: flex;
    align-items: baseline;
    flex-wrap: wrap;
    gap: 8px;
}

.work-list-title { font-size: 1.05rem; font-weight: 600; }
.work-list-meta  { margin: 2px 0 8px; font-size: 0.8rem; }

.work-list-actions {
    display: flex;
    align-items: center;
    gap: 14px;
    margin: 8px 0 0;
    font-size: 0.85rem;
}

.work-list-actions form { margin: 0; }

.work-new label { display: block; margin-top: 12px; }

/* ---------------------------------------------------------------------
   TO-DOS — the same work item as a checklist, shown as a flat list.

   Deliberately plainer than .work-list above. A checklist row carries a
   recurrence, a step count, a switch and a link to its record; a to-do
   is one line of text somebody typed on the floor, and dressing it up
   with the same furniture would make ten repairs unreadable.
   --------------------------------------------------------------------- */
.todo-list { list-style: none; margin: 0 0 16px; padding: 0; }

.todo-row {
    padding: 2px 0;
    border-bottom: 1px solid var(--border);
}

.todo-row:last-child { border-bottom: 0; }

/* Done, but still here — the undo window. Faded rather than removed, so
   a mis-tick can be put back without anybody going looking for it. */
.todo-row.is-settled { opacity: 0.55; }

.todo-list.is-past { font-size: 0.9rem; }

/* THE ACTIONS LIVE IN THE BOX, on its right edge behind a rule.
   Asked for 2026-09-02. The box is the task; what you can do to it
   belongs on the same object rather than orbiting underneath it.
   Only inside .todo-row — the same .work-step markup is the run screen's
   checklist, where there are no per-step links and the box is a box. */
.todo-row .work-step,
.todo-row .todo-box {
    display: flex;
    align-items: stretch;
    border: 0;
    padding: 4px 0;
}

.todo-row .todo-box {
    border-radius: var(--radius);
    background: var(--bg-raised);
    margin: 0 0 8px;
}

/* min-width: 0 or a long task title refuses to shrink and pushes the
   actions off the right-hand side. */
.todo-row .work-tick-form,
.todo-row .todo-link { flex: 1 1 auto; min-width: 0; }

/* NOT --border FOR THIS RULE. That token is drawn against --bg, and
   this line sits on --bg-raised: #272727 on #181818 is 1.15:1, which is
   a divider nobody can see (measured 2026-09-02). Mixed off the muted
   text instead, which lands near 2.4:1 on the dark surface and 2.3:1 on
   white — a hairline you notice without it becoming a wall. */
.todo-row .todo-actions {
    flex: 0 0 auto;
    align-self: center;
    margin: 6px 14px 6px 0;
    padding-left: 14px;
    border-left: 1px solid color-mix(in srgb, var(--text-muted) 40%, transparent);
}

/* NOT SIDE BY SIDE ON A PHONE. "Fragen & Notizen · Bearbeiten" is most
   of a narrow screen on its own, and squeezing a task title beside it
   wraps every one of them to three lines. Still in the box — the rule
   just moves from the left edge to the top. */
@media (max-width: 560px) {
    .todo-row .work-step,
    .todo-row .todo-box {
        display: block;
    }

    .todo-row .todo-actions {
        display: flex;
        margin: 0 14px;
        padding: 8px 0 6px;
        border-left: 0;
        border-top: 1px solid color-mix(in srgb, var(--text-muted) 40%, transparent);
    }
}

.todo-link {
    display: flex;
    align-items: baseline;
    justify-content: space-between;
    gap: 10px;
    padding: 10px 0;
    text-decoration: none;
    color: var(--text);
}

.todo-meta {
    margin: 0 0 8px;
    padding-left: 2px;
    font-size: 0.78rem;
}

/* WHO WROTE IT AND WHEN, under the title and inside the box. Quieter
   than the task itself — it is the answer to "how long has this been
   sitting here", which is worth having and never worth reading first. */
.todo-byline {
    display: block;
    margin-top: 2px;
    color: var(--text-muted);
    font-size: 0.78rem;
    font-weight: 400;
}

.todo-link-body { flex: 1 1 auto; }

/* REAL BUTTONS, asked for 2026-09-02. Two underlined words at the edge
   of a box read as an afterthought and are a thin target for a thumb;
   the same shape the inventory screens use gives them a border, a fill
   and a 34px height. No separator between them any more — a button has
   its own edges, and a dot between two of them is a line nobody needs.
   Still <a> elements: they go somewhere, and a link that looks like a
   button must still open in a new tab when somebody asks it to. */
.todo-actions {
    display: inline-flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 6px;
}

.todo-action > a {
    display: inline-flex;
    align-items: center;
    gap: 5px;
    white-space: nowrap;
    min-height: 34px;
    padding: 0 12px;
    background: var(--bg-input);
    border: 1px solid var(--border);
    border-radius: 999px;
    color: var(--text);
    font-size: 0.82rem;
    font-weight: 600;
    text-decoration: none;
}

.todo-action > a:hover { border-color: var(--text-muted); }

/* Overdue is the one place a to-do raises its voice, and it does it with
   the colour this app already uses for urgent — not a second red. */
.pill-warn {
    border-color: var(--urgent);
    color: var(--urgent);
}

/* READING ENDS HERE, WRITING BEGINS.

   Without it the add box grew straight out of the last task and the two
   read as one list — reported 2026-08-20, and worse for an admin, who
   sees a second create form further down as well.

   The same 2px terracotta rule the app header and the channel header
   already use for exactly this, rather than a new device: it is the mark
   this design system uses to say "different thing below". */
.create-start {
    margin-top: 26px;
    padding-top: 18px;
    border-top: 2px solid var(--terracotta);
}

/* ONE LINE AND A BUTTON. Reporting a broken chair mid-shift has to be
   as cheap as writing a message, so the date and the person are a
   second row that most people never touch. */
.todo-add {
    display: flex;
    flex-wrap: wrap;
    gap: 8px;
    margin: 0 0 8px;
}

/* Its own row above the field, and it is the heading for this block as
   well as the field's label — one element doing both jobs, so a screen
   reader is not read the same three words twice. */
.todo-add > .section-heading {
    flex: 1 1 100%;
    margin: 0 0 4px;
}

.todo-add input[type="text"] { flex: 1 1 12rem; }

.todo-add-extra {
    display: flex;
    flex-wrap: wrap;
    gap: 8px;
    flex: 1 1 100%;
}

.todo-add-extra select { flex: 1 1 8rem; }

/* The message a to-do came out of, on the screen where you name it.
   Quiet: enough to recognise, not so much that it competes with the
   field you are there to edit. */
.todo-source {
    margin: 0 0 20px;
    padding: 10px 14px;
    border-left: 3px solid var(--border);
    background: var(--surface-sunken);
}

.todo-source p { margin: 0; }
.todo-source p + p { margin-top: 4px; }

.todo-from-message label { display: block; margin-top: 14px; }

/* Folded away rather than sitting next to Speichern. §8.4 has this being
   tapped one-handed in a hurry, and a delete button a thumb-width from
   save is a mistake waiting to happen — opening it IS the confirmation. */
.danger-zone {
    margin-top: 32px;
    padding-top: 16px;
    border-top: 1px solid var(--border);
}

.danger-zone > summary {
    cursor: pointer;
    color: var(--text-muted);
    font-size: 0.9rem;
}

.danger-zone p { margin: 10px 0; font-size: 0.85rem; }

/* ---------------------------------------------------------------------
   THE THREAD ON A PIECE OF WORK.

   Looks like the channel it is not: this is a handful of lines about one
   task, and giving it avatars, reactions and read state would make it
   compete with the messaging half of the app for no gain.
   --------------------------------------------------------------------- */
.work-thread { list-style: none; margin: 8px 0 16px; padding: 0; }

.work-comment {
    padding: 10px 0;
    border-bottom: 1px solid var(--border);
}

.work-comment:last-child { border-bottom: 0; }
.work-comment.is-gone    { opacity: 0.6; }

.work-comment-head {
    margin: 0 0 4px;
    font-size: 0.78rem;
}

.work-comment-body { margin: 0; overflow-wrap: anywhere; }

.work-comment-remove { margin: 4px 0 0; }
.work-comment-remove .link-button { font-size: 0.78rem; color: var(--text-muted); }

.work-comment-add { margin-bottom: 24px; }
.work-comment-add textarea { width: 100%; margin-bottom: 8px; }

/* Datei, Mikrofon and Schreiben on one line — the composer's row, at
   the size a thread under a task wants. .btn is display:block and
   full-width in this app, so each of the three has to be told
   otherwise or they stack into three slabs. */
.work-comment-tools {
    display: flex;
    align-items: center;
    gap: 8px;
    flex-wrap: wrap;
}

.work-comment-tools .btn {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: auto;
    margin: 0;
}

/* The label is the file button, so it has to look pressable. */
.work-comment-tools label.btn { cursor: pointer; }

/* Sending is the one that should read as the action; it goes last and
   takes the room that is left. */
.work-comment-tools .btn-confirm { margin-left: auto; }

/* A file already chosen, above the row rather than inside it. */
.work-comment-add [data-file-preview]:not(:empty) { margin-bottom: 8px; }

/* =====================================================================
   RECIPE CARDS (A42)

   Two screens with different jobs. The INDEX answers "which drink" and
   is a grid of small tiles you scan; the CARD answers "how do I make
   it" and is one column you read at arm's length with one hand full
   (§8.4). Nothing here needs JavaScript: the groups are <details> and
   the search is a GET form.
   ===================================================================== */

/* ---------------------------------------------------------------------
   THE RECIPE SEARCH — one big field, no button.

   It was a small input beside a Suchen button, which is the shape of a
   search that costs a round trip. Every card is already on the page, so
   app.js filters as you type and hides the button; this is what is left,
   and it is sized like the thing you are meant to reach for first.
   --------------------------------------------------------------------- */
.recipe-search {
    display: flex;
    flex-wrap: wrap;
    gap: 8px;
    margin: 0 0 6px;
}

/* The icon and the clear button ride inside the field rather than beside
   it, so the tap target is the whole box. */
.recipe-search-field {
    position: relative;
    display: flex;
    align-items: center;
    flex: 1 1 100%;
}

.recipe-search-field > svg {
    position: absolute;
    left: 14px;
    color: var(--text-muted);
    pointer-events: none;
}

.recipe-search input[type="search"] {
    width: 100%;
    min-height: 52px;
    /* Room for the icon on the left and the cross on the right. */
    padding: 12px 46px 12px 44px;
    background: var(--bg-input);
    border: 1px solid var(--border);
    border-radius: 999px;
    color: var(--text);
    font: inherit;
    /* 16px is the floor below which iOS Safari zooms the page on focus,
       and this one is deliberately above it: it is the field somebody
       uses mid-shift. */
    font-size: 1.05rem;
    -webkit-appearance: none;
    appearance: none;
}

.recipe-search input[type="search"]:focus {
    outline: none;
    border-color: var(--terracotta);
}

/* Safari draws its own cross for type=search and Firefox does not, so
   the field's own is suppressed and one of ours is used instead. */
.recipe-search input[type="search"]::-webkit-search-cancel-button {
    -webkit-appearance: none;
    appearance: none;
}

.recipe-search-clear {
    position: absolute;
    right: 6px;
    width: 38px;
    height: 38px;
    padding: 0;
    border: 0;
    border-radius: 999px;
    background: transparent;
    color: var(--text-muted);
    font-size: 1.4rem;
    line-height: 1;
    cursor: pointer;
}

.recipe-search-clear:hover { color: var(--text); }

.recipe-search-count {
    margin: 0 0 14px;
    padding-left: 16px;
    font-size: 0.82rem;
}

.recipe-group { margin-bottom: 10px; }

.recipe-group > summary {
    display: flex;
    align-items: center;
    gap: 8px;
    padding: 10px 0;
    cursor: pointer;
    border-bottom: 1px solid var(--border);
}

.recipe-group-name { font-weight: 600; }

/* AUTO-FILL RATHER THAN A FIXED COUNT, so one column on a phone and
   three on a laptop happen without a breakpoint to maintain. */
.recipe-grid {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
    gap: 12px;
    list-style: none;
    margin: 14px 0 6px;
    padding: 0;
}

/* --- stages within a dish (A42) ------------------------------------- */

/* A NATIVE <details>. No script — the CSP forbids inline ones anyway —
   it works with a keyboard, and every browser already knows how to open
   it. The only thing worth styling is the summary row, which has to be a
   comfortable target for a thumb in a dim kitchen. */
.stage {
    margin-top: 8px;
    border: 1px solid var(--border);
    border-radius: var(--radius);
    background: var(--bg-raised);
    overflow: hidden;
}

.stage > summary {
    display: flex;
    align-items: center;
    gap: 10px;
    /* 44px of height without saying 44px: §8.4's one-handed target. */
    padding: 12px 14px;
    cursor: pointer;
    list-style: none;
    font-weight: 600;
}

/* Safari draws its own triangle through ::-webkit-details-marker. */
.stage > summary::-webkit-details-marker { display: none; }

/* The affordance, drawn rather than defaulted so it sits where the
   design wants it and rotates when open. */
.stage > summary::after {
    content: "";
    width: 8px;
    height: 8px;
    margin-left: auto;
    border-right: 2px solid var(--text-muted);
    border-bottom: 2px solid var(--text-muted);
    transform: rotate(45deg) translate(-2px, -2px);
    transition: transform 120ms ease;
}

.stage[open] > summary::after { transform: rotate(-135deg) translate(-2px, -2px); }

.stage > summary:hover { background: var(--bg-input); }

/* THE NUMBER IS THE ORDER, and it stays visible when the section is
   closed — "Marinade · Am Grill · Anrichten" is the shape of the dish
   and reading it should not require opening anything. */
.stage-no {
    flex: 0 0 auto;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 24px;
    height: 24px;
    border-radius: 50%;
    /* Moosgrün is a FILL with white lettering — §8.1. Never a thin line. */
    background: var(--moosgruen);
    color: #ffffff;
    font-size: 0.8rem;
}

.stage-title { min-width: 0; }

.stage > :not(summary) { margin: 0 14px 12px; }

.stage-photo {
    display: block;
    width: 100%;
    max-width: 320px;
    border-radius: 6px;
}

/* --- more than one photo (A42) -------------------------------------- */

.recipe-gallery,
.recipe-photo-grid,
.stage-photos {
    display: flex;
    flex-wrap: wrap;
    gap: 8px;
    margin: 8px 0 0;
    padding: 0;
    list-style: none;
}

.recipe-gallery img {
    width: 88px;
    height: 88px;
    object-fit: cover;
    border-radius: 6px;
}

/* A REAL PREVIEW, which this was not. It showed a 120x90 crop out of the
   middle, so the one screen where somebody checks what they just
   uploaded was the screen least able to show it. Reported 2026-08-25.
   Big enough to judge the framing, whole rather than cropped, and it
   still lines up in a row because the frame is fixed. */
.recipe-photo-cell {
    width: 220px;
    text-align: center;
}

.recipe-photo-cell img {
    width: 100%;
    height: 165px;
    object-fit: contain;
    background: var(--bg-input);
    border-radius: 6px;
}

/* On a phone one preview per row, at a size worth looking at. */
@media (max-width: 520px) {
    .recipe-photo-cell { width: 100%; }
    .recipe-photo-cell img { height: 220px; }
}

.stage-photos { flex-direction: column; }

.stage-photos li {
    display: flex;
    align-items: center;
    gap: 10px;
    flex-wrap: wrap;
    padding: 8px 0;
    border-top: 1px solid var(--border);
}

/* The stage's own preview, same reasoning as the dish's: it is here to
   be checked, so it is not cropped. */
.stage-photos img {
    width: 140px;
    height: 105px;
    object-fit: contain;
    background: var(--bg-input);
    border-radius: 4px;
}
.stage-photo-name { font-weight: 600; min-width: 120px; }

.stage-editor .stage-row {
    margin: 0 0 10px;
    padding: 10px 12px;
    border: 1px solid var(--border);
    border-radius: var(--radius);
}

.stage-editor .stage-row legend {
    padding: 0 6px;
    color: var(--text-muted);
    font-size: 0.85rem;
}

/* The blank slots are quieter than the real ones, so the list reads as
   "three stages and room for more" rather than as five stages. */
.stage-row-new { opacity: 0.75; }

.stage-tools {
    display: flex;
    align-items: center;
    gap: 8px;
    margin-top: 6px;
    flex-wrap: wrap;
}

.stage-tools .page-help { margin: 0; }

/* Added by script, so it only appears where it can actually work. */
.stage-add {
    width: auto;
    margin: 0 0 14px;
}

/* --- allergens (A42) ------------------------------------------------ */

/* NOT A STATE COLOUR. §8.2 reserves red and amber for status, and an
   allergen is a fact about the dish rather than a warning about it —
   fourteen red chips on a menu would drain the meaning out of the one
   place red is supposed to stop somebody. Neutral chips, which is also
   what makes them readable at a glance rather than alarming. */
.allergen-chips {
    display: flex;
    flex-wrap: wrap;
    gap: 6px;
    margin: 6px 0 0;
}

.allergen-chip {
    display: inline-block;
    padding: 2px 8px;
    border: 1px solid var(--border);
    border-radius: 999px;
    background: var(--bg-raised);
    color: var(--text-muted);
    font-size: 0.78rem;
    line-height: 1.5;
    white-space: nowrap;
}

/* Inside a shared card the surface is already --bg, so the chip needs
   the other one to stay visible against it. */
.recipe-embed .allergen-chip { background: var(--bg-raised); }

/* NOT AN ALLERGEN. Garlic and pork are things guests ask about, and a
   card must never be readable as declaring them alongside the fourteen.
   A dashed edge rather than a colour: §8.2 keeps colour for status, and
   the difference here is one of KIND, which a different shape says
   better than a different hue — and says it in greyscale too. */
.allergen-chip.is-tag {
    border-style: dashed;
    background: transparent;
}

/* NO font-weight HERE, and that is the fix rather than an omission.
   This is an <h3>, so it is set in Railroad Gothic — which ships ONE
   weight, 400. Asking for 600 does not make it bolder; it makes the
   browser SYNTHESISE a bold by smearing each glyph sideways, which
   renders as a soft, out-of-focus heading. Reported 2026-08-25 as
   "the 'weitere angaben' is very blurry", and that is exactly what it
   was: a faux-bold, not a colour or a size problem.
   §8.3's rule already says this face is display-only; the weights it
   does not have are part of the same constraint. Matched to
   .section-heading, which gets it right by not asking. */
.allergen-subhead {
    margin: 16px 0 6px;
    font-size: 0.78rem;
    text-transform: uppercase;
    letter-spacing: 0.06em;
    color: var(--text-muted);
}

.allergen-picker {
    margin: 14px 0 0;
    padding: 12px;
    border: 1px solid var(--border);
    border-radius: var(--radius);
}

.allergen-picker legend {
    padding: 0 6px;
    font-size: 0.85rem;
    color: var(--text-muted);
}

/* Fourteen of them. One per line is a scroll; a grid is a glance. */
.allergen-grid {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(190px, 1fr));
    gap: 4px 14px;
}

.recipe-tile { position: relative; }

.recipe-tile > a {
    display: block;
    padding: 10px;
    border: 1px solid var(--border);
    border-radius: var(--radius);
    background: var(--bg-raised);
    color: var(--text);
    text-decoration: none;
}

.recipe-tile > a:hover { border-color: var(--terracotta); }

/* A FIXED ASPECT, PHOTO OR NOT. The empty frame is the same size as a
   filled one, so the grid does not reflow the day a picture is uploaded
   — and an obviously empty slot is a visible invitation to add one. */
/* PORTRAIT, BECAUSE THE PHOTOS ARE PORTRAIT. Measured rather than
   assumed: the first real pictures in the app are 900x1600 and 819x1298,
   which is what a phone held normally produces and what a kitchen will
   keep producing.
   In the old 4:3 frame a 9:16 photo showed 42% of its height — the
   "very zoomed in" band reported on 2026-08-25. At 3:4 it shows 75%.
   Switching to contain instead would have avoided the crop and made the
   drink a narrow sliver between two empty margins, which is worse; a
   grid needs one frame, and the honest choice is to match it to the
   pictures that actually go in it.
   Revisit if landscape shots start appearing. */
.recipe-thumb {
    display: block;
    aspect-ratio: 3 / 4;
    margin-bottom: 8px;
    border-radius: 6px;
    overflow: hidden;
    background: var(--bg-input);
}

.recipe-thumb img {
    width: 100%;
    height: 100%;
    object-fit: cover;
    /* Food is framed in the upper half far more often than the lower.
       Costs nothing and beats centre for a cropped plate. */
    object-position: center 40%;
    display: block;
}

.recipe-thumb-empty {
    display: flex;
    align-items: center;
    justify-content: center;
    width: 100%;
    height: 100%;
    color: var(--text-muted);
}

.recipe-tile-name { display: block; font-weight: 600; }

.recipe-tile-glass {
    display: flex;
    align-items: center;
    gap: 5px;
    margin-top: 3px;
    font-size: 0.8rem;
}

.recipe-tile > .pill {
    position: absolute;
    top: 6px;
    right: 6px;
    background: var(--bg);
}

/* ---------------------------------------------------------------------
   EDITING THE CATEGORIES — one form, one Save.

   This was a form per category, each carrying its own full-width green
   Speichern. Fine at two, absurd at the eleven the bar actually has:
   eleven slabs down the page with the names lost between them. Reported
   2026-08-22.

   Now every name is a field in ONE form and the ordering and delete
   controls are small buttons on their own row, because a submit button
   sends only its own name and value — so the structure of the markup
   does the work that would otherwise need JavaScript.
   --------------------------------------------------------------------- */
.cat-list { list-style: none; margin: 0 0 14px; padding: 0; }

.cat-row {
    display: flex;
    align-items: center;
    gap: 6px;
    margin-bottom: 6px;
}

.cat-row input[type="text"] { flex: 1 1 8rem; min-width: 0; }

/* Square, quiet, and the same height as the field beside them. */
.cat-icon {
    flex: 0 0 auto;
    width: var(--touch-min);
    height: var(--touch-min);
    padding: 0;
    border: 1px solid var(--border);
    border-radius: 8px;
    background: transparent;
    color: var(--text);
    font: inherit;
    font-size: 1rem;
    line-height: 1;
    cursor: pointer;
}

.cat-icon:hover:not(:disabled) { border-color: var(--terracotta); }

/* At the ends, dimmed rather than removed: a row that changes width as
   it moves up the list is a row you lose track of. */
.cat-icon:disabled { opacity: 0.35; cursor: default; }

.cat-remove { flex: 0 0 auto; font-size: 0.82rem; }

.cat-add {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 8px;
    margin: 10px 0 0;
}

.cat-add input[type="text"] { flex: 1 1 10rem; }

/* --- one card ------------------------------------------------------ */

.recipe-card {
    border: 1px solid var(--border);
    border-radius: var(--radius);
    background: var(--bg-raised);
    overflow: hidden;
}

.recipe-photo { display: block; }

/* THE WHOLE PICTURE, NEVER A CROP OF IT.
   This was object-fit: cover at full width and 340px tall, which on a
   portrait photo from a phone — which is what a kitchen actually takes —
   showed a narrow band out of the middle and read as "very zoomed in".
   Reported 2026-08-25 with the first real photos in the app.
   The rule, applied everywhere a recipe photo appears: where the photo
   is the thing you are looking at, show all of it; where it is a small
   index image in a row of others, cover is fine. */
.recipe-photo img {
    width: 100%;
    max-height: 420px;
    object-fit: contain;
    background: var(--bg-input);
    display: block;
}

.recipe-photo-empty {
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: 8px;
    aspect-ratio: 16 / 9;
    max-height: 260px;
    background: var(--bg-input);
    color: var(--text-muted);
}

.recipe-body { padding: 18px; }

.recipe-name {
    margin: 0;
    font-size: 1.6rem;
    letter-spacing: 0.01em;
}

.recipe-subtitle { margin: 2px 0 0; color: var(--text-muted); }

.recipe-section {
    margin-top: 20px;
    padding-top: 14px;
    border-top: 1px solid var(--border);
}

/* The small label with its icon — GLAS, ZUTATEN, ZUBEREITUNG. */
.recipe-label {
    display: flex;
    align-items: center;
    gap: 8px;
    margin: 0 0 6px;
    font-size: 0.78rem;
    text-transform: uppercase;
    letter-spacing: 0.06em;
    color: var(--text-muted);
}

.glass-icon { flex: 0 0 auto; }

.recipe-value { margin: 0; }

.recipe-list,
.recipe-steps { margin: 0; padding-left: 20px; }

.recipe-list { list-style: disc; }
.recipe-list li,
.recipe-steps li { margin-bottom: 4px; }

.recipe-note {
    margin: 18px 0 0;
    font-size: 0.85rem;
}

.recipe-actions {
    display: flex;
    align-items: center;
    flex-wrap: wrap;
    gap: 14px;
    margin: 18px 0 0;
}

/* --- the fields you actually type in --------------------------------
   A textarea inherited almost nothing and looked it: no padding worth
   the name, a thinner border than the text inputs beside it, and a
   default line-height that made three lines of a method feel cramped.
   Reported 2026-08-25 as "the editable fields look a bit crappy".
   Matched to input[type=text] deliberately rather than styled
   separately — a form where the boxes disagree with each other reads as
   unfinished however nice each one is on its own. */
.recipe-form label,
.stage-editor label:not(.visually-hidden):not(.file-pick),
.allergen-picker label:not(.tag-check) { display: block; margin-top: 14px; }

.recipe-form textarea,
.recipe-form input[type="text"],
.recipe-form input[type="date"],
.recipe-form input[type="email"],
.recipe-form select,
.stage-editor textarea,
.stage-editor input[type="text"] {
    width: 100%;
    padding: 10px 12px;
    background: var(--bg-input);
    border: 1px solid var(--border);
    border-radius: 8px;
    color: var(--text);
    font: inherit;
    font-size: 16px;   /* below 16px, iOS Safari zooms on focus */
    line-height: 1.5;
}

.recipe-form textarea,
.stage-editor textarea {
    display: block;
    min-height: 96px;
    /* Only vertically: a textarea dragged wider than its column breaks
       the layout on a phone and there is nothing to gain. */
    resize: vertical;
}

.recipe-form textarea::placeholder,
.recipe-form input::placeholder,
.stage-editor textarea::placeholder,
.stage-editor input::placeholder { color: var(--text-muted); opacity: 1; }

/* --- a file button that looks like the other buttons ----------------
   The browser's own "Datei auswählen / Choose file" control cannot be
   styled, so it is hidden and a label stands in for it — clicking a
   label bound to a file input opens the picker, which is plain HTML and
   needs no script. The input keeps its size so it stays reachable by
   keyboard and by a screen reader. */
.file-pick {
    display: inline-flex;
    align-items: center;
    gap: 8px;
    min-height: var(--touch-min);
    padding: 8px 14px;
    border: 1px solid var(--border);
    border-radius: 8px;
    background: var(--bg-input);
    color: var(--text);
    cursor: pointer;
    font-size: 0.9rem;
}

.file-pick:hover { border-color: var(--text-muted); }

.file-pick input[type="file"] {
    position: absolute;
    width: 1px;
    height: 1px;
    opacity: 0;
    pointer-events: none;
}

/* Says which file was picked, once one has been — otherwise the button
   looks identical before and after choosing, which is how people upload
   nothing and wonder why. */
.file-pick.has-file { border-color: var(--terracotta); }

/* A button that sits on a line with other controls rather than filling
   the width, since .btn is deliberately full-width. */
.btn-inline {
    min-height: var(--touch-min);
    padding: 8px 14px;
    border: 1px solid var(--border);
    border-radius: 8px;
    background: var(--bg-raised);
    color: var(--text);
    font: inherit;
    font-size: 0.9rem;
    cursor: pointer;
}

.btn-inline:hover { border-color: var(--text-muted); }

/* The stage's own photo, directly under the text it belongs to. */
.stage-photo-row {
    display: flex;
    align-items: center;
    gap: 10px;
    flex-wrap: wrap;
    margin-top: 8px;
}

.stage-photo-row img {
    width: 96px;
    height: 72px;
    object-fit: contain;
    background: var(--bg-input);
    border-radius: 6px;
}

.recipe-photo-current {
    display: flex;
    align-items: flex-start;
    gap: 12px;
    margin-bottom: 10px;
}

.recipe-photo-current img {
    max-width: 160px;
    border-radius: 6px;
}

.recipe-photo-form { display: flex; flex-wrap: wrap; gap: 8px; align-items: center; }

/* --- before and after ---------------------------------------------- */

.recipe-diff { margin: 0 0 16px; }

/* SIDE BY SIDE WHERE THERE IS ROOM, STACKED WHERE THERE IS NOT. Two
   columns of 150px on a phone would wrap every ingredient line and make
   the comparison harder than reading them one after the other. */
.recipe-diff-pair {
    display: grid;
    grid-template-columns: 1fr;
    gap: 10px;
}

@media (min-width: 620px) {
    .recipe-diff-pair { grid-template-columns: 1fr 1fr; }
}

.recipe-diff-before,
.recipe-diff-after {
    padding: 10px 12px;
    border-radius: 6px;
    background: var(--bg-input);
}

.recipe-diff-before { opacity: 0.75; }

/* The new wording is the thing being decided on, so it is the one with
   the accent — a left edge rather than coloured text, which at this size
   would fail contrast. */
.recipe-diff-after { border-left: 3px solid var(--moosgruen); }

.recipe-diff-before p,
.recipe-diff-after p { margin: 0 0 2px; }

.recipe-decide label { display: block; margin-top: 14px; }
.recipe-decide input[type="text"] { width: 100%; }

/* One capability, in one channel, for one person — §Rule 5. Sits under
   the tier selector on the admin user screen, which is the only place
   anybody looks for "what may this person do". */
.grant-list { list-style: none; margin: 0 0 12px; padding: 0; }

.grant-row {
    display: flex;
    align-items: center;
    flex-wrap: wrap;
    gap: 10px;
    padding: 8px 0;
    border-bottom: 1px solid var(--border);
}

.grant-row:last-child { border-bottom: 0; }
.grant-row form { margin: 0; }
.grant-name { font-weight: 600; }

/* A DROPDOWN BELONGS TO THE BUTTON UNDER IT, and until 2026-09-03 it
   looked like it belonged to the button above: "the location chooser is
   closer to the recipes than to the order permission."

   Nothing was mispaired — the markup had each select inside its own
   form. It was spacing. .btn is width:100% with margin-top:24px, so
   inside this flex row it wraps onto its own line and lands 32px below
   its own select, while the NEXT form's select followed at the 8px gap.
   Each control sat nearer the wrong half of the pair.

   So the gap inside a pair is the flex gap, and the distance between
   pairs is bigger than both. Only became visible when a second grant
   appeared beneath the first. */
.grant-add {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 8px;
    margin-top: 28px;
}

.grant-add .btn { margin-top: 0; }

/* =====================================================================
   CALENDAR (Phase 5)

   A LIST OF DAYS, NOT A SEVEN-COLUMN GRID. On a 375px phone a month grid
   gives each day about 50px — room for a dot, so every entry needs a
   second tap before it can be read. This shows the days that have
   something on them, in order, with the entries written out. Empty days
   are absent, which is also the honest answer to "is anything happening".
   ===================================================================== */

/* WHICH VIEW IS SHOWN WHEN NOBODY HAS CHOSEN.
   The server cannot measure a screen, so on 'auto' it renders both and
   one media query picks: list on a phone, week on a laptop. An explicit
   choice renders only that view, and these rules then have nothing to
   hide. Small print, large payoff: it is the only way to be responsive
   here without JavaScript. */
.cal-view-auto .cal-week-wrap { display: none; }

@media (min-width: 760px) {
    .cal-view-auto .cal-week-wrap { display: block; }
    .cal-view-auto .cal-list-wrap { display: none; }
}

/* A COMPACT BUTTON, DEFINED HERE RATHER THAN ASSEMBLED FROM .btn.
   Two things made the first version render as a row of green slabs:
   `.btn` in this app is `display: block; width: 100%` — full-width by
   design, because most buttons here are the only thing on their line —
   and `.btn-small` sets a moosgrün background, sits LATER in this file
   than `.btn-quiet`, and therefore wins at equal specificity. Fighting
   both from a third class would be a third thing to keep in step, so
   the calendar's own controls are their own control. */
.cal-btn {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: auto;
    min-width: var(--touch-min);
    min-height: var(--touch-min);
    margin: 0;
    padding: 0 14px;
    border: 1px solid var(--border);
    border-radius: 999px;
    background: transparent;
    color: var(--text);
    font: inherit;
    font-size: 0.9rem;
    text-decoration: none;
    cursor: pointer;
}

.cal-btn:hover { border-color: var(--terracotta); }

/* Two links and a button; the current one is marked, not removed. */
.cal-views {
    display: flex;
    align-items: center;
    gap: 8px;
    margin-bottom: 18px;
}

.cal-view-btn {
    padding: 6px 14px;
    border: 1px solid var(--border);
    border-radius: 999px;
    color: var(--text-muted);
    text-decoration: none;
    font-size: 0.9rem;
}

.cal-view-btn.is-current {
    background: var(--terracotta);
    border-color: var(--terracotta);
    color: #ffffff;
    font-weight: 600;
}

/* The one filled control on the screen, and it earns it: writing an
   entry is the action this page exists to enable. Pushed to the far end
   so it never sits between the two view links. */
.cal-new {
    margin-left: auto;
    background: var(--moosgruen);
    border-color: var(--moosgruen);
    color: #ffffff;
    font-weight: 600;
}

.cal-new:hover { border-color: var(--moosgruen); filter: brightness(1.45); }

.cal-jump {
    display: flex;
    align-items: center;
    gap: 8px;
    margin-bottom: 16px;
}

/* --- the month ------------------------------------------------------ */

/* SEVEN COLUMNS EVEN ON A PHONE — the one grid that survives 375px,
   because a month cell holds a NUMBER. The week view does not, which is
   why it stacks instead. Tapping a day drops into that week, where there
   is room to read what is on it. */
/* SEVEN EQUAL COLUMNS, AND minmax(0,…) IS WHAT MAKES THEM EQUAL.
   1fr is shorthand for minmax(auto, 1fr), and auto will not shrink a
   track below its content's minimum — so one long entry title widened
   its own day and squeezed the rest. Measured 2026-09-03 on the live
   month: 37, 162, 250, 136, 124, 131, 236px across a week, while the
   weekday strip above it — which holds nothing but "MO" — sat at a tidy
   seven times 94. The days no longer stood under their own names.

   Nothing had changed in this rule. It needed a month with a long
   enough title in it to show, which is why it looked fine for weeks.

   The 0 floor lets a track shrink and hands the problem to the entry,
   which already knows what to do with it: one line, ellipsis. */
.cal-grid-head,
.cal-grid {
    display: grid;
    grid-template-columns: repeat(7, minmax(0, 1fr));
    list-style: none;
    margin: 0;
    padding: 0;
}

.cal-grid-head {
    gap: 2px;
    margin-bottom: 4px;
}

.cal-grid-head li {
    padding: 2px 0;
    text-align: center;
    color: var(--text-muted);
    font-size: 0.68rem;
    text-transform: uppercase;
    letter-spacing: 0.06em;
}

.cal-grid {
    gap: 2px;
    background: var(--border);
    border: 1px solid var(--border);
    border-radius: var(--radius);
    overflow: hidden;
}

/* min-width:0 ALL THE WAY DOWN, or the floor above buys nothing. A grid
   item and a flex item both default to min-width:auto, so a nowrap
   title would still push its width up through the cell and the link
   and back into the track the minmax was meant to free. */
.cal-cell { background: var(--bg-raised); min-width: 0; }

.cal-cell > a {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 3px;
    min-width: 0;
    min-height: 3.2rem;
    padding: 6px 2px;
    color: var(--text);
    text-decoration: none;
}

/* The days borrowed from the months either side are real and clickable,
   just quieter — a delivery on the 1st is worth seeing while you are
   looking at the last week of the month before. */
.cal-cell.is-outside { background: var(--bg); }
.cal-cell.is-outside .cal-cell-num { color: var(--text-muted); }

.cal-cell.is-today .cal-cell-num {
    background: var(--terracotta);
    color: #ffffff;
    border-radius: 999px;
    padding: 0 6px;
}

.cal-cell-num { font-size: 0.95rem; font-variant-numeric: tabular-nums; }

/* The count is the phone's answer and the titles are the laptop's; both
   are in the markup and the breakpoint picks, the same trick the auto
   view uses. */
.cal-cell-count {
    min-width: 18px;
    padding: 0 5px;
    border-radius: 999px;
    background: var(--moosgruen);
    color: #ffffff;
    font-size: 0.7rem;
    font-weight: 700;
}

.cal-cell-list { display: none; }

@media (min-width: 760px) {
    .cal-cell > a { align-items: stretch; min-height: 6.5rem; padding: 6px 8px; }
    .cal-cell-count { display: none; }

    .cal-cell-list {
        display: flex;
        flex-direction: column;
        gap: 2px;
        /* The last link in the chain — see .cal-cell above. */
        min-width: 0;
        width: 100%;
        overflow: hidden;
    }

    .cal-cell-item {
        font-size: 0.75rem;
        line-height: 1.25;
        /* One line each: a cell is a glance, not a read. */
        white-space: nowrap;
        overflow: hidden;
        text-overflow: ellipsis;
    }
}

/* --- the week ------------------------------------------------------- */

/* SEVEN ROWS ON A PHONE, SEVEN COLUMNS ON A LAPTOP, from one piece of
   markup. At 375px a seven-column week gives each day about 50px, which
   holds a dot and nothing readable (§8.4). */
.cal-week {
    display: grid;
    grid-template-columns: 1fr;
    gap: 2px;
    list-style: none;
    margin: 0;
    padding: 0;
    border: 1px solid var(--border);
    border-radius: var(--radius);
    overflow: hidden;
    background: var(--border);
}

@media (min-width: 760px) {
    .cal-week { grid-template-columns: repeat(7, 1fr); }
    .cal-wday { min-height: 8rem; }
    .cal-wday-head { flex-direction: column; align-items: flex-start; gap: 0; }
}

.cal-wday {
    padding: 8px 10px;
    background: var(--bg-raised);
}

/* A day with nothing on it recedes rather than disappearing — the point
   of the week view is that "nothing on Thursday" is an answer. */
.cal-wday.is-empty { background: var(--bg); }

.cal-wday.is-today { box-shadow: inset 3px 0 0 var(--terracotta); }

@media (min-width: 760px) {
    .cal-wday.is-today { box-shadow: inset 0 3px 0 var(--terracotta); }
}

.cal-wday-head {
    display: flex;
    align-items: baseline;
    gap: 8px;
    margin: 0 0 6px;
}

.cal-wday-name {
    font-size: 0.72rem;
    text-transform: uppercase;
    letter-spacing: 0.06em;
    color: var(--text-muted);
}

.cal-wday-num { font-size: 1.05rem; font-weight: 600; }
.cal-wday-none { margin: 0; color: var(--text-muted); }

.cal-nav {
    display: flex;
    align-items: center;
    gap: 10px;
    margin-bottom: 16px;
}

.cal-month {
    flex: 1 1 auto;
    font-size: 1.1rem;
    font-weight: 600;
}

.cal-actions { margin: 0 0 18px; }

.cal-days { list-style: none; margin: 0; padding: 0; }

/* WHAT HAS ALREADY HAPPENED, under what has not. The list opens on
   what is coming — that is the question it is usually opened with — and
   keeps going backwards below this heading rather than behind a switch
   somebody has to find first.

   The heading does the whole job of marking the turn, so it is a real
   heading with room above it: without that, a finished party reads as
   next week's, which is worse than not showing it at all. */
.cal-past-head {
    margin: 32px 0 0;
    padding-top: 20px;
    border-top: 1px solid var(--border);
    font-size: 0.95rem;
    color: var(--text-muted);
}

/* Dimmed as a whole, not per entry: everything under the heading is
   over, and saying so once is quieter than a badge on every line. It is
   still full contrast on hover and focus, because these are links
   people open to answer "what did we do that day". */
.cal-days-past { opacity: 0.72; }

.cal-days-past:hover,
.cal-days-past:focus-within { opacity: 1; }

.cal-day {
    padding: 12px 0;
    border-bottom: 1px solid var(--border);
}

.cal-day:last-child { border-bottom: 0; }

.cal-day-label {
    margin: 0 0 6px;
    font-size: 0.78rem;
    text-transform: uppercase;
    letter-spacing: 0.06em;
    color: var(--text-muted);
}

/* Today gets the app's accent as an edge, not as text: a date in
   terracotta at 0.78rem would fail contrast on this background. */
.cal-day.is-today {
    padding-left: 10px;
    border-left: 3px solid var(--terracotta);
}

.cal-entries { list-style: none; margin: 0; padding: 0; }
.cal-entry + .cal-entry { margin-top: 4px; }

.cal-entry > a {
    display: flex;
    gap: 12px;
    padding: 8px 10px;
    border-radius: var(--radius);
    background: var(--bg-raised);
    color: var(--text);
    text-decoration: none;
}

.cal-entry > a:hover { background: var(--bg-input); }

/* Fixed width so the titles line up down the day — the time is a column,
   not a prefix. */
.cal-when {
    flex: 0 0 4.5rem;
    color: var(--text-muted);
    font-variant-numeric: tabular-nums;
    font-size: 0.9rem;
}

.cal-what { flex: 1 1 auto; }
.cal-title { display: block; font-weight: 600; }
.cal-place { display: block; font-size: 0.85rem; }

/* --- one entry ------------------------------------------------------ */

.cal-card {
    padding: 18px;
    border: 1px solid var(--border);
    border-radius: var(--radius);
    background: var(--bg-raised);
}

.cal-card-when {
    margin: 0;
    font-size: 0.85rem;
    color: var(--text-muted);
}

.cal-card-title { margin: 4px 0 0; font-size: 1.5rem; }
.cal-card-place { margin: 6px 0 0; }
.cal-card-audience { margin: 12px 0 0; font-size: 0.85rem; }
.cal-card-by { margin: 12px 0 0; font-size: 0.8rem; }

/* THE QUIET HISTORY UNDER AN ENTRY (0119). "Please make the log button
   small and insignificant" — so it is the line that was already there,
   .cal-card-by, with a fold behind it: same size, same grey, no box, no
   heading. The only thing that gives it away is the caret. */
.cal-log { margin: 12px 0 0; }

.cal-log-open {
    display: inline-flex;
    align-items: baseline;
    gap: 4px;
    /* A real target for a thumb without drawing a button around it. */
    padding: 4px 0;
    color: var(--text-muted);
    font-size: 0.8rem;
    cursor: pointer;
}

/* A CARET OF OUR OWN. display:inline-flex takes the browser's marker
   away, and without one the line reads as a sentence rather than
   something that opens — which is a fold nobody ever finds. Small enough
   to stay insignificant, which is what was asked for. */
.cal-log-open { list-style: none; }
.cal-log-open::-webkit-details-marker { display: none; }

.cal-log-open::before {
    content: "\25B8";       /* ▸ */
    display: inline-block;
    font-size: 0.75em;
    transition: transform 120ms ease;
}

.cal-log[open] > .cal-log-open::before { transform: rotate(90deg); }

.cal-log[open] > .cal-log-open { color: var(--text); }

.cal-log-list {
    margin: 4px 0 0;
    padding: 8px 0 0 2px;
    border-top: 1px solid var(--border);
    list-style: none;
    color: var(--text-muted);
    font-size: 0.8rem;
}

.cal-log-list li {
    display: flex;
    flex-wrap: wrap;
    gap: 2px 8px;
    padding: 3px 0;
}

/* The date column lines up so the list reads down it, and does not wrap
   mid-timestamp on a narrow phone. */
.cal-log-when { flex: 0 0 auto; font-variant-numeric: tabular-nums; }
.cal-log-what { flex: 1 1 12rem; color: var(--text); }
.cal-log-who  { flex: 0 0 auto; }

/* ON A PHONE THE THREE COLUMNS BECOME TWO LINES. Left as a wrapping row,
   "Verschoben: … → …" took two lines and pushed "von Simon Berger" onto
   a third at the left margin, where it read as a separate entry. When
   and who belong together on one line; what happened gets the width. */
@media (max-width: 640px) {
    .cal-log-list li {
        display: grid;
        grid-template-columns: auto 1fr;
        gap: 0 8px;
    }

    /* The rows are named explicitly: the person comes last in the markup
       (it reads better to a screen reader after what was done), and
       without this the grid would flow it below the sentence instead of
       beside the time. */
    .cal-log-when { grid-column: 1; grid-row: 1; }
    .cal-log-who  { grid-column: 2; grid-row: 1; text-align: right; }
    .cal-log-what { grid-column: 1 / -1; grid-row: 2; }
}

/* The card at the top of a channel when something is due. Loud enough to
   be seen on the way past, quiet enough not to be furniture — it only
   appears while a list is open and unfinished. */
.work-card {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 12px;
    margin: 0 0 12px;
    padding: 12px 14px;
    background: var(--bg-raised);
    border: 1px solid var(--moosgruen);
    border-left-width: 4px;
    border-radius: var(--radius);
    color: var(--text);
    text-decoration: none;
}

.work-card-title { font-weight: 600; }
.work-card-count { color: var(--text-muted); font-variant-numeric: tabular-nums; }

/* --- the record of one checklist -------------------------------------- */
.work-summary {
    margin: 0 0 16px;
    padding: 10px 14px;
    background: var(--bg-raised);
    border-left: 4px solid var(--moosgruen);
    border-radius: var(--radius);
    font-size: 0.95rem;
}

.work-record { list-style: none; margin: 0; padding: 0; }

.work-record-row { border-bottom: 1px solid var(--border); }
.work-record-row:last-child { border-bottom: 0; }

.work-record-row a {
    display: flex;
    align-items: baseline;
    flex-wrap: wrap;
    gap: 4px 12px;
    padding: 10px 4px;
    color: var(--text);
    text-decoration: none;
}

.work-record-day   { flex: 0 0 auto; font-weight: 600; }
.work-record-count {
    flex: 0 0 auto;
    font-variant-numeric: tabular-nums;
    color: var(--text-muted);
}
.work-record-meta  { flex: 1 1 12rem; min-width: 0; font-size: 0.8rem; }

/* A day where everything was done. Marked on the left rather than by
   colouring the row: the list is read by scanning down it for the days
   that are NOT clean, and a page of green tells you nothing. */
.work-record-row.is-full .work-record-count { color: var(--moosgruen); font-weight: 600; }
.work-record-row.is-open .work-record-day   { color: var(--text-muted); }

/* The name on the admin user screen, correctable in place. Its own tiny
   form beside the title rather than a field in the big save below —
   that one carries tier, active and every tag, and a rename must not
   rewrite all of those from whatever the page happened to show. */
.rename-form {
    display: flex;
    gap: 8px;
    align-items: center;
    flex-wrap: wrap;
    margin: 8px 0 4px;
}

.rename-form input[type="text"] {
    flex: 1 1 220px;
    max-width: 340px;
    min-height: var(--touch-min);
    padding: 8px 12px;
    background: var(--bg-input);
    border: 1px solid var(--border);
    border-radius: 8px;
    color: var(--text);
    font: inherit;
    font-size: 16px;
}

/* --- Inventory: requests to central production (2026-08-26) --------- */

/* The count on the tab IS the notification. The owner did not want a
   broadcast for every box of markers, but a request nobody notices rots
   — so the number sits where only the people who can act on it look,
   and pings nobody. Terracotta as a FILL with white text, §8.1: it is
   never a border and never text. */
.tab-count {
    display: inline-block;
    min-width: 18px;
    margin-left: 6px;
    padding: 1px 5px;
    border-radius: 999px;
    background: var(--terracotta);
    color: #ffffff;
    font-size: 0.72rem;
    line-height: 1.5;
    text-align: center;
}

/* WHICH PLACE YOU ARE LOOKING AT, as a segmented control rather than a
   row of links. They are PLACES, and a line of pink text does not say
   that — it reads as four unrelated actions. A box with the current
   segment filled says "you are here, and these are the others" with no
   words at all. Terracotta is the fill, never the text (§8.1). */
.store-switch {
    display: flex;
    flex-wrap: wrap;
    gap: 2px;
    margin: 12px 0 6px;
    padding: 3px;
    border: 1px solid var(--border);
    border-radius: 10px;
    background: var(--bg-input);
}

.store-tab {
    flex: 1 1 auto;
    padding: 8px 14px;
    border-radius: 7px;
    color: var(--text);
    text-decoration: none;
    font-size: 0.9rem;
    text-align: center;
    white-space: nowrap;
}

.store-tab:hover { background: var(--bg-raised); }

.store-tab.is-current {
    background: var(--terracotta);
    color: #ffffff;
}

/* --- work-in-progress recipe cards (2026-08-26) --------------------- */

/* AMBER, NOT RED OR GREEN. §8.2 keeps red for urgent and green for
   confirmed; amber is the "not finished" slot and reads as caution
   rather than alarm. It never becomes decoration, because the people
   who cannot act on it never see a draft at all.
   Amber is LIGHT — 11.5:1 against the dark background as text, but a
   fill needs dark lettering on top rather than white. */
.pill-draft {
    background: var(--attention-fill);
    border-color: var(--attention-fill);
    color: var(--on-light);
}

/* AND A DASHED EDGE, carrying the same thing as a SHAPE. Colour alone
   fails in greyscale and for roughly one man in twelve; "unfinished" is
   exactly what a dashed line already means to everybody. */
.recipe-tile.is-draft > a {
    border-style: dashed;
    border-color: var(--attention);
}

.recipe-tile.is-draft .recipe-thumb { opacity: 0.75; }

/* The banner on the card itself. Left edge only — a full amber panel
   behind body text would be shouting, and this is a statement of fact
   the reader needs once. */
.msg-draft {
    background: var(--bg-input);
    border-left-color: var(--attention);
}

/* --- orders (A44) --------------------------------------------------- */

/* THE NAV ENTRY, beside Kanäle rather than among the tools on the right.
   Ordering is a place you go, like the channels, and the right-hand side
   is already carrying five items on a 375px phone. Dimmed slightly so
   the way home still reads as the primary of the two. */
.app-title-alt {
    opacity: 0.72;
    margin-left: 14px;
}

.app-title-alt:hover,
.app-title-alt:focus-visible { opacity: 1; }

.ord-head {
    display: flex;
    align-items: baseline;
    justify-content: space-between;
    gap: 12px;
    flex-wrap: wrap;
}

/* ONE LINE PER ORDER, growing in width rather than height — the shape
   the inventory list was reshaped into on request, for the same reason:
   a list is read by scanning down one column, not by unpacking a box
   per row. */
/* --- every to-do on one page (2026-09-07) --------------------------- */

/* THE SAME ROW AS THE ORDERS LIST, on purpose. Both answer "which of
   these do I need to deal with", both are scanned down rather than read
   across, and a second shape for the same job is a second thing to
   learn. Only the pills differ, because the states do. */
.work-list { list-style: none; margin: 16px 0 0; padding: 0; }

.work-row + .work-row { border-top: 1px solid var(--border); }

.work-row > a {
    display: block;
    padding: 12px 4px;
    color: var(--text);
    text-decoration: none;
}

.work-row > a:hover { background: var(--bg-input); }

.work-row-main {
    display: flex;
    align-items: center;
    gap: 8px;
    flex-wrap: wrap;
}

.work-row-title { font-weight: 600; }

.work-row-meta { display: block; margin-top: 2px; font-size: 0.85rem; }

/* Finished work stays legible — it is read to check what was done, not
   only glanced past — so this dims rather than greys it out, and the
   line through is on the title alone. */
.work-row.is-done > a { opacity: 0.66; }
.work-row.is-done .work-row-title { text-decoration: line-through; }

.work-row.is-late .work-row-title { color: var(--urgent); }

/* Which restaurant, said only when it is narrower than all of them. */
.work-row-place { border-color: var(--moosgruen); color: var(--text); }

/* THE FILTERS, on one line where there is room. A GET form, so what
   comes back is a URL that can be bookmarked or sent to somebody. */
.work-filter {
    display: flex;
    flex-wrap: wrap;
    align-items: flex-end;
    gap: 10px;
    margin-top: 16px;
}

.work-filter label { display: flex; flex-direction: column; gap: 2px; font-size: 0.85rem; }

.work-filter label.check { flex-direction: row; align-items: center; gap: 6px; }

.work-filter input[type="date"] {
    padding: 8px 10px;
    border: 1px solid var(--border);
    border-radius: 8px;
    background: var(--bg-input);
    color: var(--text);
    font: inherit;
}

.work-filter .btn { margin: 0; width: auto; }

.ord-list { list-style: none; margin: 16px 0 0; padding: 0; }

.ord-row + .ord-row { border-top: 1px solid var(--border); }

.ord-row > a {
    display: block;
    padding: 12px 4px;
    color: var(--text);
    text-decoration: none;
}

.ord-row > a:hover { background: var(--bg-input); }

.ord-row-main {
    display: flex;
    align-items: center;
    gap: 8px;
    flex-wrap: wrap;
}

.ord-row-name { font-weight: 600; }

/* THE ORDER'S NUMBER. Tabular figures so a column of them lines up when
   you scan down for one, and a shade firmer than the rest of the meta
   line — it is the thing being looked for, not context. */
.ord-row-id {
    font-variant-numeric: tabular-nums;
    color: var(--text);
}

/* THE STATUS COLUMN. margin-left:auto rather than a grid: the row wraps
   on a phone, and a real column would either squeeze the supplier's name
   or push the status off the edge. This way it hugs the right on a wide
   screen and falls in beside the name on a narrow one. */
.ord-row-state { margin-left: auto; }

.ord-row-meta {
    display: block;
    margin-top: 2px;
    font-size: 0.86rem;
}

/* THE TWO QUESTIONS. Big targets: this is answered one-handed, mid-shift,
   in a dim room — §8.4 — and picking the wrong restaurant is the one
   mistake here that nobody notices until a pallet arrives. */
/* ONE QUESTION, ANSWERED BY PRESSING THE ANSWER. The restaurant and the
   supplier used to be two fieldsets of radio buttons on one page with a
   Weiter underneath — four taps to start an order, and the second list
   could not be filtered by the first because both were already on
   screen. Now each is its own step, and each row is a link: the choice
   and the confirmation are the same press.

   Sized for §8.4 — one hand, mid-shift, possibly gloves. 56px, which is
   above the 44px floor because these are the two decisions the whole
   ordering flow rests on and there is nothing else on the screen to
   spend the room on. */
.ord-pick { list-style: none; margin: 12px 0 0; padding: 0; }

.ord-pick-row {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 12px;
    min-height: 56px;
    padding: 0 14px;
    margin-bottom: 8px;
    border: 1px solid var(--border);
    border-radius: var(--radius);
    background: var(--bg-input);
    color: var(--text);
    font-weight: 600;
    text-decoration: none;
}

.ord-pick-row:active { background: var(--bg); }

.ord-pick-go { color: var(--text-muted); font-weight: 400; }

.ord-ask fieldset {
    border: 1px solid var(--border);
    border-radius: var(--radius);
    padding: 12px;
    margin: 0 0 16px;
}

.ord-ask legend { padding: 0 6px; font-weight: 600; }

.ord-choice {
    display: flex;
    align-items: center;
    gap: 10px;
    min-height: 44px;
    padding: 4px 2px;
    cursor: pointer;
}

/* THE CART. A row is a name and a number and nothing else — "a shift-fill
   response is two taps, not a form" applies here too: the quantity box is
   the only thing anybody touches on the common path. */
.ord-items { list-style: none; margin: 16px 0; padding: 0; }

.ord-item {
    display: flex;
    align-items: center;
    gap: 10px;
    padding: 8px 0;
    border-bottom: 1px solid var(--border);
}

.ord-item-name { flex: 1 1 auto; min-width: 0; }

.ord-item-unit { display: block; font-size: 0.82rem; }

.ord-item-qty {
    flex: 0 0 5.5rem;
    text-align: right;
    /* Tall enough to hit without aiming. */
    min-height: 44px;
}

.ord-free { border: 1px solid var(--border); border-radius: var(--radius); padding: 12px; }

.ord-free-row {
    display: flex;
    gap: 8px;
    margin-top: 8px;
}

.ord-free-row input:nth-child(1) { flex: 1 1 auto; min-width: 0; }
.ord-free-row input:nth-child(2) { flex: 0 0 4.5rem; text-align: right; }
.ord-free-row input:nth-child(3) { flex: 0 0 6rem; }

/* --- the state pills ------------------------------------------------ */

/* THE ONLY ONE THAT SHOUTS is `failed`, and it earns it: an order that
   never reached the supplier, shown as though it had, is the exact
   failure that leaves a bar without limes on a Saturday. Terracotta as a
   FILL with white on it, never as a thin coloured line — §8.1.

   The rest are deliberately quiet. `sent` is the ordinary case and would
   be pure decoration in colour; `delivered` gets Moosgrün because it is
   the one genuinely finished state, and green means confirmed here. */
.pill-order-failed {
    background: var(--terracotta);
    border-color: var(--terracotta);
    color: #ffffff;
}

.pill-order-open {
    background: var(--attention-fill);
    border-color: var(--attention-fill);
    color: var(--on-light);
}

.pill-order-delivered {
    background: var(--moosgruen);
    border-color: var(--moosgruen);
    color: #ffffff;
}

.pill-order-cancelled { opacity: 0.6; text-decoration: line-through; }

/* --- an order card in a channel ------------------------------------- */

/* Built on the recipe embed's shape rather than beside it: same object,
   same bubble, same reason for its own edge. */
.order-embed {
    display: flex;
    gap: 10px;
    align-items: flex-start;
    max-width: 400px;
    margin-top: 8px;
    padding: 10px;
    border: 1px solid var(--border);
    border-radius: var(--radius);
    background: var(--bg);
    color: var(--text);
    text-decoration: none;
}

.order-embed:hover { border-color: var(--text-muted); }

.order-embed-icon {
    flex: 0 0 auto;
    display: flex;
    align-items: center;
    justify-content: center;
    width: 40px;
    height: 40px;
    border-radius: var(--radius);
    background: var(--bg-input);
    color: var(--text-muted);
}

.order-embed-body { display: flex; flex-direction: column; gap: 3px; min-width: 0; }

.order-embed-name { font-weight: 600; }

.order-embed-sub { font-size: 0.86rem; }

/* --- one order ------------------------------------------------------ */

.ord-detail-head { margin-bottom: 16px; }

.ord-table { width: 100%; border-collapse: collapse; margin: 8px 0; }

.ord-table th,
.ord-table td {
    padding: 8px 6px;
    border-bottom: 1px solid var(--border);
    text-align: left;
}

.ord-table th { font-size: 0.82rem; color: var(--text-muted); font-weight: 600; }

.ord-table .num { text-align: right; }

/* A LINE THAT CAME UP SHORT. Only where the amount differs from what was
   ordered — colouring every delivered line would teach the eye to skip
   the colour, and the whole point is that it lands on the one crate that
   is missing. Weight as well as colour: §8.1, and this is read in a
   corridor with a phone at arm's length. */
.ord-short { color: var(--attention); font-weight: 600; }

/* WHAT ARRIVED, ONE ARTICLE PER ROW. A table would be tidier on a
   desktop and unusable on the phone this is filled in on, so it is a
   list: the name and what was ordered above, the box below, thumb-sized
   and never sharing a line with anything. */
.ord-got { list-style: none; margin: 8px 0 16px; padding: 0; }

.ord-got li {
    display: flex;
    align-items: center;
    gap: 10px;
    flex-wrap: wrap;
    padding: 6px 0;
    border-bottom: 1px solid var(--border);
}

.ord-got label { flex: 1 1 60%; margin: 0; }

.ord-got .ord-got-name { display: block; font-weight: 600; }

.ord-got input {
    flex: 0 0 5.5rem;
    width: 5.5rem;
    min-height: var(--touch-min);
    margin: 0;
    text-align: right;
}

/* THE DELIVERY ACTIONS. Three controls that are not three buttons: each
   posts a different action so each is its own <form>, and "Teilweise
   geliefert" is a <details> whose summary holds a span styled as one.

   btn-confirm came out 48px tall against btn-quiet's 50, so the row
   ended on three slightly different bottoms. Measured, not guessed —
   and the 2px is the whole of it: at any width above the wrap point the
   tops already line up, so this pins the heights and leaves the rest
   alone.

   Below ~640px the row wraps to a second line, which is deliberate.
   Three German button labels do not fit across a phone. */
.ord-actions {
    display: flex;
    align-items: flex-start;
    gap: 8px;
    flex-wrap: wrap;
    margin: 16px 0;
}

.ord-actions > form,
.ord-actions > details { margin: 0; }

.ord-actions > form > .btn,
.ord-actions > details > .disclosure > .btn {
    margin: 0;
    min-height: 50px;
}

/* WHAT THE SUPPLIER ACTUALLY RECEIVES. Monospace and scrollable in its
   own box: it is a plain-text mail whose columns only line up in a fixed
   font, and wrapping it would misrepresent what goes out. */
.ord-preview pre {
    overflow-x: auto;
    padding: 12px;
    border: 1px solid var(--border);
    border-radius: var(--radius);
    background: var(--bg-input);
    font-size: 0.8rem;
    line-height: 1.5;
}

/* --- the supplier backend ------------------------------------------- */

/* THE ARTICLE LIST, one line per article.
   It was a form per row carrying four .btn buttons, and .btn is
   display:block, width:100%, margin-top:24px — right for the button
   that ends a form, and four times per row it turned a list of fifty
   into a page you scroll for a minute. */
.sup-items { margin-top: 12px; }

.sup-item {
    display: flex;
    gap: 8px;
    align-items: center;
    /* So the per-article restaurants below can claim a line of their
       own rather than squeezing in beside the buttons. */
    flex-wrap: wrap;
    padding: 6px 0;
    border-bottom: 1px solid var(--border);
}

/* WHICH RESTAURANTS ORDER THIS ARTICLE. A full-width row under the
   article it belongs to, quiet enough to skim past — the answer is "all
   of them" for almost every line, and this screen is read top to bottom
   far more often than it is edited. */
.sup-item-scope {
    flex: 1 0 100%;
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 4px 14px;
    padding: 2px 0 4px 7rem;   /* lines up under the name, past the article number */
    font-size: 0.82rem;
}

.sup-item-scope label {
    display: inline-flex;
    align-items: center;
    gap: 5px;
    cursor: pointer;
}

/* The rest of this screen sits at 44px; these are secondary, sit inside
   a row that is already tall, and are used sitting down at a desk —
   "we will not use this on our mobile version". */
.sup-item-scope input { width: 16px; height: 16px; }

.sup-scope { border: 1px solid var(--border); border-radius: var(--radius); padding: 12px; margin: 0 0 16px; }

.sup-scope legend { padding: 0 6px; font-weight: 600; }

.sup-scope > .muted { margin: 0 0 8px; }

.sup-item-nr   { flex: 0 0 7rem; min-width: 0; }
.sup-item-name { flex: 1 1 auto; min-width: 0; }
.sup-item-unit { flex: 0 0 9rem; }

/* Beating .recipe-form's width:100% on specificity rather than on source
   order — see the note by .time-pick for why that distinction matters. */
.sup-items .sup-item input,
.sup-items .sup-item select {
    width: auto;
    min-height: var(--touch-min);
    padding: 8px 10px;
    background: var(--bg-input);
    border: 1px solid var(--border);
    border-radius: 8px;
    color: var(--text);
    font: inherit;
    font-size: 16px;   /* below 16px, iOS Safari zooms on focus */
}

.sup-item-actions { display: flex; gap: 4px; flex: 0 0 auto; }

/* A SQUARE, NOT A .btn. Same touch target as everything else on a phone,
   none of the block layout or the 24px of air above it. */
.icon-btn {
    display: flex;
    align-items: center;
    justify-content: center;
    width: var(--touch-min);
    height: var(--touch-min);
    flex: 0 0 auto;
    margin: 0;
    padding: 0;
    background: var(--bg-input);
    border: 1px solid var(--border);
    border-radius: 8px;
    color: var(--text-muted);
    font-size: 1rem;
    line-height: 1;
    cursor: pointer;
}

.icon-btn:hover { color: var(--text); border-color: var(--text-muted); }

.sup-item.is-archived input,
.sup-item.is-archived select { opacity: 0.55; }

/* The row wraps rather than overflowing on a narrow screen — this page
   is desktop work, but it must not break if it is opened on a phone. */
@media (max-width: 640px) {
    .sup-item { flex-wrap: wrap; }
    .sup-item-nr   { flex: 0 0 6rem; }
    .sup-item-name { flex: 1 1 100%; order: -1; }
}

/* --- orders awaiting a greenlight (2026-08-26) ---------------------- */

/* AMBER, the same slot the recipe drafts use: not finished, not wrong.
   §8.2 keeps red for urgent and green for confirmed, and an order sitting
   with an admin is neither — it is caution, not alarm. */
.pill-order-pending {
    background: var(--attention-fill);
    border-color: var(--attention-fill);
    color: var(--on-light);
}

/* Declined is not cancelled. Cancelled is somebody changing their own
   mind and is struck through; this one was refused, and stays legible so
   the reason beside it can be read. */
.pill-order-declined {
    background: var(--terracotta);
    border-color: var(--terracotta);
    color: #ffffff;
}

.ord-decline {
    display: flex;
    gap: 6px;
    align-items: center;
    flex-wrap: wrap;
}

.ord-decline input { flex: 1 1 12rem; min-width: 0; }

/* The count beside Bestellungen, matching the one on Anfragen. */
.app-title-alt.has-waiting { opacity: 1; color: var(--terracotta); }

/* A checkbox with a label and an explanation under it. Used by the
   supplier form and by the search filters, which had been carrying the
   class with no rule behind it. */
.check {
    display: flex;
    gap: 10px;
    align-items: flex-start;
    min-height: 44px;
    padding: 6px 0;
    cursor: pointer;
}

.check input { width: 20px; height: 20px; margin-top: 2px; accent-color: var(--moosgruen); }

.check .muted { display: block; font-size: 0.86rem; }

/* --- supplier replies (2026-08-26) ---------------------------------- */

.ord-replies-head, .ord-tray-head { margin-top: 24px; }

/* A CHAT, AS OF 2026-09-03. This was a column of quiet cards, and the
   note explaining why said that dressing an outsider's mail as one of
   our own messages "would blur a line worth keeping visible". The line
   is still worth keeping — but stacking every turn in identical cards
   blurred a different one: which of these is somebody talking to us.
   "below the answer it should feel more like a chat."

   So: sides, not sameness. Ours right, theirs left — the one convention
   every person on the floor already reads fluently — and the outside/
   inside distinction now comes through position and fill rather than
   through a border nobody was decoding. */
.ord-chat {
    display: flex;
    flex-direction: column;
    gap: 10px;
    list-style: none;
    margin: 8px 0 0;
    padding: 0;
}

.ord-msg {
    /* Not the full width: a bubble that reaches both edges has no side,
       and the side is the whole signal here. */
    max-width: 82%;
    padding: 10px 12px;
    border: 1px solid var(--border);
    border-radius: var(--radius);
    background: var(--bg-input);
}

/* THEIRS: left, and edged in Moosgrün as before. */
.ord-msg-theirs {
    align-self: flex-start;
    border-left: 3px solid var(--moosgruen);
    border-top-left-radius: 4px;
}

/* OURS: right, on the page's own ground rather than the input fill, so
   the two still read apart in a screenshot with no colour. */
.ord-msg-ours {
    align-self: flex-end;
    background: var(--bg);
    border-top-right-radius: 4px;
}

.ord-msg-meta { margin: 0 0 4px; font-size: 0.82rem; }

.ord-msg-body { margin: 0; }

.ord-msg-raw { margin-top: 8px; }

.ord-msg-raw summary { font-size: 0.82rem; color: var(--text-muted); cursor: pointer; }

.ord-msg-raw pre {
    overflow-x: auto;
    max-height: 300px;
    margin-top: 6px;
    padding: 10px;
    border-radius: var(--radius);
    background: var(--bg);
    font-size: 0.76rem;
    line-height: 1.5;
}

/* The tray, amber like everything else that is waiting on a person. */
.ord-tray { list-style: none; margin: 8px 0 0; padding: 0; }

.ord-tray-row {
    padding: 10px 12px;
    margin-bottom: 8px;
    border: 1px solid var(--border);
    border-left: 3px solid var(--attention);
    border-radius: var(--radius);
    background: var(--bg-input);
}

.ord-tray-assign {
    display: flex;
    gap: 8px;
    align-items: center;
    flex-wrap: wrap;
    margin-top: 8px;
}

.ord-tray-assign select { flex: 1 1 16rem; min-width: 0; }

/* A reply count beside an order's state, not instead of it. Quiet on
   purpose: it says there is something to read, while the state next to
   it says whether the delivery is still outstanding. */
.pill-reply {
    background: var(--bg-input);
    border-color: var(--moosgruen);
    color: var(--text);
}

/* A bounce is the mail server, not the supplier. Terracotta edge and a
   plain fill — it must never be mistakable for somebody talking, because
   it is the app's only way of saying the order never arrived. */
.ord-msg-bounce { border-left-color: var(--terracotta); }

/* Delayed, not refused: still trying, so amber rather than red. */
.ord-msg-notice { border-left-color: var(--attention); }

/* The server's own words — "550 5.1.1 user unknown". Monospace because
   it is a machine's sentence and paraphrasing it would throw away the
   only clue anybody has. */
.ord-msg-diag {
    font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
    font-size: 0.82rem;
    word-break: break-word;
}

/* --- the supplier backend, made legible (2026-08-27) ---------------- */

/* CENTRED AND CAPPED ON A DESKTOP. This screen is only ever used sitting
   down — "we will not use this on our mobile version" — and a form
   stretched across a 2560px monitor puts the label at one end of the
   desk and the field at the other. */
.admin-nav ~ .wrap,
body:has(.admin-nav) .wrap {
    max-width: 1100px;
    margin-inline: auto;
}

/* FOUR RESTAURANTS SIDE BY SIDE rather than one under another: comparing
   two telephone numbers should not need scrolling. Stacks below 900px
   anyway — a page that cannot be opened on a phone at all is one
   somebody eventually needs on a phone. */
.sup-stores {
    display: grid;
    gap: 12px;
    grid-template-columns: 1fr;
}

@media (min-width: 900px) {
    .sup-stores { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}

@media (min-width: 1400px) {
    .sup-stores { grid-template-columns: repeat(4, minmax(0, 1fr)); }
}

.sup-stores .card { margin: 0; }

/* --- the supplier list ---------------------------------------------- */

/* Its own scroller, so a long description never pushes the page
   sideways — the rule the whole app follows for wide content. */
.sup-table-wrap { overflow-x: auto; }

.sup-table {
    width: 100%;
    border-collapse: collapse;
    margin-top: 12px;
}

.sup-table th,
.sup-table td {
    padding: 10px 12px;
    text-align: left;
    border-bottom: 1px solid var(--border);
    vertical-align: top;
}

.sup-table th {
    font-size: 0.78rem;
    text-transform: uppercase;
    letter-spacing: 0.04em;
    color: var(--text-muted);
    border-bottom-width: 2px;
}

.sup-table tbody tr:hover { background: var(--bg-input); }

.sup-table .num { text-align: right; }

.sup-table td a { font-weight: 600; text-decoration: none; color: var(--text); }
.sup-table td a:hover { text-decoration: underline; }

.sup-table-mail { display: block; font-size: 0.8rem; }

/* The column the list is actually scanned by. Held to a readable
   measure so one chatty entry cannot set the width of the table. */
.sup-table-note { max-width: 34ch; color: var(--text-muted); font-size: 0.9rem; }

.sup-table-flags { white-space: nowrap; }

.sup-table tr.is-archived td { opacity: 0.55; }

/* --- when the delivery should come ---------------------------------- */

.ord-when {
    border: 1px solid var(--border);
    border-radius: var(--radius);
    padding: 4px 12px 12px;
    margin: 0 0 16px;
}

.ord-when-opt {
    display: flex;
    align-items: center;
    gap: 10px;
    flex-wrap: wrap;
    min-height: 48px;
    cursor: pointer;
}

.ord-when-opt + .ord-when-opt { border-top: 1px solid var(--border); }

.ord-when-date {
    /* Room for a full German date without the picker icon crowding it. */
    flex: 0 1 12rem;
    min-height: 44px;
}

/* A note box you can actually write three lines in. */
.ord-cart textarea {
    width: 100%;
    min-height: 6.5rem;
    resize: vertical;
    line-height: 1.5;
}

/* WHILE RECORDING, THE ROW CARRIES LESS.
   Cancel and pause are two more 44px targets, and on a 375px phone that
   pushed Senden off the right-hand edge — breaking the very path the
   buttons were added to improve. Attaching a photo and opening the
   formatting bar are both meaningless with a recording running, so they
   stand aside until it ends. Hidden rather than disabled: a greyed-out
   button still occupies the space that was the problem. */
.composer-row.is-recording .plus,
.composer-row.is-recording .format-toggle {
    display: none;
}

/* --- picking a date and a time (A45) --------------------------------- */

.date-quick {
    display: flex;
    gap: 6px;
    flex-wrap: wrap;
    margin: 6px 0 4px;
}

/* Shortcuts, not the main control — quiet until pressed. */
.chip {
    min-height: 38px;
    padding: 6px 14px;
    border: 1px solid var(--border);
    border-radius: 999px;
    background: var(--bg-input);
    color: var(--text);
    font-size: 0.9rem;
    cursor: pointer;
}

.chip:hover { border-color: var(--text-muted); }

.chip.is-on {
    background: var(--terracotta);
    border-color: var(--terracotta);
    color: #ffffff;
}

/* TWO DROPDOWNS RATHER THAN <input type="time">, which cannot be made to
   show 24 hours: its format follows the browser's locale and no
   attribute overrides it. See the note in kalender-termin.php.
   Sized generously — this is set one-handed in a dim room (§8.4). */
.time-pick {
    display: flex;
    align-items: center;
    gap: 8px;
}

/* TWO NUMBERS DO NOT NEED A COLUMN EACH.
   .recipe-form select sets width:100%, which is right for a category
   list and absurd for "17" — it gave each dropdown the full width of
   the form and pushed the minutes off the right-hand edge (reported
   2026-08-27 with a screenshot).

   Written as .recipe-form .time-pick to beat that rule on specificity
   rather than on source order. Equal-specificity selectors deciding by
   which one happens to come later in the file have caught this project
   out more than once. */
.recipe-form .time-pick select,
.time-pick select {
    width: auto;
    flex: 0 0 auto;
    /* Room for two digits and the browser's own arrow, and no more. */
    min-width: 4.75rem;
    min-height: var(--touch-min);
    padding-right: 8px;
    font-variant-numeric: tabular-nums;
    text-align: center;
}

.time-pick-sep {
    font-weight: 600;
    color: var(--text-muted);
}

/* THE DATE FIELD, MATCHED TO THE REST OF THE FORM.
   It was the only control on the screen the browser was left to draw
   itself — a pale native box among styled ones, which is most of what
   "still looks a bit shitty" was about. It now takes the same
   background, border and radius as every other field, and is capped:
   a date is eight characters and a full-width box for it looks like a
   mistake beside the chips underneath. */
.recipe-form input[type="date"] {
    max-width: 14rem;
    min-height: var(--touch-min);
    font-variant-numeric: tabular-nums;
}

/* The little calendar icon is near-black on a dark field otherwise —
   invisible until you happen to click it. */
:root:not([data-theme="light"]) .recipe-form input[type="date"]::-webkit-calendar-picker-indicator {
    filter: invert(1);
    opacity: 0.75;
    cursor: pointer;
}

@media (prefers-color-scheme: dark) {
    :root:not([data-theme="light"]) .recipe-form input[type="date"]::-webkit-calendar-picker-indicator {
        filter: invert(1);
    }
}

:root[data-theme="dark"] .recipe-form input[type="date"]::-webkit-calendar-picker-indicator {
    filter: invert(1);
}

/* --- a calendar entry shared into a channel -------------------------- */

.event-embed {
    display: flex;
    gap: 12px;
    align-items: center;
    max-width: 400px;
    margin-top: 8px;
    padding: 10px;
    border: 1px solid var(--border);
    border-radius: var(--radius);
    background: var(--bg);
    color: var(--text);
    text-decoration: none;
}

.event-embed:hover { border-color: var(--text-muted); }

/* A torn-off calendar leaf. The date is the thing being shared, so it
   is the thing that reads first — a title above a date makes you look
   twice to answer "when". */
.event-embed-when {
    flex: 0 0 auto;
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    width: 46px;
    height: 46px;
    border-radius: var(--radius);
    background: var(--terracotta);
    color: #ffffff;
    line-height: 1.1;
}

.event-embed-day { font-size: 1.15rem; font-weight: 700; }

.event-embed-month {
    font-size: 0.66rem;
    text-transform: uppercase;
    letter-spacing: 0.04em;
}

.event-embed-body { display: flex; flex-direction: column; gap: 3px; min-width: 0; }

.event-embed-name { font-weight: 600; }

.event-embed-sub { font-size: 0.86rem; }

/* A card for something already past is history, not a reminder. Dimmed
   rather than hidden: the message it sits in is still a real message. */
.event-embed.is-past { opacity: 0.6; }

/* The row that adds a new article, matched to the ones above it. */
.sup-add {
    display: flex;
    gap: 8px;
    align-items: center;
    flex-wrap: wrap;
    margin-top: 12px;
}

.sup-add input,
.sup-add select {
    width: auto;
    min-height: var(--touch-min);
    padding: 8px 10px;
    background: var(--bg-input);
    border: 1px solid var(--border);
    border-radius: 8px;
    color: var(--text);
    font: inherit;
    font-size: 16px;
}

.sup-add input[name="unit_new"] { flex: 0 0 9rem; }

/* The one .btn here should sit on the row, not below it on its own line. */
.sup-add .btn {
    width: auto;
    margin-top: 0;
    flex: 0 0 auto;
}

/* --- inviting people to a calendar entry (A45) ----------------------- */

.cal-invite {
    margin-top: 20px;
    padding: 12px 14px;
    border: 1px solid var(--border);
    border-radius: var(--radius);
    background: var(--bg-input);
}

.cal-invite summary { font-weight: 600; cursor: pointer; }

.cal-invited { list-style: none; margin: 8px 0; padding: 0; }

.cal-invited li { padding: 3px 0; }

.cal-invited .muted { margin-left: 6px; font-size: 0.86rem; }

.cal-invite input[type="email"] {
    width: 100%;
    max-width: 22rem;
    min-height: var(--touch-min);
    margin-top: 6px;
    padding: 8px 10px;
    background: var(--bg);
    border: 1px solid var(--border);
    border-radius: 8px;
    color: var(--text);
    font: inherit;
    font-size: 16px;
}

/* Someone already invited stays ticked and unclickable — they cannot be
   un-invited by clearing a box, and pretending otherwise would be a lie
   about what the button does. */
.cal-invite .tag-check input:disabled + span { opacity: 0.6; }

/* --- pausing or deleting an article (2026-08-27) --------------------- */

/* PAUSED IS SAID IN WORDS, not only in grey. Greying alone reads as
   "disabled" or as a rendering fault, and says nothing at all to a
   reader who cannot tell the two greys apart. Terracotta as a fill with
   white on it — §8.1 — never as thin coloured text. */
.sup-item-paused {
    flex: 0 0 auto;
    padding: 3px 8px;
    border-radius: 999px;
    background: var(--terracotta);
    color: #ffffff;
    font-size: 0.7rem;
    font-weight: 700;
    letter-spacing: 0.05em;
}

.sup-item.is-paused input,
.sup-item.is-paused select { opacity: 0.55; }

/* The ✕ asks rather than acts. A <details> so the choice appears without
   a page load, which matters here: this list is one form and a round
   trip would carry every unsaved edit with it. */
.row-menu { position: relative; flex: 0 0 auto; }

/* THE TRIGGER KEEPS ITS OWN DISPLAY. A <summary> given any display
   other than list-item stops being the disclosure trigger, and the
   panel below it renders open for ever — which is exactly what both of
   these did until it was measured. list-style and the marker pseudo are
   safe: neither changes display. */
.disclosure {
    list-style: none;
    cursor: pointer;
}

.disclosure::-webkit-details-marker { display: none; }
.disclosure::marker { content: ""; }

.row-menu[open] .icon-btn { border-color: var(--terracotta); color: var(--terracotta); }

/* THE DISPLAY IS ONLY SET WHEN IT IS OPEN, and that is the whole trick.
   A browser hides a closed <details>'s children with its own
   `display: none` — and an author rule saying `display: flex` beats the
   UA stylesheet, so the panel simply never hid. Shipped that way and
   caught by checking offsetParent, which stayed DETAILS in both states
   instead of going null.

   So: no `display` in the closed state, and flex only under [open]. */
.row-menu[open] .row-menu-panel { display: flex; }

.row-menu-panel {
    position: absolute;
    right: 0;
    z-index: 20;
    flex-direction: column;
    gap: 6px;
    min-width: 12rem;
    margin-top: 4px;
    padding: 10px;
    border: 1px solid var(--border);
    border-radius: var(--radius);
    background: var(--bg-raised, var(--bg));
    /* The token, not a hex: it is defined once per theme, and a shadow
       tuned for the dark one sits like a bruise under the light one.
       Caught by verify-theme, which is exactly its job. */
    box-shadow: var(--shadow-lg);
}

/* The buttons inside are .btn, which is block and full width — right
   here, for once: a stacked menu is exactly what this is. Only the 24px
   of air above each one has to go. */
.row-menu-panel .btn { margin-top: 0; }

/* The archive at the foot of the supplier list. Two columns, no header:
   it is a shelf, not a working list, and giving it the same furniture as
   the table above would invite it to be read as one. */
.sup-table-archive td { opacity: 0.75; }

.sup-table-archive .btn {
    width: auto;
    margin-top: 0;
    padding: 6px 12px;
    min-height: 38px;
}

/* --- "are you sure" without a dialog box ----------------------------- */

/* The same disclosure the ✕ on an article row uses. Inline rather than a
   modal: nothing to trap focus, nothing to dismiss with a key that may
   not exist on a phone, and it cannot be missed behind another window. */
.confirm { margin-top: 24px; }

/* The trigger's own display is left alone — see .disclosure. The button
   inside it is what carries the styling. */
.confirm > summary .btn { margin-top: 0; }

.confirm[open] > summary .btn { opacity: 0.6; }

.confirm-panel {
    margin-top: 10px;
    padding: 14px;
    border: 1px solid var(--terracotta);
    border-radius: var(--radius);
    background: var(--bg-input);
}

.confirm-panel p { margin: 0 0 8px; }

.confirm-panel .btn { margin-top: 6px; }

/* --- the order thread and writing back (2026-08-27) ------------------ */

/* AN INTERNAL NOTE WENT NOWHERE, and that is the thing to see. It sits
   on our side like everything we wrote, but dashed — which is what "not
   sent" already looks like everywhere else in this app. Without it a
   note about a missing crate reads as the supplier admitting to one. */
.ord-msg-note {
    border: 1px dashed var(--text-muted);
    background: transparent;
}

/* The note the order itself went out with: ours, sent, and the first
   thing in the conversation because it is what the answer answers. */
.ord-msg-note-sent { border-left: 3px solid var(--moosgruen); }

.ord-compose {
    margin-top: 16px;
    padding: 14px;
    border: 1px solid var(--border);
    border-radius: var(--radius);
    background: var(--bg-input);
}

.ord-compose label { display: block; font-weight: 600; margin-bottom: 6px; }

.ord-compose textarea {
    width: 100%;
    min-height: 5.5rem;
    padding: 10px 12px;
    background: var(--bg);
    border: 1px solid var(--border);
    border-radius: 8px;
    color: var(--text);
    font: inherit;
    font-size: 16px;
    line-height: 1.5;
    resize: vertical;
}

/* Two buttons that do very different things sit side by side, so
   neither may be the full-width default. */
.ord-compose-actions {
    display: flex;
    gap: 8px;
    flex-wrap: wrap;
    margin-top: 10px;
}

.ord-compose-actions .btn { width: auto; margin-top: 0; flex: 0 0 auto; }

.ord-compose .muted { margin: 8px 0 0; font-size: 0.86rem; }

/* Partly delivered is amber: something arrived, something did not. Green
   would claim it is finished and red that it failed. */
.pill-order-partial {
    background: var(--attention-fill);
    border-color: var(--attention-fill);
    color: var(--on-light);
}

.ord-partial { margin-top: 0; }
.ord-partial textarea { width: 100%; }

/* THE ORDER POPUP. A card in a channel opens the order here rather than
   sending you to a page and making you find your way back — mid-shift,
   one-handed, in the middle of reading a conversation. */
.ord-karte {
    width: min(34rem, calc(100vw - 24px));
    max-height: min(80vh, 44rem);
    padding: 0;
    background: var(--bg-raised);
    color: var(--text);
    border: 1px solid var(--border);
    border-radius: 16px;
    overflow: hidden;
}

.ord-karte::backdrop { background: var(--scrim); }

/* THE ORDER WENT OUT, said loudly enough to be believed. It was a green
   line above the order and read as one more label on a busy page —
   "it was not clear the order actually went out" (2026-09-04), which
   matters because the cure for that doubt is ordering the same crates a
   second time.

   WRITTEN WITH THE open ATTRIBUTE, so it is on the page before any
   script runs. A plain open dialog is not modal and sits in the flow —
   which is a perfectly good notice — and app.js promotes it to a real
   modal with a backdrop where showModal() exists. Nothing below depends
   on which of the two happened. */
.ord-done {
    width: min(28rem, calc(100vw - 24px));
    margin: 0 auto 20px;
    padding: 20px;
    border: 1px solid var(--moosgruen);
    border-radius: 16px;
    background: var(--bg-raised);
    color: var(--text);
}

.ord-done::backdrop { background: var(--scrim); }

.ord-done h2 { margin: 0 0 6px; color: var(--moosgruen); }

.ord-done p { margin: 0 0 16px; }

/* The two ways onward sit side by side and neither is hidden behind the
   other: checking what you sent and starting the next one are equally
   likely, and a dialog that guesses wrong costs a tap either way. */
.ord-done-actions { display: flex; flex-wrap: wrap; gap: 8px; }

.ord-done-actions form { margin: 0; }

.ord-done-actions .btn { margin: 0; width: auto; flex: 1 1 12rem; }

/* The cart's three exits, on one line where there is room for it. */
.ord-cart-actions {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 8px;
    margin-top: 24px;
}

.ord-cart-actions form { margin: 0; }

.ord-cart-actions .btn { margin: 0; width: auto; flex: 1 1 12rem; }

.ord-karte-body {
    max-height: min(80vh, 44rem);
    padding: 20px 20px 24px;
    overflow-y: auto;
    /* Long orders and long threads scroll; the close button does not
       scroll away with them. */
    overscroll-behavior: contain;
}

/* Not empty while it loads. Two lines of nothing at the size the order
   will be, so the popup does not jump when it arrives. */
.ord-karte-body.is-loading { min-height: 7rem; }

.ord-karte-close {
    position: absolute;
    top: 8px;
    right: 8px;
    z-index: 1;
    width: 40px;
    height: 40px;
    background: var(--bg-raised);
    color: var(--text-muted);
    border: 0;
    border-radius: 50%;
    font-size: 1.6rem;
    line-height: 1;
    cursor: pointer;
}

.ord-karte-close:hover { color: var(--text); }

.ord-karte .ord-detail-head { margin-top: 0; }

.ord-karte-more { margin: 18px 0 0; }

/* Room for the close button, which sits in the same corner. */
.ord-karte-title {
    margin: 0 44px 12px 0;
    font-size: 1.15rem;
}

/* STRAIGHTENING A RECIPE PHOTO. The picture is the screen — everything
   else is a row of buttons under it — because the only question being
   asked here is "does this look right yet". */
.photo-edit .photo-stage {
    position: relative;
    max-width: 40rem;
    margin: 0 auto 14px;
    /* touch-action:none or the browser pans the page instead of letting
       the drag through, which makes cropping impossible on a phone —
       the one device this is for. */
    touch-action: none;
    user-select: none;
}

.photo-edit .photo-stage img {
    display: block;
    width: 100%;
    height: auto;
    border-radius: 10px;
    /* A dark photo on a dark page needs an edge, or the crop box appears
       to float over nothing. */
    background: var(--bg);
}

.photo-crop {
    position: absolute;
    border: 2px solid var(--text);
    /* The shadow does the dimming: everything outside the box goes back
       without a second element to keep in step with the first. */
    box-shadow: 0 0 0 9999px var(--scrim);
    cursor: move;
}

.photo-crop-grip {
    position: absolute;
    /* 22px, not the 8px that looks right on a desktop: this is dragged
       with a thumb, and a handle smaller than the thumb is a handle you
       miss and then re-drag the whole box by accident. */
    width: 22px;
    height: 22px;
    background: var(--text);
    border-radius: 50%;
}

.photo-crop-grip[data-grip="nw"] { top: -11px; left: -11px; }
.photo-crop-grip[data-grip="ne"] { top: -11px; right: -11px; }
.photo-crop-grip[data-grip="sw"] { bottom: -11px; left: -11px; }
.photo-crop-grip[data-grip="se"] { bottom: -11px; right: -11px; }

.photo-actions,
.photo-ratios,
.photo-crop-form {
    display: flex;
    flex-wrap: wrap;
    gap: 10px;
    max-width: 40rem;
    margin: 0 auto 12px;
}

/* The buttons in these rows sit side by side; the global .btn is a
   full-width block, which would stack them one per line. */
.photo-actions .btn,
.photo-ratios .btn,
.photo-crop-form .btn {
    display: inline-block;
    width: auto;
    margin-top: 0;
}

.photo-ratios .btn.is-on {
    background: var(--moosgruen);
    color: var(--bg);
    border-color: var(--moosgruen);
}

.photo-edit .page-help { max-width: 40rem; margin: 0 auto 12px; }

.photo-restore,
.photo-done {
    max-width: 40rem;
    margin: 18px auto 0;
}

/* "Bearbeiten" and "Entfernen" under a gallery photo, side by side —
   they are two answers to the same question. */
.recipe-photo-tools {
    display: flex;
    align-items: center;
    justify-content: center;
    gap: 12px;
}

/* Files hung on a calendar entry. A picture is shown at a size you can
   judge from across a counter; a PDF is a named row, because a menu is
   read by opening it. */
.cal-files {
    list-style: none;
    margin: 0 0 16px;
    padding: 0;
    display: flex;
    flex-wrap: wrap;
    gap: 14px;
}

.cal-files li {
    display: flex;
    flex-direction: column;
    align-items: flex-start;
    gap: 6px;
}

.cal-file-img img {
    display: block;
    width: 160px;
    height: 160px;
    object-fit: cover;
    border-radius: 10px;
    background: var(--bg);
}

/* A document row wants the width; a thumbnail does not. */
.cal-files li:has(.attachment-doc) { flex: 1 1 100%; }

.cal-file-form {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 10px;
    margin-bottom: 16px;
}

.cal-file-form .btn-inline { margin-top: 0; }

/* ---- the header fold-out ------------------------------------------- */

/* WIDE IS THE DEFAULT, and the fold-out is what a narrow screen does.
   Written this way round because the wide layout is the one that has to
   look untouched: on a desktop this is the same bar it has always been,
   and the <details> around it must leave no trace. */
.app-menu {
    display: flex;
    align-items: center;
    flex: 0 0 auto;
}

/* No triangle, no button — on a wide screen this element is not there. */
.app-menu > summary {
    display: none;
    list-style: none;
}

.app-menu > summary::-webkit-details-marker { display: none; }

/* NOT AN OVERRIDE OF A SHUT <details>. That is what this was, and on a
   browser that hides a closed one with something author CSS cannot
   reach, the entire right-hand side of the bar vanished — no Admin, no
   Konto, no calendar (reported 2026-08-28). The element now ships open
   and script closes it on a narrow screen, so this rule only ever
   styles a panel the browser is already showing. */
.app-menu > .app-bar-right { display: flex; }

@media (max-width: 700px) {
    /* Now it really is a menu. */
    .app-menu { position: relative; }

    .app-menu > summary {
        display: flex;
        align-items: center;
        justify-content: center;
        /* The full touch target, because this is now the only way to
           reach Anfragen, Kalender, Suche, Admin and Konto. */
        min-width: var(--touch-min);
        min-height: var(--touch-min);
        border-radius: 8px;
        color: var(--text);
        cursor: pointer;
    }

    .app-menu > summary:active { background: var(--bg-input); }
    .app-menu > summary.has-waiting { color: var(--terracotta); }
    .app-menu > summary .badge { margin-left: 2px; }

    /* Closed: the UA hides it. Open: a card under the button. */
    .app-menu[open] > .app-bar-right {
        position: absolute;
        top: calc(100% + 6px);
        right: 0;
        z-index: 20;
        flex-direction: column;
        align-items: stretch;
        gap: 2px;
        min-width: 11rem;
        padding: 8px;
        background: var(--bg-raised);
        border: 1px solid var(--border);
        border-radius: 12px;
        box-shadow: var(--shadow-lg);
    }

    .app-menu:not([open]) > .app-bar-right { display: none; }

    /* Full-width rows in a menu, not chips in a bar. */
    .app-menu[open] > .app-bar-right > a {
        justify-content: flex-start;
        min-height: var(--touch-min);
        padding: 0 12px;
        max-width: none;
        font-size: 0.95rem;
    }

    /* The two icons had no label while they sat in the bar; in a list of
       words a bare glyph reads as a gap. */
    .app-menu[open] .app-search::after {
        content: attr(aria-label);
        margin-left: 10px;
        font-size: 0.95rem;
        font-weight: 600;
    }

    /* WHAT THE FOLD IS FOR: the two places keep their whole name. */
    .app-title { font-size: 1rem; }
}

/* ---- Inventar ------------------------------------------------------- */

/* The rooms, as a row of tabs. "All articles" is one of them rather than
   a separate screen: what do we have, and what is in Westend, are the
   same question asked with and without a filter. */
.inv-places {
    display: flex;
    flex-wrap: wrap;
    gap: 6px;
    margin: 0 0 18px;
}

.inv-place {
    display: flex;
    align-items: center;
    min-height: var(--touch-min);
    padding: 0 14px;
    background: var(--bg-raised);
    border: 1px solid var(--border);
    border-radius: 999px;
    color: var(--text);
    font-size: 0.9rem;
    text-decoration: none;
}

.inv-place.is-on {
    background: var(--moosgruen);
    border-color: var(--moosgruen);
    color: var(--text);
}

/* THE ROW UNDER THE PLACES (2026-09-28): the doors you can go through
   from here — Lieferungen, Produktion, the station. Smaller and quieter
   than the places above, because "where am I" comes before "what can I
   do", and a manager has no doors at all so the row is not drawn.
   Verwalten sits at the end of the places row, dashed: a way out to
   Admin, not a place. */
.inv-place-quiet { color: var(--text-muted); border-style: dashed; }
.inv-actions { display: flex; flex-wrap: wrap; gap: 6px; margin: -10px 0 18px; }
.inv-action {
    display: inline-flex;
    align-items: center;
    min-height: 36px;
    padding: 0 12px;
    border: 1px solid var(--border);
    border-radius: 999px;
    color: var(--text-muted);
    font-size: 0.85rem;
    text-decoration: none;
}
.inv-action:hover { color: var(--text); }

.inv-area-head {
    margin: 18px 0 6px;
    font-size: 0.78rem;
    letter-spacing: 0.1em;
    text-transform: uppercase;
    color: var(--text-muted);
}

/* An area as a fold (2026-09-15): the row is the whole tap target, the
   arrow says it opens, the number says how much is inside. */
.inv-area-fold { border-bottom: 1px solid var(--border); }
summary.inv-area-head {
    display: flex;
    align-items: baseline;
    gap: 10px;
    margin: 0;
    padding: 14px 4px 10px;
    font-size: 0.85rem;
    color: var(--text);
    cursor: pointer;
    list-style: none;
}
summary.inv-area-head::-webkit-details-marker { display: none; }
.inv-area-name { font-weight: 600; }
.inv-area-name::before { content: "▸ "; color: var(--text-muted); }
.inv-area-fold[open] .inv-area-name::before { content: "▾ "; }
.inv-area-count { margin-left: auto; color: var(--text-muted); font-variant-numeric: tabular-nums; letter-spacing: 0; }
.inv-area-fold > .inv-walk { margin-bottom: 10px; }

/* The walk. Deliberately dense: it is read as a list of names in an
   order, and generous spacing would put half of a cold room off the
   bottom of a phone. */
.inv-walk,
.inv-areas,
.inv-places-list,
.inv-codes {
    list-style: none;
    margin: 0 0 14px;
    padding: 0;
    display: flex;
    flex-direction: column;
    gap: 2px;
}

.inv-walk li,
.inv-places-list li,
.inv-codes li {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 8px;
    padding: 6px 10px;
    background: var(--bg-raised);
    border: 1px solid var(--border);
    border-radius: 8px;
}

.inv-walk-name { flex: 1 1 12rem; }
.inv-walk-name em { margin-left: 8px; font-size: 0.82rem; font-style: normal; }

.inv-walk-tools {
    display: flex;
    align-items: center;
    gap: 6px;
}

.inv-walk-tools form { display: inline-flex; }

/* Every one of these rows is a little form on a line. */
.inv-area-row,
.inv-area-add,
.inv-code-add {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 8px;
    width: 100%;
    margin-bottom: 8px;
}

.inv-area-row input[type="text"],
.inv-area-add input[type="text"],
.inv-code-add input[type="text"] {
    flex: 1 1 10rem;
    min-width: 0;
    min-height: var(--touch-min);
    padding: 6px 10px;
    background: var(--bg-input);
    color: var(--text);
    border: 1px solid var(--border);
    border-radius: 8px;
    font: inherit;
}

.inv-code-add input[type="number"] {
    width: 5.5rem;
    min-height: var(--touch-min);
    padding: 6px 10px;
    background: var(--bg-input);
    color: var(--text);
    border: 1px solid var(--border);
    border-radius: 8px;
    font: inherit;
}

/* The global select rule is full-width, which would stack every one of
   these onto its own line. */
.inv-area-row select,
.inv-area-add select {
    flex: 0 1 12rem;
    width: auto;
    min-height: var(--touch-min);
}

.inv-area-row .btn-inline,
.inv-area-add .btn-inline,
.inv-code-add .btn-inline { margin-top: 0; }

.inv-place-name { flex: 1 1 8rem; font-weight: 600; }

.inv-codes li .mono {
    font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
    font-size: 0.88rem;
}

.inv-archive { margin-top: 24px; }

/* An article can be on several shelves in one building, so a location's
   row holds a little stack of them rather than a single line. */
.inv-places-list li { align-items: flex-start; }

.inv-place-shelves {
    flex: 1 1 16rem;
    display: flex;
    flex-direction: column;
    gap: 4px;
    min-width: 0;
}

.inv-place-shelves .inv-area-row { margin-bottom: 0; }

/* The tools on a walk row. Real buttons rather than bare text links: as
   link-buttons they wrapped onto their own lines and read as prose
   instead of as things you press. */
.inv-btn {
    display: inline-flex;
    align-items: center;
    white-space: nowrap;
    min-height: 34px;
    padding: 0 12px;
    background: var(--bg-input);
    border: 1px solid var(--border);
    border-radius: 999px;
    color: var(--text);
    font-size: 0.82rem;
    font-weight: 600;
    text-decoration: none;
    cursor: pointer;
}

.inv-btn:hover { border-color: var(--text-muted); }

/* Terracotta is what removal looks like everywhere else in the app —
   outlined rather than filled, because it is one row leaving a shelf,
   not the article being destroyed. */
.inv-btn-drop {
    background: transparent;
    border-color: var(--terracotta);
    color: var(--terracotta);
}

.inv-btn-drop:hover { background: var(--terracotta); color: var(--bg); }

/* Keeps the tools on one line and lets the name give way first. */
.inv-walk-tools { flex: 0 0 auto; flex-wrap: nowrap; }

.inv-save { max-width: 40rem; margin-top: 28px; }

/* Says why a form filled itself in, and offers the way out. Sits above
   the form rather than inside it, so it is not mistaken for a field. */
.draft-restored {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 8px;
    max-width: 40rem;
}

.draft-restored .link-button { min-height: auto; }

/* ---- where an article is kept --------------------------------------- */

/* MORE ROOM, AND EVERY ROW THE SAME SHAPE (2026-09-01).
   Each shelf row was a flex line whose select sized itself, so the
   buttons landed at a different x in every card and one of them wrapped
   onto its own line. A grid gives the three parts fixed proportions, so
   the selects line up down the page and the buttons sit in a column
   whatever the location is called. */
.inv-places-list {
    width: min(58rem, 100vw - 2rem);
    margin-left: calc(50% - min(58rem, 100vw - 2rem) / 2);
}

/* A FIXED FIRST TRACK, not minmax. Each card is its own grid, so a
   flexible column resolves to a different width in every one of them —
   which is precisely how "Aufnehmen" ended up at a different x under
   every location name. A fixed width is the only thing that lines up
   across separate grids. */
.inv-places-list li {
    display: grid;
    grid-template-columns: 13rem minmax(0, 1fr);
    gap: 10px 16px;
    align-items: start;
    padding: 12px 14px;
}

.inv-place-name {
    flex: none;
    padding-top: 8px;
}

.inv-place-shelves { flex: none; }

/* select | button | button, and the select is a FIXED width for the same
   reason the column above is: a row with two buttons and a row with one
   would otherwise give their selects different widths, and the eye
   reads that as sloppiness rather than as a difference in meaning. */
.inv-place-shelves .inv-area-row {
    display: grid;
    grid-template-columns: 15rem auto auto;
    justify-content: start;
    gap: 8px;
    align-items: center;
}

.inv-place-shelves .inv-area-row select {
    flex: none;
    width: 100%;
    min-width: 0;
}

@media (max-width: 44rem) {
    .inv-places-list {
        width: auto;
        margin-left: 0;
    }

    .inv-places-list li { grid-template-columns: 1fr; }
    .inv-place-name { padding-top: 0; }
}

/* The article's number. Tabular figures so a column of them lines up,
   and quiet — it is how you name the thing out loud, not the thing
   itself. */
.inv-nr {
    font-variant-numeric: tabular-nums;
    color: var(--text-muted);
    font-size: 0.85rem;
}

/* In the walk it leads the row, at a fixed width so the names start on
   one line down the page rather than stepping in and out. */
.inv-walk li .inv-nr {
    flex: 0 0 2.6rem;
    text-align: right;
}

.inv-nr-line { margin-bottom: 14px; }

/* ---- the catalogue table -------------------------------------------- */

/* THE CATALOGUE OUTGREW THE COLUMN IT SITS IN. Seven columns of article
   data inside a 44rem page scrolled sideways on a wide screen with room
   to spare (2026-09-01). The table alone breaks out; the form below it
   keeps a sane field width, which is what the 44rem was for.

   IT USED TO BREAK OUT OF THE COLUMN, and that is what went wrong. The
   rule set a width and a centring margin under one class name, while the
   element also carries `.table-scroll` — declared 250 lines LOWER with a
   `margin` shorthand. Equal specificity, later wins: the width applied
   and the centring did not, so on the kitchen's iPad the table started
   at the page's left edge, left 254px of nothing beside it and hung
   206px off the right ("lots of space on the left and then one has to
   scroll", 2026-09-09).

   NOW THE PAGE IS WIDE AND THE TABLE SIMPLY FILLS IT — see .page-wide.
   One element sticking out of a narrow column was always going to look
   like a mistake; the honest fix was to admit the whole screen is a
   table and not a text. What is left here is a plain 100% with a
   max-width, which cannot be knocked over by rule order because there is
   nothing clever left to knock over. */
@media (min-width: 62rem) {
    .table-scroll.inv-table-wide {
        width: 100%;
        max-width: calc(100vw - 3rem);
        margin-left: 0;
        margin-right: 0;
    }
}

/* A sortable header. The arrow says both WHICH column is sorting and
   which way — an arrow on every header says neither. */
.inv-sort {
    color: inherit;
    text-decoration: none;
    white-space: nowrap;
}

.inv-sort:hover { color: var(--text); }
.inv-sort.is-on { color: var(--moosgruen); }

/* WHOSE LABEL IS ON IT, at a glance.
   A moss RIM was the first attempt and the palette had already ruled it
   out in writing: "never as a border or text — at 1.45:1 a thin green
   line on the dark background cannot be seen". Measured here at 1.57:1
   against a card, which is the same answer.
   So Moosgrün does what this palette says it is for — a fill with white
   lettering — and only for OURS. Bought-in goods keep the plain pill:
   marking the exception is quieter than marking both, and it is the
   exception people are scanning the column for. */
.pill-track-container {
    background: var(--moosgruen);
    border: 1px solid var(--moosgruen);
    color: #ffffff;
}

.pill-track-quantity {
    background: transparent;
    border: 1px solid var(--border);
    color: var(--text-muted);
}

/* ---- production runs and one bag ------------------------------------ */

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

.inv-runs li {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 10px;
    padding: 8px 12px;
    background: var(--bg-raised);
    border: 1px solid var(--border);
    border-radius: 8px;
}

.inv-run-what { flex: 1 1 12rem; }
.inv-run-what em { margin-left: 8px; font-style: normal; font-size: 0.9rem; }

.inv-bag-facts,
.inv-history {
    list-style: none;
    margin: 0 0 16px;
    padding: 0;
    display: flex;
    flex-direction: column;
    gap: 2px;
}

.inv-bag-facts li {
    display: grid;
    grid-template-columns: 9rem minmax(0, 1fr);
    gap: 10px;
    align-items: center;
    padding: 8px 12px;
    background: var(--bg-raised);
    border: 1px solid var(--border);
    border-radius: 8px;
}

.inv-history li {
    display: flex;
    flex-wrap: wrap;
    gap: 10px;
    padding: 6px 12px;
    border-left: 2px solid var(--border);
}

/* Frozen and thawed are the two worth spotting; fresh is the ordinary
   case and stays plain. Fills, never rims — the palette's own rule. */
.pill-cond-frozen { background: var(--moosgruen); border-color: var(--moosgruen); color: #ffffff; }
.pill-cond-thawed { background: var(--attention-fill); border-color: var(--attention-fill); color: var(--on-light); }

/* The freezer tick sits on the area's own row, beside its name. */
.inv-freezer {
    display: inline-flex;
    align-items: center;
    gap: 6px;
    white-space: nowrap;
    font-size: 0.85rem;
    color: var(--text-muted);
}

.inv-freezer input { width: auto; min-height: 0; margin: 0; }

/* Freeze and thaw, on the page a camera lands on. Full width because a
   thumb finds them with the bag in the other hand. */
.inv-bag-actions {
    display: flex;
    flex-wrap: wrap;
    gap: 10px;
    margin: 0 0 20px;
}

.inv-bag-actions form { flex: 1 1 14rem; }
.inv-bag-actions .btn { margin-top: 0; }
.inv-runs form { display: inline-flex; }

/* -------------------------------------------------------------------
   VORBEREITUNG — the Aufbau team's morning list, and the grid behind it.
   ------------------------------------------------------------------- */

/* The card at the top of the home screen. One line, one tap, and only
   for the people who actually prep. */
.prep-card { margin: 0 0 16px; }

.prep-card-link {
    display: flex;
    align-items: baseline;
    justify-content: space-between;
    gap: 12px;
    padding: 14px 16px;
    border: 1px solid var(--border);
    border-radius: var(--radius);
    background: var(--bg-raised);
    color: var(--text);
    text-decoration: none;
}

.prep-card-link:hover { border-color: var(--text-muted); }
.prep-card-title { font-weight: 700; }
.prep-card-count { color: var(--text-muted); font-size: 0.88rem; }

/* The day, above the list. Small — it confirms what you already know
   rather than announcing it. */


/* THE NUMBER FIRST, IN A FIXED COLUMN. Somebody reads this standing up
   with one hand: the amounts line up down the left edge so the whole
   list is one glance rather than twenty-four separate readings. */
.prep-list { list-style: none; margin: 0 0 20px; padding: 0; }

.prep-list li {
    display: flex;
    align-items: baseline;
    gap: 14px;
    padding: 11px 4px;
    border-bottom: 1px solid var(--border);
}

.prep-list li:last-child { border-bottom: 0; }

.prep-amount {
    flex: 0 0 auto;
    min-width: 3.2em;
    font-size: 1.25rem;
    font-weight: 700;
    font-variant-numeric: tabular-nums;
}

.prep-name-cell { flex: 1 1 auto; font-size: 1.05rem; }
.prep-name-cell em { display: block; font-size: 0.78rem; font-style: normal; }

.prep-week { margin: 0 0 24px; }
.prep-week summary { cursor: pointer; padding: 8px 0; font-size: 0.88rem; }

/* The houses, as a row of buttons in the admin screen. */
.prep-houses { display: flex; flex-wrap: wrap; gap: 8px; margin: 0 0 16px; }
.prep-houses .is-on { border-color: var(--text); font-weight: 700; }

.prep-switch { margin: 0 0 16px; }
.prep-switch p { margin: 0 0 6px; }

/* WIDER THAN A PHONE, SO IT SCROLLS ITSELF. Five day columns plus a name
   and a note will not fit, and a page that slides sideways under your
   thumb while you read is worse than a table that does. */
.table-scroll { overflow-x: auto; margin: 0 0 16px; }

.prep-grid { border-collapse: collapse; width: 100%; }

.prep-grid th,
.prep-grid td {
    padding: 4px 6px;
    border-bottom: 1px solid var(--border);
    text-align: left;
    vertical-align: middle;
}

.prep-grid thead th {
    font-size: 0.76rem;
    text-transform: uppercase;
    letter-spacing: 0.05em;
    color: var(--text-muted);
    white-space: nowrap;
}

.prep-grid input {
    width: 100%;
    min-height: 38px;
    padding: 0 8px;
    background: var(--bg-input);
    border: 1px solid var(--border);
    border-radius: 8px;
    color: var(--text);
    font: inherit;
}

.prep-grid input:focus { border-color: var(--text-muted); outline: none; }

.prep-name { min-width: 11em; font-weight: 600; }
.prep-cell { max-width: 5.5em; text-align: center; font-variant-numeric: tabular-nums; }
.prep-note { min-width: 10em; }

/* The read-only week under the morning list. */
.prep-grid-read td { font-variant-numeric: tabular-nums; }
.prep-grid-read th[scope="row"] { font-weight: 600; white-space: nowrap; }
.prep-grid-read th[scope="row"] em { display: block; font-size: 0.74rem; font-style: normal; }

/* Today's column, marked with a fill rather than a border — §8.1, and
   the same reason the label rim was changed: a thin line on this
   background is not something anybody sees. */
.prep-grid-read .is-today {
    background: color-mix(in srgb, var(--text-muted) 14%, transparent);
    font-weight: 700;
}

/* Checking the screen as somebody else sees it. A control, not content:
   it sits above the list and stays quiet. */
.prep-preview-pick {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 8px;
    margin: 0 0 16px;
    font-size: 0.85rem;
}

.prep-preview-pick select {
    min-height: 34px;
    padding: 0 8px;
    background: var(--bg-input);
    border: 1px solid var(--border);
    border-radius: 8px;
    color: var(--text);
    font: inherit;
}

/* WARM AND KALT, each able to fold away. Open by default: somebody who
   arrives to two closed boxes has to tap before they can read anything,
   every morning, to get back to what they were already being shown. */
.prep-section { margin: 0 0 14px; }

.prep-section > summary {
    display: flex;
    align-items: baseline;
    justify-content: space-between;
    gap: 10px;
    padding: 9px 4px;
    border-bottom: 1px solid color-mix(in srgb, var(--text-muted) 40%, transparent);
    cursor: pointer;
    list-style: none;
}

.prep-section > summary::-webkit-details-marker { display: none; }

.prep-section-name {
    font-size: 0.78rem;
    font-weight: 700;
    text-transform: uppercase;
    letter-spacing: 0.08em;
}

/* A marker of its own, since the native triangle is hidden: it has to be
   obvious that this opens and shuts. */
.prep-section > summary .prep-section-name::before {
    content: '▾';
    display: inline-block;
    width: 1.1em;
    color: var(--text-muted);
}

.prep-section:not([open]) > summary .prep-section-name::before { content: '▸'; }

/* The same division in the week table. */
.prep-grid-break th {
    padding-top: 14px;
    font-size: 0.72rem;
    font-weight: 700;
    text-transform: uppercase;
    letter-spacing: 0.08em;
    color: var(--text-muted);
    border-bottom: 1px solid color-mix(in srgb, var(--text-muted) 40%, transparent);
}

.prep-section-pick {
    min-height: 38px;
    padding: 0 6px;
    background: var(--bg-input);
    border: 1px solid var(--border);
    border-radius: 8px;
    color: var(--text);
    font: inherit;
    font-size: 0.82rem;
}

/* -------------------------------------------------------------------
   The morning list, after 2026-09-02: name then number, one left edge.
   ------------------------------------------------------------------- */

/* TODAY, AND THE DATE THAT PROVES IT. A fill rather than coloured
   lettering: §8.1 gives terracotta as a fill with white on it, and as
   small text on the dark ground it measures about 3.5:1 — under the
   4.5:1 this size needs. White on the fill is 4.9:1.

   Sentence case, not uppercase with tracking. The old label was set in
   spaced capitals and read as slightly crooked next to the list. */
.prep-today {
    display: inline-block;
    /* Room above as well as below: with a notice or a house name over
       it the pill was sitting flush against them. */
    margin: 4px 0 12px 4px;
    padding: 5px 12px;
    border-radius: 999px;
    background: var(--terracotta);
    color: #ffffff;
    font-size: 0.8rem;
    font-weight: 700;
}

/* ONE LEFT EDGE FOR EVERYTHING. The rows and the section headings carry
   4px of inset; the day and the house name did not, so they sat four
   pixels proud of the list — which is exactly the "skewed" look. */
.prep-house { padding-left: 4px; }

/* NAME FIRST, AMOUNT AGAINST THE RIGHT EDGE. "Barbacoa 3" is the order
   somebody says it in; the number keeps a column of its own so the
   amounts still line up to be read in one pass. */
.prep-list li > .prep-amount {
    margin-left: auto;
    min-width: 0;
    padding-left: 12px;
    text-align: right;
}

/* -------------------------------------------------------------------
   A week planned in advance: holidays, and anything else already known.
   ------------------------------------------------------------------- */

/* THE STANDING AMOUNT AS A PLACEHOLDER, so an empty box reads as "this
   is what will happen" rather than "nothing". Anything typed over it is
   the exception, and shows as one. */
.prep-cell.is-planned {
    border-color: var(--terracotta);
    font-weight: 700;
}

.prep-cell::placeholder { color: var(--text-muted); opacity: 0.7; }

.prep-plan-name { min-width: 10em; font-weight: 600; white-space: nowrap; }

.prep-plan-date {
    display: block;
    font-style: normal;
    font-weight: 400;
    opacity: 0.75;
}

.prep-plan-reset { margin: 0 0 24px; }

/* Said on the morning list, only on the days it is true. Somebody who
   knows Wednesday by heart will not notice one number moving. */
.prep-planned-note {
    margin: 0 0 12px 4px;
    padding-left: 10px;
    border-left: 3px solid var(--terracotta);
    color: var(--text);
    font-size: 0.85rem;
}

/* A whole day closed, in one click. */
.prep-day-btn {
    display: block;
    margin-top: 5px;
    padding: 3px 8px;
    background: var(--bg-input);
    border: 1px solid var(--border);
    border-radius: 999px;
    color: var(--text-muted);
    font: inherit;
    font-size: 0.68rem;
    font-weight: 600;
    text-transform: none;
    letter-spacing: 0;
    white-space: nowrap;
    cursor: pointer;
}

.prep-day-btn:hover { border-color: var(--text-muted); color: var(--text); }

/* The fill, so a shut day is obvious across the whole column header
   rather than a word somebody has to read. */
.prep-day-btn.is-on {
    background: var(--terracotta);
    border-color: var(--terracotta);
    color: #ffffff;
}

.prep-grid th.is-closed { color: var(--text); }

/* Said once, on the morning list, instead of a column of noughts. */
.prep-closed {
    margin: 0 0 16px 4px;
    padding: 12px 14px;
    border-radius: var(--radius);
    background: var(--terracotta);
    color: #ffffff;
    font-weight: 700;
}

/* A day of this week that is not the standard amount. */
.prep-grid-read td.is-planned-cell {
    color: var(--text);
    font-weight: 700;
    text-decoration: underline;
    text-decoration-color: var(--terracotta);
    text-underline-offset: 3px;
}

/* -------------------------------------------------------------------
   WHO WROTE THIS: the initials circle, and runs of messages as blocks.

   Reported 2026-09-02 by the team: the channels are hard to take in.
   Four messages from one person were four identical cards with the name
   repeated on each, and nothing on the row for the eye to hold.

   EIGHT COLOURS, and this is a deliberate extension of §8.1 — approved
   on the day rather than assumed. They are fills carrying white text,
   which is the rule the palette already states; every one measures at
   least 5.35:1 with white on it, so the letters read on any of them.
   Muted on purpose: they sit beside terracotta and moosgrün without
   arguing with either, and a channel of twenty-seven people should not
   look like a box of highlighters.

   BY USER ID, NOT BY NAME — see Web::avatarColour. A colour somebody
   has learned to recognise must not move because a name was corrected.
   ------------------------------------------------------------------- */

:root {
    --avatar-1: #975b4f;   /* terracotta, the house colour  5.35:1 */
    --avatar-2: #1f5d4c;   /* deep green                    7.70:1 */
    --avatar-3: #5a4a86;   /* violet                        7.61:1 */
    --avatar-4: #3b5a7a;   /* slate blue                    7.17:1 */
    --avatar-5: #7a3f5c;   /* plum                          7.80:1 */
    --avatar-6: #7d5a24;   /* ochre                         6.25:1 */
    --avatar-7: #1f5a5e;   /* teal                          7.83:1 */
    --avatar-8: #6b4a3a;   /* clay brown                    7.88:1 */
}

/* CENTRED ON THE NAME, not hung from the top of the row.
   Reported 2026-09-02: "the positioning looks a little bit off". It was
   34px top-aligned against a name line 20px tall, so it hung thirteen
   pixels below the name and nine into the first line of the message —
   attached to neither, which is exactly what it looked like.
   28px, lifted 4px, puts its middle on the name's middle and its bottom
   where the text begins. Measured, not guessed. */
.avatar {
    flex: 0 0 auto;
    display: flex;
    align-items: center;
    justify-content: center;
    width: 28px;
    height: 28px;
    margin-top: -4px;
    border-radius: 50%;
    color: #ffffff;
    font-size: 0.7rem;
    font-weight: 700;
    letter-spacing: 0.02em;
    user-select: none;
}

.avatar-1 { background: var(--avatar-1); }
.avatar-2 { background: var(--avatar-2); }
.avatar-3 { background: var(--avatar-3); }
.avatar-4 { background: var(--avatar-4); }
.avatar-5 { background: var(--avatar-5); }
.avatar-6 { background: var(--avatar-6); }
.avatar-7 { background: var(--avatar-7); }
.avatar-8 { background: var(--avatar-8); }

/* The gap a continuation leaves, so its text still lines up under the
   text above rather than sliding back to the left edge. */
.avatar-empty { background: none; }

/* THE ROW: circle on the left, everything else in one column beside it.
   min-width:0 or a pasted link stops the column shrinking and pushes
   the page sideways. */
.message {
    display: flex;
    align-items: flex-start;
    gap: 10px;
}

.message-main { flex: 1 1 auto; min-width: 0; }

/* ONE CARD FOR THE WHOLE RUN, not a stack of boxes with the borders
   turned off. The first attempt only hid the border and left the
   background and a 4px gap, so the messages still read as separate
   tiles — measured on the phone before this was fixed.

   The opener loses its bottom edge and the continuation its top, and
   the margin between them goes: what is left is one card with several
   paragraphs in it, which is what a run of messages is. */
.message:has(+ .message-continues) {
    margin-bottom: 0;
    padding-bottom: 6px;
    border-bottom: 0;
    border-bottom-left-radius: 0;
    border-bottom-right-radius: 0;
}

.message-continues {
    margin-top: 0;
    padding-top: 2px;
    border-top: 0;
    border-top-left-radius: 0;
    border-top-right-radius: 0;
    box-shadow: none;
}

/* A run of three: the middle one has neither edge. */
.message-continues:has(+ .message-continues) {
    margin-bottom: 0;
    padding-bottom: 6px;
    border-bottom: 0;
    border-bottom-left-radius: 0;
    border-bottom-right-radius: 0;
}

/* The time on a continuation: present for anybody who looks, silent
   otherwise. */
.message-time-quiet {
    display: block;
    margin-bottom: 2px;
    color: var(--text-muted);
    font-size: 0.72rem;
}

/* The green edge of your own messages still belongs to the whole run,
   so a continuation of yours keeps it. */
.message-continues.message-mine { border-left-color: var(--moosgruen); }
.message-continues.message-mentions-me { border-left-color: var(--terracotta); }

/* -------------------------------------------------------------------
   THE CHANNEL DRAWER. Asked for 2026-09-02: a way to fold the channels
   away and reach the conversations without scrolling past seventeen of
   them.

   The same shape as the conversations' drawer below it, deliberately —
   two drawers on one screen that opened differently would be two things
   to learn. It shares the summary styling; only the arrow and the
   heading spacing need saying twice.
   ------------------------------------------------------------------- */

.channel-foldout > summary {
    display: flex;
    align-items: center;
    gap: 8px;
    min-height: var(--touch-min);
    padding: 0 4px;
    margin-top: 22px;
    color: var(--text-muted);
    font-size: 0.78rem;
    font-weight: 600;
    letter-spacing: 0.08em;
    text-transform: uppercase;
    cursor: pointer;
    list-style: none;
}

.channel-foldout > summary::-webkit-details-marker { display: none; }

.channel-foldout > summary::before {
    content: '';
    width: 0;
    height: 0;
    border-left: 5px solid currentColor;
    border-top: 4px solid transparent;
    border-bottom: 4px solid transparent;
    transition: transform 120ms ease;
}

.channel-foldout[open] > summary::before { transform: rotate(90deg); }

@media (prefers-reduced-motion: reduce) {
    .channel-foldout > summary::before { transition: none; }
}

.channel-foldout > summary:hover { color: var(--text); }

/* The heading this replaces carried the spacing; without it the first
   channel sits against the summary. */
.channel-foldout .channel-list { margin-top: 4px; }

/* -------------------------------------------------------------------
   THE CONVERSATIONS ABOVE THE CHANNELS, WHEN SOMETHING IS WAITING.

   Asked for 2026-09-02: "as we have many channels one has to scroll
   down a lot to see if or not we have an unread DM". Seventeen channels
   is a long way to look for a thing that might not be there.

   ONE RULE, ONE MOVING PART. The conversations have exactly two
   positions, and which one they are in is decided by a single fact the
   reader already knows: whether anybody has written to them. Nothing
   unread, nothing moves — the screen stays the one they learned. This
   is deliberately not the full re-ranking that was discussed, which
   would have made the top of the page a different kind of thing from
   one hour to the next.

   A wrapper around exactly the two lists, not the whole page: making
   <main> a flex column would stop every margin above here collapsing
   and shift the calendar and the shift requests by a few pixels for no
   reason anybody asked for.
   ------------------------------------------------------------------- */

/* THE RAIL IS NOT IN THIS SELECTOR ANY MORE. It was, and because this
   sits at top level while "the rail is hidden on a phone" sits a
   thousand lines above it, the display:flex won — so every page that
   draws the rail put the entire channel list above its own content on a
   phone. 600px of it, before the members you tapped for.

   Reported 2026-09-03 on Mitglieder & Info and on pages, and not on a
   channel, which is the clue that dates it: a channel scrolls itself to
   the newest message on open, so nobody ever saw the top of it. The
   rail's own column lives in the desktop block where the rail does. */
.home-lists { display: flex; flex-direction: column; }

/* The one line that does the work. */
.dm-first { order: -1; }

/* Flex does not collapse margins, and the drawers were relying on that
   to sit 22px apart rather than 44px. Stated once here so the spacing
   is the same whichever order they end up in. */
.home-lists > details,
.channel-rail > details { margin-top: 0; }

.home-lists > .dm-foldout,
.home-lists > .channel-foldout > summary,
.channel-rail > .rail-dms,
.channel-rail > .rail-channels > summary { margin-top: 18px; }

/* Same on the home screen: the separator hangs off the conversations,
   so when they move above the channels it has to move with them. */
.dm-foldout.dm-first { border-top: 0; padding-top: 0; margin-top: 18px; }

.dm-foldout.dm-first ~ .channel-foldout > summary {
    border-top: 1px solid var(--border);
    padding-top: 12px;
}

/* "NEU" — everything unread, whatever kind it is, at the top of the
   home screen. Asked for 2026-09-03.

   No drawer on this one. It is the group somebody opened the app to
   see, and a fold on it would be a tap between them and the thing they
   came for. It simply is not there when there is nothing in it. */
.new-group { margin: 0 0 18px; }
.new-group .channel-list { margin-top: 4px; }

/* The three new questions on a to-do — which restaurant, who, and who
   else can see it. Boxed so they read as three decisions rather than a
   run of loose controls, and matching .ord-ask, which asks the same
   shape of question when starting an order. */
.work-scope {
    border: 1px solid var(--border);
    border-radius: var(--radius);
    padding: 12px;
    margin: 0 0 16px;
}

.work-scope legend { padding: 0 6px; font-weight: 600; }

.work-scope > .muted { margin: 0 0 8px; font-size: 0.85rem; }

/* THE RARELY-USED FILTERS, FOLDED. Two bare date boxes and a loose
   checkbox under the tabs read as debris — reported 2026-09-08 with a
   screenshot. The restaurant and the status answer most questions on
   their own, so the rest waits behind a summary and opens itself when
   any of it is on. */
.work-filter-fold { margin-top: 12px; }

.work-filter-fold > summary {
    display: flex;
    align-items: center;
    gap: 8px;
    min-height: var(--touch-min);
    color: var(--text-muted);
    cursor: pointer;
    font-size: 0.9rem;
}

.work-filter-fold[open] > summary { color: var(--text); }

.work-filter select {
    padding: 8px 10px;
    border: 1px solid var(--border);
    border-radius: 8px;
    background: var(--bg-input);
    color: var(--text);
    font: inherit;
}

/* Editing sits under the row rather than inside the link, so the row
   itself still does the obvious thing: open the task and tick it. */
.work-row-edit {
    display: inline-block;
    padding: 4px 4px 8px;
    color: var(--text-muted);
    font-size: 0.85rem;
}

/* EDIT AND DELETE ON ONE LINE, and on a desktop on the row itself
   rather than under it — asked for 2026-09-08. On a phone they stay
   below, where a 44px target beside a title would crowd both. */
.work-row-tools {
    display: flex;
    align-items: center;
    gap: 6px;
    padding: 2px 4px 8px;
}

/* .btn is display:block and full-width in this app — most buttons are
   the only thing on their line — so a pair on a row has to be told
   otherwise. The quiet look itself is settled at .btn-small.btn-quiet,
   where the reason is written down. */
.work-row-tools .btn {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    margin: 0;
    width: auto;
}

.work-row-del { margin: 0; }

.work-row-del > summary { list-style: none; }

.work-row-del > summary::-webkit-details-marker { display: none; }

@media (min-width: 48rem) {
    /* The row becomes two columns: the task, and what you can do to it.
       position:relative on the row rather than a flex parent, because
       the link inside must keep filling the row — a tap anywhere on it
       still opens the task. */
    .work-row { position: relative; }

    .work-row-tools {
        position: absolute;
        top: 8px;
        right: 4px;
        padding: 0;
    }

    /* Room for the buttons, so a long title wraps before them and not
       under them. MEASURED, not guessed: the pair is 196px wide with the
       German labels, and at 12rem a wrapped title stopped 2px short of
       Bearbeiten. 13rem leaves a thumb-visible gap. */
    .work-row > a { padding-right: 13rem; }
}

/* NOT ON A PHONE, AND ONLY ON HOVER ON A DESKTOP (2026-09-15): "the edit
   and delete buttons are more prominent than the date". A task is opened
   far more often than it is edited, and the task page carries Bearbeiten,
   whose editor carries Löschen — so nothing is lost, and delete is no
   longer one thumb-slip away while scrolling.

   "Can hover" rather than "is wide", so a tablet held sideways does not
   get buttons that only a mouse could ever reveal. Kept visible while
   focused or while its delete question is open, or the question would
   vanish the moment the pointer moved towards it. */
.work-row-tools { display: none; }

@media (min-width: 48rem) and (hover: hover) {
    .work-row-tools { display: flex; opacity: 0; transition: opacity 120ms ease; }

    .work-row:hover .work-row-tools,
    .work-row:focus-within .work-row-tools,
    .work-row-tools:has(details[open]) { opacity: 1; }
}

@media not all and (min-width: 48rem) and (hover: hover) {
    .work-row > a { padding-right: 4px; }
}

/* THE DATE LINE IS THE INFORMATION — when, who, which department — and
   read at the grey of a hint it looked like one. Between the two greys. */
.work-row-meta.muted { color: color-mix(in srgb, var(--text) 78%, transparent); }

/* ---------------------------------------------------------------------
   ANLERNEN — picking one article out of a hundred.

   A scrolling list of submit buttons rather than a <select>, because the
   row IS the answer: tapping a name links the barcode, with nothing to
   press afterwards. teach.js hides the rows that do not match what has
   been typed.

   AT THE END OF THIS FILE ON PURPOSE. Single class against single class
   is decided by which comes last, and this project has lost an afternoon
   to that twice — .btn-small beating .btn-quiet, .table-scroll beating
   .inv-table-wide.
   --------------------------------------------------------------------- */

.teach-list {
    list-style: none;
    margin: 6px 0 0;
    padding: 0;
    max-height: 22rem;
    overflow-y: auto;
    border: 1px solid var(--border);
    border-radius: var(--radius);
    background: var(--bg-input);
}

/* What is already taught, and what is not (2026-09-15). */
.teach-has-code { color: var(--text-muted); font-size: 0.85rem; }
.teach-overview { margin-top: 18px; border-top: 1px solid var(--border); }
.teach-overview > summary { padding: 12px 2px; font-weight: 600; cursor: pointer; }
.teach-overview-list { list-style: none; margin: 0 0 8px; padding: 0; }
.teach-overview-list li {
    display: flex;
    flex-wrap: wrap;
    align-items: baseline;
    gap: 2px 10px;
    padding: 8px 2px;
    border-top: 1px solid var(--border);
}
.teach-overview-name { font-weight: 500; }
.teach-overview-list .mono { word-break: break-all; }
.teach-overview-by { margin-left: auto; font-size: 0.85rem; }

/* hidden is set by teach.js; the list item is a flex child otherwise. */
.teach-list li[hidden] { display: none; }

.teach-list li + li { border-top: 1px solid var(--border); }

.teach-list button {
    display: block;
    width: 100%;
    min-height: var(--touch-min);
    padding: 10px 14px;
    border: 0;
    background: none;
    color: var(--text);
    font: inherit;
    text-align: left;
    cursor: pointer;
}

.teach-list button:hover,
.teach-list button:focus-visible {
    background: var(--bg-raised);
}

/* The filter sits directly above its list, so they read as one control. */
.teach-form input[type="search"] { margin-bottom: 0; }

/* ---------------------------------------------------------------------
   DELETING A TO-DO FROM THE LIST — the confirmation, fixed twice over
   (reported with a screenshot 2026-09-11).

   THE BUTTON WAS GREEN. .btn-danger sets terracotta far up this file and
   .btn-small sets moosgrün much further down, and single class against
   single class is settled by which comes last — so "endgültig löschen"
   was rendered in the same green as "save". The third time this exact
   trap has been sprung here, after .btn-small over .btn-quiet and
   .table-scroll over .inv-table-wide; the pairing below is the fix the
   other two already use.

   THE PANEL LANDED ON THE NEIGHBOURS. On a desktop .work-row-tools is
   position:absolute so the buttons can sit on the row itself — which
   means the panel inside it is out of flow too: it cannot push the row
   taller, so it hung across the rows below, and being a flex child of a
   row-direction box it sat BESIDE the buttons rather than under them.

   So it is anchored to its own <details> and floats, which is what it
   always looked like it was trying to be. z-index works without a
   stacking context of its own because position:relative with z-index
   auto does not create one — the panel simply paints above rows that
   never asked for a layer.
   --------------------------------------------------------------------- */

.btn-small.btn-danger {
    background: var(--terracotta);
    border: 1px solid var(--terracotta);
    color: #ffffff;
}

.work-row-del { position: relative; }

.work-row-del > .confirm-panel {
    position: absolute;
    top: calc(100% + 6px);
    right: 0;
    z-index: 5;
    width: 17rem;
    max-width: calc(100vw - 2rem);
    margin-top: 0;
    background: var(--bg-raised);
    box-shadow: 0 12px 32px -14px rgba(0, 0, 0, 0.75);
}

/* The panel is its own surface now, so the button inside it does not
   need the extra breathing room a panel in flow wanted. */
.work-row-del > .confirm-panel .btn { margin-top: 8px; }

/* ---------------------------------------------------------------------
   A SHARED TASK — the calendar card's shape, with the box on the left
   answering a different question (0097).

   On an event that box is the date, because "when" is what you want to
   know about an appointment. About a job you want to know whether it
   still needs doing, so here the box IS a checkbox: empty and outlined
   while it is open, filled with a tick once it is done. Read at a glance
   in a busy channel without reading a word.

   Done is also dimmed via .is-past, like an event that has happened —
   history, not a reminder.
   --------------------------------------------------------------------- */

.task-embed-state {
    background: transparent;
    border: 2px solid var(--text-muted);
    color: #ffffff;
    font-size: 1.4rem;
    font-weight: 700;
}

.task-embed-state.is-done {
    background: var(--moosgruen);
    border-color: var(--moosgruen);
}

/* Two ways to start something on Aufgaben — a to-do and a checklist —
   side by side (2026-09-13). .btn is block and full-width in this app,
   so a pair on one line has to be told otherwise, as on the task rows. */
.work-new-row {
    display: flex;
    flex-wrap: wrap;
    gap: 8px;
}

.work-new-row .btn {
    display: inline-flex;
    align-items: center;
    width: auto;
    margin: 0;
}

/* ---------------------------------------------------------------------
   THREE COSMETIC FAULTS ON THE TASK SCREEN, reported with a screenshot
   2026-09-13. Each looked like a one-off; two of them were not.

   "OOMMENTS". Section headings are <h2>, so they inherit Railroad Gothic
   — and are then shrunk to 0.78rem. The note on h1, h2, h3 near the top
   of this file already says why that is wrong: the face "is illegible at
   small sizes, so it never reaches body text, labels or buttons". At
   12px its C closes into an O. That is true of every .section-heading in
   the app, not only this one, so the fix is here rather than on the task
   page: small uppercase labels are the body face, and Railroad Gothic
   stays at the sizes it was drawn for. A class beats the element rule
   whatever the order, so this cannot be undone by source position.
   --------------------------------------------------------------------- */

.section-heading {
    font-family: Montserrat, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
    font-weight: 600;
    letter-spacing: 0.08em;
    line-height: 1.3;
}

/* THE GREY BOX. The app styles text INPUTS and the composer's textarea,
   and nothing else — so the comment box under a task fell through to the
   browser's own textarea: light grey, monospace, a different corner. It
   now looks like every other field on the same screen. */
.work-comment-add textarea {
    display: block;
    padding: 10px 12px;
    background: var(--bg-input);
    border: 1px solid var(--border);
    border-radius: 8px;
    color: var(--text);
    font: inherit;
    font-size: 16px;   /* below 16px, iOS Safari zooms the page on focus */
    resize: vertical;
}

.work-comment-add textarea:focus-visible {
    outline: 2px solid var(--moosgruen);
    outline-offset: 1px;
}

/* TAKE A PHOTO AND SAVE, NOT LEVEL. .btn carries margin-top: 24px
   everywhere, and this row centres its children — so Save sat half of
   that lower than its neighbour. And "Take a photo" is a <label> wrapping
   the file input, which does not centre its text the way a <button>
   does. Same height, same line, both centred, sharing the width. */
.work-photo-form .btn {
    display: inline-flex;
    flex: 1 1 0;
    align-items: center;
    justify-content: center;
    width: auto;
    margin: 0;
    text-align: center;
}

/* Editing a task comment beside deleting it, and correcting a voice
   note's transcript — 2026-09-14. */
.work-comment-remove { display: flex; gap: 16px; align-items: center; }
.work-comment-remove a.link-button { font-size: 0.78rem; color: var(--text-muted); }
.transcript-correct { display: inline-block; margin-top: 4px; font-size: 0.78rem; }
.transcript-edit-audio { display: block; width: 100%; margin: 0 0 14px; }

/* Badges for what is new — 2026-09-14. The calendar count sits on the
   corner of its icon rather than beside it: the icon is a fixed touch
   target in a full bar, and a number next to it would push search off a
   phone. */
.app-search { position: relative; }
.app-icon-badge {
    position: absolute;
    top: 2px;
    right: -2px;
    min-width: 18px;
    margin: 0;
    padding: 0 5px;
    font-size: 0.68rem;
    line-height: 18px;
}
.app-search.has-new { color: var(--text); }

/* Moosgrün like every unread count, so "new" reads the same everywhere. */
.pill-new { background: var(--moosgruen); border-color: var(--moosgruen); color: #ffffff; }

.cal-new {
    margin: 0 0 16px;
    padding: 10px 14px;
    border-left: 3px solid var(--moosgruen);
    border-radius: 6px;
    background: var(--surface, rgba(255, 255, 255, 0.04));
}
.cal-new-head { margin: 0 0 6px; font-size: 0.85rem; text-transform: uppercase; letter-spacing: 0.04em; }
.cal-new ul { margin: 0; padding: 0; list-style: none; }
.cal-new li + li { margin-top: 4px; }
.cal-new a { display: flex; flex-wrap: wrap; gap: 4px 10px; align-items: baseline; text-decoration: none; }

/* Scheduled messages, read first and edited one at a time — 2026-09-14. */
.plan-card { padding-top: 14px; }
.plan-card-head {
    display: flex;
    flex-wrap: wrap;
    justify-content: space-between;
    align-items: baseline;
    gap: 4px 12px;
    margin-bottom: 10px;
}
.plan-to { color: var(--text-muted); text-decoration: none; }
.plan-to strong { color: var(--text); }
.plan-when { color: var(--text-muted); font-size: 0.85rem; white-space: nowrap; }
.plan-body { margin: 0 0 6px; }

/* A draft among the scheduled ones (2026-09-27).
 *
 * FULL STRENGTH TEXT, and this was wrong for half a day: the body was
 * set in --text-muted, whose own definition three thousand lines up
 * says "fine for secondary, not for body". On a dark phone screen it
 * read as disabled — and the words are the entire reason for the card.
 * Reported the same evening: "readability and cosmetics".
 *
 * WHAT CARRIES THE DIFFERENCE INSTEAD is the green Entwurf pill in the
 * head, which is the marker the channel list already uses for exactly
 * this. Not amber, not red: §8.2 keeps those for status, and an unsent
 * message is a kind of thing, not a state of alarm.
 */
.draft-body { margin: 0 0 6px; color: var(--text); }

/* A HEADING THAT DIVIDES RATHER THAN LABELS. The same 2px terracotta as
   the page header above it (.card h1), so Geplant reads as two stacks
   with a line over each — which is what the header already taught the
   eye to expect on this page. */
.plan-section {
    margin: 32px 0 16px;
    padding-bottom: 10px;
    border-bottom: 2px solid var(--terracotta);
}
.plan-actions {
    display: flex;
    flex-wrap: wrap;
    gap: 8px;
    align-items: center;
    margin-top: 12px;
}
.plan-actions form { margin: 0; }
.plan-actions .btn { display: inline-flex; align-items: center; justify-content: center; width: auto; margin: 0; text-decoration: none; }
.plan-edit textarea { width: 100%; }

/* Private to-dos: the add row on "Meine Liste" — 2026-09-14. */
.work-tabs { flex-wrap: wrap; }
.work-private-add { display: flex; flex-wrap: wrap; gap: 8px; align-items: stretch; margin: 8px 0 18px; }
.work-private-add input[type="text"] { flex: 1 1 220px; min-width: 0; }
.work-private-add input[type="date"] { flex: 0 1 170px; }
.work-private-add .btn { width: auto; margin: 0; }

/* -----------------------------------------------------------------
   HOME: TODAY'S TILES AND THE CALENDAR IN LINES — 2026-09-14.
   Reported from a desktop screenshot: prep, scheduled, the calendar and
   the channels were all the same big card, so a thing happening read
   as one more place to go. Tiles for what is waiting today; one line
   per calendar entry with a date chip; the channels keep the cards.
   ----------------------------------------------------------------- */
.home-tiles {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
    gap: 10px;
    margin: 0 0 20px;
}
.home-tile {
    display: flex;
    flex-direction: column;
    gap: 2px;
    min-height: var(--touch-min);
    padding: 12px 14px;
    border: 1px solid var(--border);
    border-radius: var(--radius);
    background: var(--bg-raised);
    color: var(--text);
    text-decoration: none;
}
.home-tile:hover { border-color: var(--text-muted); }
.home-tile-num { font-size: 1.5rem; font-weight: 700; line-height: 1.1; }
.home-tile-label { font-weight: 600; }
.home-tile-sub { color: var(--text-muted); font-size: 0.8rem; }
/* The same colour cues the old cards carried, on the top edge only. */
.home-tile-plan { border-top: 3px solid var(--moosgruen); }
.home-tile-work { border-top: 3px solid var(--moosgruen); }
.home-tile-prep { border-top: 3px solid var(--yellow); }

.home-cal { margin: 0 0 24px; }
.home-cal-head { display: flex; justify-content: space-between; align-items: baseline; gap: 12px; }
.home-cal-open { color: var(--text-muted); font-size: 0.85rem; text-decoration: none; white-space: nowrap; }
.home-cal-open:hover { color: var(--text); }
.home-cal-list { margin: 0; padding: 0; list-style: none; border-top: 1px solid var(--border); }
.home-cal-list li { border-bottom: 1px solid var(--border); }
.home-cal-row {
    display: flex;
    align-items: center;
    gap: 12px;
    min-height: var(--touch-min);
    padding: 8px 2px;
    color: var(--text);
    text-decoration: none;
}
.home-cal-row:hover .home-cal-title { text-decoration: underline; }
.home-cal-chip {
    display: flex;
    flex: 0 0 44px;
    flex-direction: column;
    align-items: center;
    padding: 3px 0;
    border: 1px solid var(--border);
    border-radius: 8px;
    line-height: 1.1;
}
.home-cal-wday { color: var(--text-muted); font-size: 0.68rem; text-transform: uppercase; letter-spacing: 0.04em; }
.home-cal-day { font-size: 1.05rem; font-weight: 700; }
.home-cal-text { display: flex; flex-direction: column; min-width: 0; }
.home-cal-title { font-weight: 600; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.home-cal-meta { font-size: 0.82rem; }

@media (max-width: 480px) {
    /* Three abreast on a phone, a little tighter, so the channels start
       near the top of the screen rather than a screen further down. */
    .home-tiles { grid-template-columns: repeat(auto-fit, minmax(96px, 1fr)); gap: 8px; }
    .home-tile { padding: 10px; }
    .home-tile-num { font-size: 1.25rem; }
    .home-tile-sub { font-size: 0.72rem; }
}

/* Several-day calendar entries — 2026-09-14. */
.cal-range { display: block; font-size: 0.8rem; }
.cal-end { padding-top: 4px; }
.cal-end-hint { margin: 0 0 6px; font-size: 0.85rem; }

/* The line under "Alle im Kanal" — 2026-09-14. */
.work-vis-hint { margin: -2px 0 8px 30px; font-size: 0.82rem; }

/* People picker: you first, everybody else folded — 2026-09-14. */
.people-fold { margin-top: 6px; border: 1px solid var(--border); border-radius: 8px; }
.people-fold > summary { padding: 10px 12px; cursor: pointer; min-height: var(--touch-min); display: flex; align-items: center; gap: 6px; }
.people-fold[open] > summary { border-bottom: 1px solid var(--border); }
.people-fold-list { padding: 6px 12px 10px; }

/* Conversations: the most recent eight, then "Alle anzeigen" — 2026-09-14.
   The fold is a switch for this rule, not a second list; see index.php. */
.dm-capped > li:nth-child(n+9) { display: none; }
details[data-dm-foldout]:has(.dm-more[open]) .dm-capped > li:nth-child(n+9),
.home-lists:has(.dm-more[open]) .dm-capped > li:nth-child(n+9) { display: list-item; }
.dm-more { margin: 2px 0 6px; }
.dm-more > summary { cursor: pointer; color: var(--text-muted); font-size: 0.88rem; padding: 6px 2px; list-style: none; }
.dm-more > summary::-webkit-details-marker { display: none; }
.dm-more .dm-more-open, .dm-more[open] .dm-more-closed { display: none; }
.dm-more[open] .dm-more-open { display: inline; }

.dm-hidden-row { display: flex; gap: 10px; align-items: center; }
.dm-hidden-row .channel { flex: 1; margin-bottom: 0; }
.dm-hidden-list li + li { margin-top: 10px; }
.channel-tab-hide { margin-left: auto; }
.dm-new-anyway { margin-top: 22px; }

/* Pinning — 2026-09-14. The pin and hide buttons sit together at the far
   end of the tab strip; the pinned section on the home screen and the rail
   uses the lists' own styles. */
.channel-tab-pin { margin-left: auto; }
.channel-tab-pin + .channel-tab-hide { margin-left: 0; }
.channel-tab-pin form, .channel-tab-pin { display: inline-flex; }
.channel-tab.is-pinned { color: var(--text); }
.pinned-group { margin-bottom: 8px; }
.rail-pins { margin: 0 0 8px; padding: 0; list-style: none; }

/* THE PIN BUTTON IN OUR GREEN — asked for 2026-09-14 ("the button design
   sucks a bit, can you make it in our typical dark green button"). A real
   button among the tabs, not another tab: it does something rather than
   going somewhere. Filled to pin; outlined in the same green once pinned,
   so the state can be read at a glance. Hide stays a quiet tab. */
.channel-tab-pin { align-self: center; }
.channel-tab-pin .channel-tab {
    min-height: 32px;
    margin: 4px 0;
    padding: 0 14px;
    border: 1px solid var(--moosgruen);
    border-radius: 8px;
    background: var(--moosgruen);
    color: #ffffff;
    font: inherit;
    font-size: 0.85rem;
    font-weight: 600;
    cursor: pointer;
}
.channel-tab-pin .channel-tab:hover { color: #ffffff; filter: brightness(1.45); }
.channel-tab-pin .channel-tab.is-pinned {
    background: transparent;
    color: var(--text);
}
.channel-tab-pin .channel-tab.is-pinned:hover { filter: none; background: var(--moosgruen); color: #ffffff; }
.channel-tab-pin + .channel-tab-hide { margin-left: 6px; }

/* -----------------------------------------------------------------
   HIDE FROM THE ROW — 2026-09-14. A × at the right edge of a conversation
   row, shown on hover (and keyboard focus) where there is a mouse; on a
   touch screen it stays out of sight until a swipe left uncovers the word.
   ----------------------------------------------------------------- */
li:has(> .row-hide) { position: relative; }
.row-hide {
    position: absolute;
    top: 50%;
    right: 8px;
    margin: 0;
    transform: translateY(-50%);
    opacity: 0;
    pointer-events: none;
    transition: opacity 0.12s ease;
}
.channel-list li:has(> .row-hide) { margin-bottom: 10px; }
.channel-list li:has(> .row-hide) > .channel { margin-bottom: 0; transition: transform 0.18s ease; }
.row-hide-btn {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    min-width: 28px;
    height: 28px;
    padding: 0;
    border: 0;
    border-radius: 999px;
    background: var(--bg-input);
    color: var(--text-muted);
    font: inherit;
    font-size: 1.05rem;
    line-height: 1;
    cursor: pointer;
}
.row-hide-btn:hover { color: var(--text); background: var(--border); }
.rh-text { display: none; }

@media (hover: hover) {
    li:hover > .row-hide,
    li:focus-within > .row-hide { opacity: 1; pointer-events: auto; }
    /* The badge moves aside for the ×, rather than sitting under it. */
    .channel-rail li:hover:has(> .row-hide) .badge { margin-right: 26px; }
}

/* Swiped open on a phone: the row slides left and the word shows. */
li.is-swiped > .row-hide {
    top: 0;
    bottom: 0;
    right: 0;
    transform: none;
    opacity: 1;
    pointer-events: auto;
}
li.is-swiped .row-hide-btn {
    height: 100%;
    padding: 0 16px;
    border-radius: var(--radius);
    background: var(--bg-input);
    color: var(--text);
    font-size: 0.9rem;
    font-weight: 600;
}
li.is-swiped .rh-x { display: none; }
li.is-swiped .rh-text { display: inline; }
li.is-swiped > .channel,
li.is-swiped > .rail-item { transform: translateX(-120px); }
li.is-hiding { opacity: 0.4; }

.row-hide-toast {
    position: fixed;
    left: 50%;
    bottom: 24px;
    z-index: 50;
    display: flex;
    gap: 14px;
    align-items: center;
    padding: 10px 16px;
    border: 1px solid var(--border);
    border-radius: 999px;
    background: var(--bg-raised);
    color: var(--text);
    box-shadow: var(--shadow-sm);
    transform: translateX(-50%);
}
.dm-info-hide { font-size: 0.9rem; }

/* THE WORD, NOT THE ×, UNDER A MOUSE TOO — 2026-09-14: "maybe we can have
   a miniword 'hide' that appears on mouseover ... we'd have the space".
   A × was read as "delete"; the word says what happens. */
@media (hover: hover) {
    .row-hide .rh-x { display: none; }
    .row-hide .rh-text { display: inline; }
    .row-hide .row-hide-btn {
        min-width: 0;
        height: 24px;
        padding: 0 10px;
        font-size: 0.75rem;
        font-weight: 600;
    }
    .channel-rail li:hover:has(> .row-hide) .badge { margin-right: 84px; }
}

/* -----------------------------------------------------------------
   TASK URGENCY, CREATION DATE AND SORTING — 2026-09-14.
   Normal has no label. Niedrig is quiet grey, Hoch amber, Dringend red,
   Kritisch red and filled — the one that should be impossible to miss.
   ----------------------------------------------------------------- */
.pill-prio-low { color: var(--text-muted); }
.pill-prio-high { border-color: var(--attention); color: var(--attention); }
.pill-prio-urgent { border-color: var(--urgent); color: var(--urgent); }
.pill-prio-critical { border-color: var(--urgent-fill); background: var(--urgent-fill); color: #ffffff; }

.work-row-created { display: inline-block; min-width: 3.6em; font-variant-numeric: tabular-nums; }

.work-sort { display: flex; flex-wrap: wrap; gap: 4px 12px; align-items: baseline; margin: 4px 2px 10px; font-size: 0.85rem; }
.work-sort-opt { color: var(--text-muted); text-decoration: none; }
.work-sort-opt:hover { color: var(--text); }
.work-sort-opt.is-current { color: var(--text); font-weight: 600; text-decoration: underline; text-underline-offset: 3px; }

.work-prio .prio-choices { display: flex; flex-wrap: wrap; gap: 6px; }
.prio-choice { display: inline-flex; }
.prio-choice input { position: absolute; opacity: 0; width: 1px; height: 1px; }
.prio-choice span {
    display: inline-flex;
    align-items: center;
    min-height: 36px;
    padding: 0 12px;
    border: 1px solid var(--border);
    border-radius: 999px;
    color: var(--text-muted);
    font-size: 0.88rem;
    cursor: pointer;
}
.prio-choice input:focus-visible + span { outline: 2px solid var(--text); outline-offset: 2px; }
.prio-choice input:checked + span { color: var(--text); border-color: var(--text); font-weight: 600; }
.prio-choice-high input:checked + span { border-color: var(--attention); color: var(--attention); }
.prio-choice-urgent input:checked + span { border-color: var(--urgent); color: var(--urgent); }
.prio-choice-critical input:checked + span { border-color: var(--urgent-fill); background: var(--urgent-fill); color: #ffffff; }

/* -----------------------------------------------------------------
   MESSAGE TOOLS ON HOVER, ON A TAP ON A PHONE — 2026-09-14.
   A run of messages already shares one name and one card; it still read
   as separate blocks because every message carried its own row of seven
   icons and its own time. Now the icons appear only for the message being
   pointed at (a small floating bar at its top right, the way Slack does
   it) or tapped, and a continuation's time sits quietly in the empty
   avatar column until then. Reactions and a reply count stay visible —
   those say something; the tools only offer something.
   ----------------------------------------------------------------- */
.message { position: relative; }
.message-tools { display: none; align-items: center; gap: 4px; flex-wrap: wrap; }
.message-actions:not(:has(> .chip, > .action)) { margin-top: 0; }

.message-continues .message-time-quiet {
    position: absolute;
    top: 6px;
    left: 4px;
    width: 42px;
    margin: 0;
    text-align: center;
    font-size: 0.66rem;
    opacity: 0;
    transition: opacity 0.12s ease;
}

@media (hover: hover) {
    .message-tools {
        position: absolute;
        top: 4px;
        right: 8px;
        /* BEHIND THE COMPOSER (6 < 8), AND THAT IS NOT NEGOTIABLE: a bar
           scrolled past the box must go under Senden, not over it —
           reported twice, 2026-09-27 and again 2026-09-28 after one day
           at 9. The other half of the problem, the newest message's bar
           being cut in half by the composer, is solved with ROOM rather
           than layering: see .messages below in this block. */
        z-index: 6;
        padding: 2px 4px;
        border: 1px solid var(--border);
        border-radius: 10px;
        background: var(--bg-raised);
        box-shadow: var(--shadow-sm);
    }
    .message:hover .message-tools,
    .message:focus-within .message-tools,
    .message-tools:has(.picker[open]) { display: flex; }

    /* ROOM FOR THE LAST BAR (2026-09-28). The bar hangs off the bottom
       edge of its message, and for the newest message that edge is
       where the composer sits — so with the bar correctly behind the
       composer, the newest message's bar was "halfway swallowed". Two
       fixes were tried in the wrong place (lifting the composer, then
       lifting the bar) and each cured one report by causing the other.
       The list simply ends with enough space that the last bar never
       reaches the composer. Only where there is a pointer: on a phone
       the bar is in the flow, not hanging, and needs no room. */
    .messages { padding-bottom: 44px; }
    .message:hover .message-time-quiet,
    .message:focus-within .message-time-quiet { opacity: 1; }
    /* The emoji list opens leftwards from a bar at the right edge. */
    .message-tools .picker-list { left: auto; right: 0; max-width: min(400px, 90vw); }
}

/* ON A PHONE THE TOOLS ARE ALWAYS THERE (changed 2026-09-17): "the whole
   reactions bar … should just be there by default, not on tap — way
   easier". A tap to reveal them was one more step before every reaction.
   A tap still shows a continuation's quiet time, which is information
   rather than a control. */
@media (hover: none) {
    .message-tools { display: flex; margin-top: 4px; }
    .message.is-active .message-time-quiet { opacity: 1; }
}

/* THE PLAYER AND ITS TRANSCRIPT STAY INSIDE THE CARD — 2026-09-14. On a
   narrow phone the voice note stuck out past the message's right edge:
   a flex item will not shrink below its content by default, and the
   player's own controls are wide. */
.attachments > li { min-width: 0; max-width: 100%; }
.attachments > li:has(.attachment-audio) { flex: 1 1 100%; }
.attachment-audio, .attachment-transcript { max-width: min(24rem, 100%); }
.voice-unplayable { max-width: min(24rem, 100%); margin: 4px 0 0; font-size: 0.8rem; }

/* -----------------------------------------------------------------
   THE TASKS PAGE ON A PHONE — 2026-09-14 ("very convoluted and super
   full"). Filters and sorting behind one line; the four lists in one
   swipeable row instead of two lines; no second "Aufgaben" heading under
   the one the bar already shows; the date on the add box labelled.
   ----------------------------------------------------------------- */
.work-controls > .work-controls-summary { display: none; }
.work-private-due-label { align-self: center; font-size: 0.85rem; }

@media (max-width: 700px) {
    .work-page-title { display: none; }

    .work-tabs { flex-wrap: nowrap; overflow-x: auto; scrollbar-width: none; }
    .work-tabs::-webkit-scrollbar { display: none; }
    .work-tabs .store-tab { flex: 0 0 auto; }

    .work-controls { margin: 8px 0 14px; border: 1px solid var(--border); border-radius: 10px; background: var(--bg-input); }
    .work-controls > .work-controls-summary {
        display: flex;
        flex-direction: column;
        gap: 2px;
        min-height: var(--touch-min);
        padding: 10px 14px;
        cursor: pointer;
        list-style: none;
    }
    .work-controls > .work-controls-summary::-webkit-details-marker { display: none; }
    .work-controls-label { font-weight: 600; }
    .work-controls-label::after { content: " ▾"; color: var(--text-muted); }
    .work-controls[open] .work-controls-label::after { content: " ▴"; }
    .work-controls-now { color: var(--text-muted); font-size: 0.85rem; }
    .work-controls[open] { padding: 0 8px 8px; }
    .work-controls[open] > .work-controls-summary { padding-left: 6px; padding-right: 6px; }

    .work-new-row { display: flex; gap: 8px; margin: 10px 0; }
    .work-new-row .btn { flex: 1 1 0; min-height: 40px; padding: 0 10px; font-size: 0.9rem; }

    .work-private-add input[type="date"] { flex: 1 1 120px; }
}

/* THE ORDER PAGES, CENTRED — 2026-09-14. They used a container with no
   width and no margin, so both the list and a single order ran flush
   against the left edge of a desktop screen and stretched across all of
   it (the same fault the Aufgaben page had). A little wider than a
   reading page: the list lines and the article rows carry more across. */
.ord-page { max-width: 56rem; }

/* An order item's name in the reader's language, after the German one
   the supplier gets — 2026-09-14. */
.ord-item-tr { font-size: 0.88em; }

/* Orders by delivery week, older weeks folded — 2026-09-14. */
.ord-week { margin: 14px 0 0; }
.ord-week-head {
    display: flex;
    flex-wrap: wrap;
    justify-content: space-between;
    align-items: baseline;
    gap: 2px 12px;
    margin: 0;
    padding: 10px 4px 6px;
    border-bottom: 1px solid var(--border);
    font-size: 0.95rem;
}
summary.ord-week-head { cursor: pointer; list-style: none; }
summary.ord-week-head::-webkit-details-marker { display: none; }
.ord-week-title { font-weight: 600; }
details.ord-week > summary .ord-week-title::before { content: "▸ "; color: var(--text-muted); }
details.ord-week[open] > summary .ord-week-title::before { content: "▾ "; }
.ord-week-pending .ord-week-head { border-bottom-color: var(--attention); }
.ord-week .ord-list { margin-top: 0; }

/* ---------------------------------------------------------------------
   THE RUNDGANG (A48) — counting on a phone or the store's iPad.

   Big rows, one hand, a cold room: every target is at least the touch
   minimum, the number field is wide enough for "12,5", and − / + sit
   where a thumb lands. "–" is the placeholder, never a value.
   --------------------------------------------------------------------- */
.walk-status { margin: 4px 0 10px; color: var(--text-muted); font-size: 0.92rem; }
.walk-save-state { font-size: 0.85rem; }
.walk-save-state.is-ok { color: var(--moosgruen); }
.walk-save-state.is-offline { color: var(--attention); }

.walk-counter { margin: 0 0 14px; }
.walk-counter > summary { display: flex; align-items: center; gap: 8px; cursor: pointer; list-style: none; }
.walk-counter > summary::-webkit-details-marker { display: none; }
.walk-counter .walk-who { margin-top: 10px; }

.walk-names { display: flex; flex-wrap: wrap; gap: 8px; margin: 8px 0; }
.walk-names .chip { height: var(--touch-min); padding: 0 16px; font: inherit; cursor: pointer; border: 1px solid var(--border); }
.walk-who-other { display: flex; gap: 8px; margin-top: 8px; }
.walk-who-other input { flex: 1 1 10rem; }
.walk-start { margin: 18px 0 8px; }

.walk-areas { list-style: none; margin: 0 0 18px; padding: 0; display: flex; flex-direction: column; gap: 6px; }
.walk-area {
    display: flex; align-items: center; gap: 10px;
    min-height: 56px; padding: 8px 14px;
    background: var(--bg-raised); border: 1px solid var(--border); border-radius: 10px;
    color: var(--text); text-decoration: none; font-size: 1.05rem;
}
.walk-area:hover { background: var(--bg-input); }
.walk-area.is-done { border-color: var(--moosgruen); }
.walk-area.is-done .walk-area-progress { color: var(--moosgruen); }
.walk-area-name { flex: 1 1 auto; font-weight: 600; }
.walk-area-progress { font-variant-numeric: tabular-nums; color: var(--text-muted); }

.walk-scan { display: flex; align-items: center; gap: 8px; margin: 0 0 12px; }
.walk-scan input { flex: 1 1 auto; min-width: 0; min-height: var(--touch-min); margin: 0; }
.walk-camera {
    flex: 0 0 auto; display: inline-flex; align-items: center; justify-content: center;
    width: var(--touch-min); height: var(--touch-min); padding: 0;
    border: 1px solid var(--border); border-radius: 10px; background: var(--bg-raised); color: var(--text); cursor: pointer;
}
.walk-camera[hidden] { display: none; }
/* The way to the keyboard, beside the camera and quieter than it: on a
   walk the camera is what people reach for a hundred times and a typed
   code is the exception. See Web::walkScanBox. */
.walk-type { color: var(--text-muted); }
.walk-type:active { color: var(--text); }
.walk-page h1 { margin-bottom: 2px; }
.walk-camera-view { position: relative; margin: 0 0 14px; }
.walk-camera-view video { width: 100%; max-height: 60vh; border-radius: 10px; background: #000; }
.walk-camera-view .btn { margin-top: 8px; }

.walk-lines { list-style: none; margin: 0 0 18px; padding: 0; }
.walk-line { border-top: 1px solid var(--border); }
.walk-line-form { display: flex; flex-wrap: wrap; align-items: center; gap: 6px 12px; padding: 10px 2px; }
.walk-line-name { flex: 1 1 12rem; display: flex; flex-wrap: wrap; align-items: baseline; gap: 6px; }
.walk-line-name em { font-style: normal; font-size: 0.85rem; }
.walk-line-by { flex-basis: 100%; font-size: 0.8rem; min-height: 1em; }
.walk-line-ctl { display: flex; align-items: center; gap: 6px; }
.walk-step {
    width: var(--touch-min); height: var(--touch-min);
    border: 1px solid var(--border); border-radius: 10px; background: var(--bg-raised);
    color: var(--text); font-size: 1.4rem; line-height: 1; cursor: pointer;
}
.walk-step:active { background: var(--bg-input); }
.walk-qty {
    width: 5.2rem; height: var(--touch-min); text-align: center;
    font-size: 1.15rem; font-variant-numeric: tabular-nums;
}
.walk-line.is-counted .walk-qty { border-color: var(--moosgruen); }
.walk-line.is-focus { background: var(--bg-raised); }
.walk-line-error { flex-basis: 100%; margin: 0 0 8px; color: var(--urgent); font-size: 0.85rem; }
.walk-nav { display: flex; flex-wrap: wrap; gap: 8px; margin: 14px 0; }
.walk-nav .btn { flex: 1 1 12rem; }
.walk-finish textarea { width: 100%; }

.walk-reports { list-style: none; margin: 0 0 14px; padding: 0; }
.walk-reports li + li { border-top: 1px solid var(--border); }
.walk-reports a { display: flex; flex-wrap: wrap; justify-content: space-between; gap: 4px 12px; padding: 10px 2px; color: var(--text); text-decoration: none; }
.walk-reports a:hover { background: var(--bg-input); }
.walk-filter { display: flex; flex-wrap: wrap; align-items: end; gap: 8px; margin: 0 0 14px; }
.walk-filter label { display: flex; flex-direction: column; gap: 2px; font-size: 0.85rem; }
.walk-report-actions { margin: 0 0 14px; }
.walk-facts { display: grid; grid-template-columns: repeat(auto-fit, minmax(14rem, 1fr)); gap: 8px 16px; margin: 0 0 14px; }
.walk-facts dt { font-size: 0.78rem; letter-spacing: 0.06em; text-transform: uppercase; color: var(--text-muted); }
.walk-facts dd { margin: 2px 0 0; }
.walk-report tr.is-uncounted td { color: var(--text-muted); }
.walk-report tr.is-uncounted td strong { color: var(--attention); }
.walk-diff { font-size: 0.8rem; }
.walk-diff.is-up { color: var(--moosgruen); }
.walk-diff.is-down { color: var(--attention); }
.inv-walk-start { display: flex; flex-wrap: wrap; gap: 8px; margin: 0 0 14px; }
.inv-walk-start .btn { width: auto; display: inline-flex; }
.link-quiet { color: var(--text-muted); }

/* ONE ROW PER ARTICLE ON A PHONE (2026-09-17). The number field took the
   generic full-width input rule and pushed the − / + onto a second line,
   so a room of forty articles was eighty rows of scrolling. Name left,
   counter right; a long name wraps within its own column. */
.walk-line-form { flex-wrap: nowrap; }
.walk-line-name { flex: 1 1 auto; min-width: 0; }
.walk-line-ctl { flex: 0 0 auto; gap: 4px; }
.walk-line-ctl .walk-qty { flex: 0 0 auto; width: 3.6rem; min-width: 0; margin: 0; padding: 0 4px; }
.walk-line-ctl .walk-step { flex: 0 0 auto; width: 40px; }
.walk-line-name { display: block; line-height: 1.3; }
.walk-line-name .inv-nr { margin-right: 4px; }
.walk-line-by { display: block; }

/* The camera: the video, and a square guide over the part that is read. */
.walk-camera-frame { position: relative; overflow: hidden; border-radius: 10px; background: #000; }
.walk-camera-frame video { display: block; width: 100%; max-height: 55vh; object-fit: cover; }
.walk-camera-guide {
    position: absolute; top: 50%; left: 50%;
    width: min(60vw, 45vh, 320px); aspect-ratio: 1 / 1;
    transform: translate(-50%, -50%);
    border: 3px solid rgba(255, 255, 255, 0.9);
    border-radius: 12px;
    box-shadow: 0 0 0 9999px rgba(0, 0, 0, 0.35);
    pointer-events: none;
}
.walk-camera-status { margin: 8px 0; font-size: 0.9rem; }
.walk-camera-actions { display: flex; flex-wrap: wrap; gap: 8px; }
.walk-camera-actions .btn { flex: 1 1 10rem; }

/* NO ZOOM WHILE COUNTING (2026-09-17): tapping − / + quickly on an iPhone
   read as a double-tap and zoomed the page in. manipulation keeps
   scrolling and pinch-to-zoom but drops the double-tap zoom; the 16 px
   floor stops iOS zooming into a field it thinks is too small to read. */
.walk-page, .walk-page button, .walk-page input, .walk-page a { touch-action: manipulation; }
.walk-page input, .walk-page textarea, .walk-page select { font-size: max(16px, 1rem); }
.walk-line-ctl .walk-qty { font-size: max(16px, 1.15rem); }

/* DRAG AND DROP in Inventar (2026-09-17). The handle is the only part
   that grabs — touch-action none there, so the rest of the row still
   scrolls the page on a phone. */
.inv-drag {
    display: inline-flex; align-items: center; justify-content: center;
    width: 32px; height: var(--touch-min); margin: -6px 0 -6px -6px;
    color: var(--text-muted); font-size: 1.2rem; cursor: grab;
    touch-action: none; user-select: none; -webkit-user-select: none;
}
.inv-drag:active { cursor: grabbing; }
.inv-walk > li.is-dragging {
    pointer-events: none;
    opacity: 0.9;
    border-color: var(--moosgruen);
    box-shadow: var(--shadow-sm);
    background: var(--bg-input);
}
body.inv-dragging { user-select: none; -webkit-user-select: none; cursor: grabbing; }
.inv-sections ul.inv-walk:empty { min-height: 44px; border: 1px dashed var(--border); border-radius: 8px; }
.inv-sections.is-saved .inv-area-fold[open] > summary .inv-area-count { color: var(--moosgruen); }

/* WHO HAS WHICH AREA (0108). The name sits beside the area's; the tools
   under the row are small, because counting is what the row is for. */
.walk-area-item { display: flex; flex-direction: column; gap: 4px; }
.walk-area-holder { font-weight: 400; color: var(--text-muted); }
.walk-area-holder.is-mine { color: var(--moosgruen); font-weight: 600; }
.walk-area-tools { display: flex; flex-wrap: wrap; align-items: flex-start; gap: 6px; padding: 0 4px 4px; }
.walk-area-tools form { margin: 0; }
.walk-assign > summary { list-style: none; cursor: pointer; }
.walk-assign > summary::-webkit-details-marker { display: none; }
.walk-assign-form { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 6px; }
.walk-assign-form select { min-height: var(--touch-min); margin: 0; }
.walk-held { margin: 0 0 10px; }

/* WARM AND KALT AS FOLDS in the prep admin (2026-09-17). */
.prep-fold { margin: 0 0 14px; border: 1px solid var(--border); border-radius: 10px; background: var(--bg-raised); }
.prep-fold > summary.prep-fold-head {
    display: flex; align-items: baseline; gap: 10px;
    padding: 12px 14px; cursor: pointer; list-style: none; font-weight: 600;
}
.prep-fold > summary::-webkit-details-marker { display: none; }
.prep-fold-name::before { content: "▸ "; color: var(--text-muted); }
.prep-fold[open] .prep-fold-name::before { content: "▾ "; }
.prep-fold > .table-scroll { padding: 0 8px 8px; }
.prep-cold-switch { margin-top: 12px; padding-top: 12px; border-top: 1px solid var(--border); }
.prep-cold-switch p { margin: 0 0 8px; }

/* --- a voice note whose text is still being written ------------------- */
/* 2026-09-17. The words arrive a few seconds after the page was drawn, so
   this holds their place rather than leaving blank space that reads as
   "transcription is broken".

   DASHED AND FAINT, deliberately: it is a promise, not content, and it
   must not look like something to read. The three dots breathe slowly —
   slow enough to say "working" and not fast enough to pull the eye off
   a conversation.

   NO SPINNER. The label plus a soft pulse costs nothing and does not
   claim to know how long it will take. */
.attachment-transcript.is-waiting {
    border-style: dashed;
    background: none;
}

.attachment-transcript.is-waiting .transcript-label {
    margin: 0;
    animation: transcript-wait 1.8s ease-in-out infinite;
}

@keyframes transcript-wait {
    0%, 100% { opacity: 0.55; }
    50%      { opacity: 1; }
}

/* Somebody who has asked for less movement gets the label and no pulse —
   it says the same thing either way. */
@media (prefers-reduced-motion: reduce) {
    .attachment-transcript.is-waiting .transcript-label { animation: none; }
}

/* --- the scan station (A46, 2026-09-18) ------------------------------ */
/* An iPad on a shelf by the cold room, read from arm's length with a bag
   in the other hand. So: the mode is the biggest thing on the page and
   has a colour of its own, and the result of the last scan is the second
   biggest. Everything else is quiet. */

.station-house { margin: 0 0 10px; }

.station-modes {
    display: grid;
    grid-template-columns: 1fr 1fr;
    gap: 10px;
    margin: 0 0 10px;
}

.station-mode {
    display: flex;
    align-items: center;
    justify-content: center;
    min-height: 72px;
    border: 2px solid var(--border);
    border-radius: var(--radius);
    background: var(--bg-input);
    color: var(--text-muted);
    font-size: 1.25rem;
    font-weight: 700;
    text-decoration: none;
}

/* TAKE OUT IS THE EVERYDAY MODE and wears the house green; THROWING AWAY
   wears the warning red, so a station left in the wrong mode from this
   morning looks wrong from across the room. */
.station-mode-take.is-on {
    background: var(--moosgruen);
    border-color: var(--moosgruen);
    color: #ffffff;
}

.station-mode-bin.is-on {
    background: var(--urgent-fill);
    border-color: var(--urgent-fill);
    color: #ffffff;
}

.station-reasons {
    display: flex;
    flex-wrap: wrap;
    gap: 8px;
    margin: 4px 0 12px;
}

.station-reasons .chip {
    display: inline-flex;
    align-items: center;
    min-height: var(--touch-min);
    text-decoration: none;
}

.station-result {
    margin: 12px 0;
    padding: 14px 16px;
    border-radius: var(--radius);
    background: var(--bg-raised);
    border: 1px solid var(--border);
}

.station-did-take { border-left: 6px solid var(--moosgruen); }
.station-did-bin  { border-left: 6px solid var(--urgent-fill); }

.station-result-what { margin: 0 0 4px; font-size: 1.1rem; }

.station-result-item {
    margin: 0 0 8px;
    font-size: 1.35rem;
    font-weight: 700;
    overflow-wrap: anywhere;
}

.station-result-item .muted {
    display: block;
    font-size: 0.85rem;
    font-weight: 400;
}

.station-scan { margin: 14px 0 6px; }

.station-scan-row {
    display: flex;
    gap: 8px;
}

.station-scan-row input {
    flex: 1 1 auto;
    min-width: 0;
    text-transform: uppercase;
}

.station-recent {
    list-style: none;
    margin: 0;
    padding: 0;
}

.station-recent li {
    display: flex;
    align-items: center;
    gap: 10px;
    padding: 10px 0;
    border-bottom: 1px solid var(--border);
}

.station-recent-what {
    flex: 1 1 auto;
    min-width: 0;
    display: flex;
    flex-direction: column;
    overflow-wrap: anywhere;
}

.station-bin-btn { white-space: nowrap; }

/* --- the station grows to five modes (2026-09-21) --------------------- */
/* Entnehmen and Wegwerfen keep the two loud colours: one is what the
   station does all day, the other is the one that must never be on by
   accident. The three in between move a bag without ending it, and share
   a calmer fill so the eye still finds the two that matter. */
.station-modes {
    grid-template-columns: repeat(auto-fit, minmax(135px, 1fr));
}

.station-mode { position: relative; min-height: 64px; font-size: 1.1rem; }

.station-mode-store.is-on,
.station-mode-thaw.is-on {
    background: var(--terracotta);
    border-color: var(--terracotta);
    color: #ffffff;
}

/* Cold, not a brand colour: status, used once. */
.station-mode-freeze.is-on {
    background: #1d4e6b;
    border-color: #1d4e6b;
    color: #ffffff;
}

/* How many bags are waiting for a shelf — on the button that deals with
   them, so an arrived delivery is visible from any mode. */
.station-mode-count {
    position: absolute;
    top: 6px;
    right: 8px;
    min-width: 22px;
    padding: 1px 6px;
    border-radius: 999px;
    background: var(--urgent-fill);
    color: #ffffff;
    font-size: 0.75rem;
    font-weight: 700;
    text-align: center;
}

.station-did-store,
.station-did-thaw   { border-left: 6px solid var(--terracotta); }
.station-did-freeze { border-left: 6px solid #1d4e6b; }

/* THE OLDER-BAG BLOCK. Amber, not red: nothing has gone wrong and
   nothing has happened yet — it is a question, and both answers are
   allowed. */
.station-fifo {
    border: 2px solid var(--attention-fill);
    border-left-width: 6px;
}

.station-fifo-older {
    margin: 0 0 6px;
    font-size: 1.1rem;
    font-weight: 700;
}

/* --- deliveries on trolleys (2026-09-21) ------------------------------ */
/* The packing page borrows the station's big buttons for its trolleys:
   the same iPad, the same arm's length. */
.dlv-add-trolley { display: contents; }
.dlv-add-trolley .station-mode { width: 100%; cursor: pointer; font: inherit; font-weight: 700; font-size: 1.1rem; }

/* A trolley's bag count is information, not an alarm. */
.station-mode-count.dlv-count { background: rgba(0, 0, 0, 0.35); }

.dlv-fold {
    border-bottom: 1px solid var(--border);
    padding: 8px 0;
}

.dlv-fold summary {
    min-height: var(--touch-min);
    display: flex;
    align-items: center;
    gap: 8px;
    cursor: pointer;
}

.dlv-print {
    display: flex;
    align-items: center;
    gap: 10px;
    flex-wrap: wrap;
    margin: 12px 0 0;
}

.dlv-lines { list-style: none; margin: 0; padding: 0; }

.dlv-line {
    padding: 10px 0;
    border-bottom: 1px solid var(--border);
}

/* SHORT IS AMBER DOWN THE EDGE, the same amber as the older-bag question:
   something to know about, not something that has gone wrong. */
.dlv-line.is-short {
    padding-left: 10px;
    border-left: 4px solid var(--attention-fill);
}

.dlv-line-head {
    display: flex;
    justify-content: space-between;
    align-items: baseline;
    gap: 12px;
}

.dlv-line-num { white-space: nowrap; font-variant-numeric: tabular-nums; font-weight: 700; }
.dlv-line-num em { font-weight: 400; font-style: normal; margin-left: 6px; }

.dlv-why { margin: 4px 0 0; font-weight: 600; }

.dlv-line-form,
.dlv-plan-add {
    display: flex;
    flex-wrap: wrap;
    gap: 8px;
    margin-top: 8px;
    align-items: center;
}

.dlv-line-form select,
.dlv-line-form input[name="note"],
.dlv-plan-add select { flex: 1 1 10rem; min-width: 0; }

.dlv-qty { flex: 0 0 6.5rem; width: 6.5rem; }

.pill-dlv-packing  { background: var(--bg-input); border: 1px solid var(--border); }
.pill-dlv-shipped  { background: var(--attention-fill); color: var(--on-light); }
.pill-dlv-received { background: var(--moosgruen); color: #ffffff; }

/* Which start the ruler measured — two plain choices, no default. */
.cal-start { border: 0; margin: 0 0 12px; padding: 0; }
.cal-start legend { padding: 0; margin: 0 0 6px; font-weight: 600; }

/* --- the waiting room, as crates (2026-09-21) ------------------------- */
/* One line per article, one button that says where it will put them. The
   button is the full-strength one on purpose: on this screen it IS the
   action, and the scan field underneath is the fallback. */
.station-groups { list-style: none; margin: 0 0 8px; padding: 0; }

.station-group {
    padding: 12px 0;
    border-bottom: 1px solid var(--border);
}

.station-group-head {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 10px;
}

.station-group-head .station-recent-what { flex: 1 1 12rem; font-size: 1.05rem; }

.station-group-btn {
    min-height: 52px;
    padding: 0 18px;
    white-space: nowrap;
}

/* On a phone the button takes the whole width under the name, so it is a
   thumb's target rather than something squeezed beside two lines of text. */
@media (max-width: 560px) {
    .station-group-head form { flex: 1 1 100%; }
    .station-group-btn { width: 100%; }
}

.station-group .dlv-fold { border-bottom: 0; padding: 4px 0 0; }

/* --- deliveries on the road, as cards --------------------------------- */
.rcv-cards { list-style: none; margin: 0; padding: 0; display: grid; gap: 12px; }

.rcv-card {
    display: grid;
    gap: 4px;
    padding: 18px 18px 16px;
    border: 1px solid var(--border);
    border-left: 6px solid var(--attention-fill);
    border-radius: var(--radius);
    background: var(--bg-raised);
    color: var(--text);
    text-decoration: none;
}

.rcv-card strong { font-size: 1.4rem; line-height: 1.2; }
.rcv-card span { color: var(--text-muted); }
.rcv-card .rcv-card-go { margin-top: 8px; color: var(--pink); font-weight: 700; }

/* The crate's button sits close under its name: a .btn brings a top margin
   of its own, which on a phone read as the button belonging to the NEXT
   crate rather than this one. */
.station-group-head { row-gap: 6px; }
.station-group-head form { margin: 0; }
.station-group-btn { margin: 0; }

/* Wareneingang: the ordinary day is a summary and one button. */
.rcv-summary { margin: 0 0 10px; font-size: 1.15rem; font-weight: 700; }
.rcv-go { min-height: 64px; font-size: 1.15rem; }
.rcv-missing { margin-top: 14px; border-bottom: 0; }

/* --- "Was wird gezählt?" — fifty-eight ticks, one save ----------------- */
.crit-list {
    list-style: none;
    margin: 0;
    padding: 0;
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(17rem, 1fr));
    gap: 2px 18px;
}

.crit-list .fill-toggle { margin: 0; padding: 8px 0; }

/* The save button stays in reach however long the list is. */
.crit-save {
    position: sticky;
    bottom: 0;
    padding: 12px 0;
    background: var(--bg);
    border-top: 1px solid var(--border);
    margin-top: 16px;
}

/* --- Nachschub: "Brauchen mehr" / "Haben zu viel" ----------------------- */
/* One article = one fold; the summary is the whole row until it is opened. */
.sup-row summary { flex-wrap: wrap; }
.sup-name { flex: 1 1 12rem; }

.sup-form {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 8px;
    margin: 8px 0 4px;
}

.sup-form .sup-amount { flex: 0 0 7rem; width: 7rem; min-height: var(--touch-min); font-size: 1.1rem; }
.sup-form .sup-note { flex: 1 1 12rem; min-height: var(--touch-min); }
.sup-btn { margin: 0; }
.sup-explain { flex: 1 1 14rem; margin: 0; }

/* The owner's own knob sits apart from the two the house uses. */
.sup-form-admin { border-top: 1px dashed var(--border); padding-top: 8px; }

.sup-asked { display: flex; align-items: center; justify-content: space-between; gap: 12px; flex-wrap: wrap; }
.sup-asked form { margin: 0; }
.sup-asked .btn { margin: 0; }
.sup-counted { margin-top: 18px; }

@media (max-width: 560px) {
    .sup-form .sup-btn { flex: 1 1 100%; }
}

/* --- a to-do is one press (2026-09-22) ---------------------------------- */
.todo-done { margin: 12px 0 4px; }
.todo-done .todo-done-btn { width: 100%; min-height: 64px; font-size: 1.15rem; margin: 0; }
.todo-done .btn-quiet { margin: 6px 0 0; }
.todo-state { margin: 8px 0 0; font-weight: 600; }

/* "Erledigt · Rückgängig" on the list it came back to. */
.work-done-flash { display: flex; flex-wrap: wrap; align-items: center; gap: 4px 16px; }
.work-done-flash form { margin: 0; }
.work-done-flash .link-button { font-weight: 700; }

/* The comment count sits with the other pills; the creation date is now
   the last and quietest thing on the line. */
.work-row-comments { font-variant-numeric: tabular-nums; }
.work-row-created { display: inline; min-width: 0; font-size: 0.9em; opacity: 0.8; }

/* A channel's Aufgaben tab draws the same rows as the page (2026-09-22). */
.work-done-fold { margin-top: 18px; }
.work-done-fold .work-list { margin-top: 4px; }
.todo-add .btn + .btn { margin-left: 8px; }
.work-row-source { display: inline-block; padding: 0 4px 8px; font-size: 0.85rem; }

/* --- the Aufgaben controls in one row, sorting prominent (2026-09-22) --- */
.work-quick-filter { display: flex; flex-wrap: wrap; gap: 8px; align-items: center; margin: 12px 0 4px; }
.work-quick-filter select { flex: 1 1 9rem; min-width: 0; min-height: var(--touch-min); }
.work-quick-filter .btn { margin: 0; width: auto; display: inline-flex; }
.work-sort-switch { margin: 6px 0 4px; }

/* Wieder öffnen on a done row. */
.work-row-reopen { margin: 0; padding: 0 4px 8px; }
.work-row-reopen .btn { margin: 0; width: auto; display: inline-flex; }

/* A colour per channel: the row's left edge and a dot before its name.
   Ten tones, by channel id, the same on every screen. */
.work-row { border-left: 4px solid transparent; }
.work-row > a { padding-left: 10px; }
.ch-dot { display: inline-block; width: 8px; height: 8px; border-radius: 50%; margin-right: 4px; vertical-align: middle; background: var(--border); }
.work-row.ch-0 { border-left-color: #7a9e7e; } .work-row.ch-0 .ch-dot { background: #7a9e7e; }
.work-row.ch-1 { border-left-color: #c98b5a; } .work-row.ch-1 .ch-dot { background: #c98b5a; }
.work-row.ch-2 { border-left-color: #6f8fb8; } .work-row.ch-2 .ch-dot { background: #6f8fb8; }
.work-row.ch-3 { border-left-color: #b86f8f; } .work-row.ch-3 .ch-dot { background: #b86f8f; }
.work-row.ch-4 { border-left-color: #8f7fb8; } .work-row.ch-4 .ch-dot { background: #8f7fb8; }
.work-row.ch-5 { border-left-color: #5fa8a0; } .work-row.ch-5 .ch-dot { background: #5fa8a0; }
.work-row.ch-6 { border-left-color: #b8a15f; } .work-row.ch-6 .ch-dot { background: #b8a15f; }
.work-row.ch-7 { border-left-color: #d07070; } .work-row.ch-7 .ch-dot { background: #d07070; }
.work-row.ch-8 { border-left-color: #7f9fb5; } .work-row.ch-8 .ch-dot { background: #7f9fb5; }
.work-row.ch-9 { border-left-color: #9c8a6d; } .work-row.ch-9 .ch-dot { background: #9c8a6d; }

/* The task itself at the top of its page (2026-09-22). */
.task-head {
    margin: 4px 0 6px;
    padding: 2px 0 2px 12px;
    border-left: 4px solid var(--border);
    font-size: 1.35rem;
    line-height: 1.3;
    font-weight: 700;
    text-transform: none;
    letter-spacing: normal;
    overflow-wrap: anywhere;
}
.task-head-where { display: block; margin-top: 4px; font-size: 0.85rem; font-weight: 400; }
.task-head.ch-0 { border-left-color: #7a9e7e; } .task-head.ch-0 .ch-dot { background: #7a9e7e; }
.task-head.ch-1 { border-left-color: #c98b5a; } .task-head.ch-1 .ch-dot { background: #c98b5a; }
.task-head.ch-2 { border-left-color: #6f8fb8; } .task-head.ch-2 .ch-dot { background: #6f8fb8; }
.task-head.ch-3 { border-left-color: #b86f8f; } .task-head.ch-3 .ch-dot { background: #b86f8f; }
.task-head.ch-4 { border-left-color: #8f7fb8; } .task-head.ch-4 .ch-dot { background: #8f7fb8; }
.task-head.ch-5 { border-left-color: #5fa8a0; } .task-head.ch-5 .ch-dot { background: #5fa8a0; }
.task-head.ch-6 { border-left-color: #b8a15f; } .task-head.ch-6 .ch-dot { background: #b8a15f; }
.task-head.ch-7 { border-left-color: #d07070; } .task-head.ch-7 .ch-dot { background: #d07070; }
.task-head.ch-8 { border-left-color: #7f9fb5; } .task-head.ch-8 .ch-dot { background: #7f9fb5; }
.task-head.ch-9 { border-left-color: #9c8a6d; } .task-head.ch-9 .ch-dot { background: #9c8a6d; }
/* Plain words, not a second top bar: the body font, in its own case. */
.task-head { font-family: Montserrat, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; font-size: 1.25rem; }
.task-head-where { text-transform: none; letter-spacing: normal; }

/* The people picker's search box (2026-09-22). Sticky, so it stays in
   reach at the top of a long list. */
.people-search {
    position: sticky;
    top: 0;
    z-index: 1;
    width: 100%;
    min-height: var(--touch-min);
    margin: 4px 0 8px;
}
.people-fold-list .check[hidden] { display: none; }
.people-none { margin: 6px 0; }

/* AND THE SAME BOX OVER THE OTHER TWO LISTS OF NAMES (2026-09-27):
   starting a conversation, and naming individuals on an urgent request.
   A .tag-check is display:flex, which beats the browser's own rule for
   [hidden] — so a row the search filtered out would stay on screen. */
.tag-check[hidden] { display: none; }

/* A task's deadline, drawn as the calendar draws a date (2026-09-22). */
input.date-field {
    width: 100%;
    max-width: 14rem;
    min-height: var(--touch-min);
    padding: 10px 12px;
    background: var(--bg-input);
    border: 1px solid var(--border);
    border-radius: 8px;
    color: var(--text);
    font: inherit;
    font-size: 16px;
    font-variant-numeric: tabular-nums;
}
:root:not([data-theme="light"]) input.date-field::-webkit-calendar-picker-indicator { filter: invert(1); opacity: 0.75; cursor: pointer; }
:root[data-theme="light"] input.date-field::-webkit-calendar-picker-indicator { filter: none; }
@media (prefers-color-scheme: light) {
    :root:not([data-theme="dark"]) input.date-field::-webkit-calendar-picker-indicator { filter: none; }
}

/* The people search, round like the other search fields (2026-09-22). */
.people-search {
    padding: 10px 16px;
    background: var(--bg-input);
    border: 1px solid var(--border);
    border-radius: 999px;
    color: var(--text);
    font: inherit;
    font-size: 16px;
    -webkit-appearance: none;
    appearance: none;
}
.people-search:focus { outline: none; border-color: var(--terracotta); }

/* Gerade erledigt, and newest/oldest (2026-09-22). */
.work-recent-done { margin: 10px 0 14px; }
.work-recent-done .work-list { margin-top: 4px; }
.work-sort-dir { display: flex; flex-wrap: wrap; gap: 6px; margin: 2px 0 8px; }
.work-sort-dir .chip { text-decoration: none; }

/* Abholung on an order (2026-09-22). */
.ord-pickup p { margin: 0; }
.ord-pickup .dlv-fold { margin-top: 8px; border-bottom: 0; }
.ord-pickup-form .btn { margin-top: 8px; }

/* --- the key board, pinned at the top of the Schlüssel channel (2026-09-22) --- */
.key-board { margin: 8px 0 18px; }
.key-houses {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(17rem, 1fr));
    gap: 12px;
}
.key-house {
    padding: 12px 14px;
    border: 1px solid var(--border);
    border-radius: var(--radius);
    background: var(--bg-raised);
}
/* THE HOUSE IS THE HEADING, and has to look like one (2026-09-23): "it
   also seems the key listings are larger then the name of the restaurant
   in the header". It was 1.05rem in a condensed face against key labels
   set bold in the body face, so the labels won. Back to the h3 size the
   rest of the app uses, and the labels step down — see .key-label. */
.key-house-name { margin: 0 0 6px; font-size: 1.3rem; }
.key-list { list-style: none; margin: 0; padding: 0; }
.key-row { padding: 8px 0; border-top: 1px solid var(--border); }
.key-row:first-child { border-top: 0; }
.key-row.is-mine { background: color-mix(in srgb, var(--moosgruen) 10%, transparent); margin: 0 -8px; padding: 8px; border-radius: 6px; }
.key-line { display: flex; flex-wrap: wrap; justify-content: space-between; gap: 2px 10px; }
/* Below the house, not above it — 700 at full body size read as the
   loudest thing in the box. */
.key-label { font-weight: 600; font-size: 0.95rem; }
.key-pending { margin: 2px 0 0; font-size: 0.85rem; color: var(--attention-fill, var(--text)); }
.key-actions { display: flex; flex-wrap: wrap; align-items: center; gap: 6px; margin-top: 6px; }
.key-actions:empty { display: none; }
.key-inline { margin: 0; }
.key-inline .btn, .key-give .btn, .key-add .btn { margin: 0; width: auto; display: inline-flex; }
.key-fold { margin: 0; }
.key-fold > summary { cursor: pointer; font-size: 0.9rem; min-height: 32px; display: inline-flex; align-items: center; }
.key-give, .key-add { display: flex; flex-wrap: wrap; gap: 6px; align-items: center; margin: 6px 0 0; }
.key-give select, .key-give input[type="text"], .key-add input[type="text"] { flex: 1 1 10rem; min-width: 0; min-height: 38px; }
.key-ask, .key-places { margin-top: 12px; padding: 10px 14px; border: 1px solid var(--border); border-radius: var(--radius); }
.key-ask-form label { display: block; margin-top: 10px; }
.key-ask-form select, .key-ask-form input[type="text"] { width: 100%; }
.key-ask-form .btn { margin-top: 12px; }
.key-none { margin: 4px 0; }

/* A handover or request as a card in the channel. */
.key-card {
    margin: 6px 0 2px;
    padding: 10px 12px;
    border: 1px solid var(--border);
    border-left: 4px solid var(--border);
    border-radius: 8px;
    background: var(--bg-raised);
}
.key-card.is-waiting { border-left-color: var(--attention-fill, #c98b5a); }
.key-card-line { margin: 0; }
.key-card-state { margin: 6px 0 0; }
/* The folds read as buttons, not as stray words. */
.key-actions .key-fold > summary,
.key-add-fold > summary {
    list-style: none;
    padding: 4px 12px;
    border: 1px solid var(--border);
    border-radius: 999px;
    background: var(--bg-input);
}
.key-actions .key-fold > summary::-webkit-details-marker,
.key-add-fold > summary::-webkit-details-marker { display: none; }
.key-actions .key-fold[open] { flex: 1 1 100%; }
.key-add-fold { margin-top: 8px; }

/* --- the key board, tidied (2026-09-22) -------------------------------- */
.key-icon { display: inline-block; width: 26px; text-align: center; }
.key-icon-card { vertical-align: -3px; color: var(--text); }
.key-label { display: inline-flex; align-items: center; gap: 6px; }

/* One row: the key and its holder on the left, its buttons on the right;
   an open fold takes the whole width underneath. */
.key-row {
    display: grid;
    grid-template-columns: minmax(0, 1fr) auto;
    column-gap: 10px;
    align-items: start;
}
.key-row > .key-line, .key-row > .key-pending { grid-column: 1; }
.key-row > .key-actions { grid-column: 2; grid-row: 1 / span 2; justify-content: flex-end; margin-top: 0; }
.key-row:has(.key-fold[open]) > .key-actions { grid-column: 1 / -1; grid-row: auto; justify-content: flex-start; margin-top: 8px; }
.key-line { flex-direction: column; justify-content: flex-start; }
.key-holder { font-size: 0.95rem; }

.key-panel {
    display: grid;
    gap: 8px;
    margin-top: 8px;
    padding: 10px;
    border: 1px solid var(--border);
    border-radius: 8px;
    background: var(--bg);
}
.key-panel-row { display: flex; flex-wrap: wrap; align-items: center; gap: 6px; margin: 0; }
.key-panel-row select, .key-panel-row input[type="text"] { flex: 1 1 10rem; min-width: 0; min-height: 38px; }
.key-panel-row .btn { margin: 0; width: auto; }
.key-panel-label { flex: 1 1 100%; font-size: 0.85rem; color: var(--text-muted); }
.key-panel-foot { display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between; gap: 8px; }
.key-panel-foot .key-inline, .key-panel-foot .btn { margin: 0; }
.key-cancel { margin-left: auto; }
.key-panel .key-give { margin: 0; }

/* Managing a key: the house's "+ Schlüssel" steps aside. */
.key-house:has(.key-admin[open]) .key-add-fold { display: none; }

@media (max-width: 560px) {
    .key-row > .key-actions { grid-column: 1 / -1; grid-row: auto; justify-content: flex-start; margin-top: 6px; }
}

/* Desktop: never more than two houses side by side, so each box has room
   for a key, its holder and its buttons on one line. A box narrower than
   that (a phone, a narrow window) puts the buttons under the key. */
.key-houses { grid-template-columns: repeat(auto-fill, minmax(24rem, 1fr)); }
@media (max-width: 640px) { .key-houses { grid-template-columns: 1fr; } }
.key-house { container-type: inline-size; }
@container (max-width: 25rem) {
    .key-row > .key-actions { grid-column: 1 / -1; grid-row: auto; justify-content: flex-start; margin-top: 6px; }
}
/* The key's name keeps its room; three buttons wrap rather than squeeze it. */
.key-row { grid-template-columns: minmax(11rem, 1fr) auto; }
.key-row > .key-actions { max-width: 17rem; }
.key-actions .key-inline .btn-small,
.key-actions .key-fold > summary {
    min-height: 32px;
    padding: 4px 12px;
    border: 1px solid var(--border);
    border-radius: 999px;
    background: var(--bg-input);
    color: var(--text);
    font-size: 0.9rem;
    font-weight: 500;
    line-height: 1.2;
}
.key-actions .key-inline .btn-confirm { background: var(--moosgruen); border-color: var(--moosgruen); color: #fff; }
/* …but an open fold takes the whole width of the box. */
.key-row:has(.key-fold[open]) > .key-actions { max-width: none; }
.key-actions .key-fold[open] > .key-panel { max-width: 32rem; }
.key-actions .key-inline .btn-danger { color: var(--urgent, #c0392b); border-color: currentColor; background: transparent; }

/* Aufgaben: the search (2026-09-22), round like the other search fields.
   Verwaltung → Team borrows the same box (2026-09-23) rather than grow a
   second kind of search field: it is the same gesture on a longer list. */
.work-search,
.admin-search { position: relative; display: flex; margin: 12px 0 0; }
.admin-search { gap: 8px; margin-bottom: 14px; }
/* Shown only where there is no script to filter with. */
.admin-search-go { flex: 0 0 auto; }
.admin-search-none { margin: 10px 2px; }
.work-search input[type="search"],
.admin-search input[type="search"] {
    flex: 1 1 auto;
    min-width: 0;
    min-height: 44px;
    padding: 10px 42px 10px 16px;
    background: var(--bg-input);
    border: 1px solid var(--border);
    border-radius: 999px;
    color: var(--text);
    font: inherit;
    font-size: 16px;
    -webkit-appearance: none;
    appearance: none;
}
.work-search input[type="search"]:focus,
.admin-search input[type="search"]:focus { outline: none; border-color: var(--terracotta); }
.work-search input[type="search"]::-webkit-search-cancel-button,
.admin-search input[type="search"]::-webkit-search-cancel-button { -webkit-appearance: none; appearance: none; }
.work-search-clear,
.admin-search-clear {
    position: absolute;
    right: 8px;
    top: 50%;
    transform: translateY(-50%);
    width: 30px;
    height: 30px;
    display: grid;
    place-items: center;
    border-radius: 50%;
    color: var(--text-muted);
    text-decoration: none;
    font-size: 1.3rem;
}
.work-search-clear:hover,
.admin-search-clear:hover { color: var(--text); background: var(--bg); }
/* A real button here rather than a link, so it needs the button reset. */
.admin-search-clear { padding: 0; border: 0; background: transparent; cursor: pointer; }
.admin-search-clear[hidden] { display: none; }

/* One fold per house on the key board (2026-09-22). */
.key-house > .key-house-head {
    display: flex;
    flex-wrap: wrap;
    align-items: baseline;
    justify-content: space-between;
    gap: 4px 10px;
    min-height: var(--touch-min);
    cursor: pointer;
    list-style: none;
}
.key-house > .key-house-head::-webkit-details-marker { display: none; }
.key-house > .key-house-head .key-house-name { margin: 0; }
.key-house > .key-house-head::after { content: "▾"; color: var(--text-muted); margin-left: auto; }
.key-house[open] > .key-house-head::after { content: "▴"; }
.key-house-sum { font-size: 0.85rem; }
.key-house-waits { color: var(--attention-fill, #c98b5a); }
/* AN OPEN HOUSE KEEPS ITS NAME ON SCREEN (2026-09-23): "when you unfold
   a tab it should pin to which restaurant at the top". Four houses with
   a dozen keys between them is more than a phone screen, and a list of
   key labels with the heading scrolled away is a list you cannot place.

   It sticks under the app header — --header-h-context is measured by
   app.js and has a sensible fallback without it — and inside its own
   box, so it leaves with the house it belongs to rather than pushing the
   next one's name aside. The negative margins let the bar cover the
   whole width of the card as the rows pass under it. */
.key-house[open] > .key-house-head {
    position: sticky;
    top: var(--header-h-context);
    z-index: 3;
    margin: -12px -14px 6px;
    padding: 12px 14px 6px;
    border-bottom: 1px solid var(--border);
    border-radius: var(--radius) var(--radius) 0 0;
    background: var(--bg-raised);
}

/* The key and where it is, on one line where there is room (2026-09-22). */
.key-line { flex-direction: row; align-items: baseline; gap: 4px 8px; }
.key-line .key-holder { flex: 1 1 auto; }
@container (max-width: 25rem) {
    .key-line { flex-direction: column; align-items: flex-start; }
}

/* ONE LINE PER KEY (2026-09-23). The grid gave the buttons a fixed slice
   of the row, so "Jefferson · seit 22. Sep" and "📍 Taunus Office" wrapped
   under the key even in a wide box. Now the row is one flexible line —
   key, where it is, then the buttons on the right — and only what does
   not fit moves down. */
.key-row { display: flex; flex-wrap: wrap; align-items: center; gap: 4px 10px; }
.key-row > .key-line { flex: 1 1 auto; min-width: 0; }
.key-row > .key-pending { flex: 1 1 100%; margin: 0; }
.key-row > .key-actions { margin-left: auto; margin-top: 0; max-width: none; justify-content: flex-end; }
.key-row:has(.key-fold[open]) > .key-actions { flex: 1 1 100%; justify-content: flex-start; margin-top: 4px; }
.key-line { flex-wrap: wrap; }
/* One house per row: with the folds, height is no longer the problem, and
   a full-width box is what puts a key, its holder and its buttons on one
   line (2026-09-23). */
.key-houses { grid-template-columns: 1fr; }

/* =====================================================================
   NO CONTROL SMALLER THAN 16px, ANYWHERE (2026-09-23)

   iOS Safari zooms the whole page in when you focus a field whose text
   is under 16px, and does not zoom back out — you are left pinching a
   half-width page one-handed mid-shift. It is the single most common
   iPhone-only layout bug there is, and the app had been fixing it one
   screen at a time: the composer, the login, the recipe editor and the
   Rundgang each carry their own "16px, or iOS zooms" line.

   Found by a sweep across every screen at 320–430px: the Aufgaben
   filters had been missed. "Erledigt von", "Wer" and the two date
   fields sit inside a .work-filter label set at 0.85rem and inherited
   it, so four controls on the busiest screen in the app came out at
   13.6px. Rather than patch that one label, this is the floor for all
   of them.

   max(16px, 1em), NOT a flat 16px: a control in a larger context — the
   Rundgang's number fields, which are deliberately big for a cold room
   — keeps the size it was given. em in font-size resolves against the
   parent, so this reads "whatever you inherited, but never under 16".

   LABELS ARE NOT TOUCHED. Small print beside a field is text, and text
   does not make the page jump.
   ===================================================================== */
input:not([type="checkbox"]),
textarea:not([hidden]),
select:not([hidden]) {
    font-size: max(16px, 1em);
}
/* EXACTLY ONE :not(), AND THAT IS THE POINT. It has to outrank the class
   rules that hand a control `font: inherit` — the shorthand resets the
   size along with the family, which is how the Aufgaben selects ended up
   at 13.6px — so a bare `select` would lose that argument. One :not()
   ties with a single-class rule and wins on order.

   It must also NOT outrank a rule that made a control deliberately
   bigger: .walk-line-ctl .walk-qty is two classes, and the Rundgang's
   number field is large on purpose, for a cold room. A second :not()
   here took it back down to 16px — caught by re-measuring, not by
   reading. Keep it at one. */

/* WHO MAY SEE A TASK (2026-09-24) — the same chips as the urgency above
   it, because it sits on the same form and is the same kind of answer:
   one of two, one line, no dropdown to open. The chosen one is filled
   rather than outlined, which is what tells you at a glance where an
   existing task stands when you open it to edit. */
.work-vis .vis-choices { display: flex; flex-wrap: wrap; gap: 6px; }
.vis-choice { display: inline-flex; }
.vis-choice input { position: absolute; opacity: 0; width: 1px; height: 1px; }
.vis-choice span {
    display: inline-flex;
    align-items: center;
    min-height: 36px;
    padding: 0 14px;
    border: 1px solid var(--border);
    border-radius: 999px;
    color: var(--text-muted);
    font-size: 0.88rem;
    cursor: pointer;
}
.vis-choice input:focus-visible + span { outline: 2px solid var(--text); outline-offset: 2px; }
.vis-choice input:checked + span {
    background: var(--terracotta);
    border-color: var(--terracotta);
    color: #ffffff;
    font-weight: 600;
}

/* Hidden while nobody is named — app.js decides, and without it the
   fieldset simply stays, which is the answer that applies anyway. */
.work-vis[hidden] { display: none; }

/* WHERE A KEY HAS BEEN (2026-09-24) — inside the admin's Verwalten fold,
   so it costs the board nothing. It borrows .cal-log-list, which is the
   same three columns saying the same kind of thing: when, what, who. */
.key-log-fold { margin-top: 4px; }
.key-log { margin-top: 6px; padding-top: 6px; max-height: 15rem; overflow-y: auto; }
.key-log .cal-log-what { color: var(--text); }
/* A handover nobody has confirmed, or one somebody took back — the two
   states that explain why the board says what it says. */
.key-log-state { color: var(--attention-fill, var(--text-muted)); }
