:root {
  --bg: #f5f2ec;
  --bg-1: #fbfaf7;
  --bg-2: #efece4;
  --bg-3: #e8e4da;
  /* One step darker than the value taken off the preview. At #e0dbd0 the
     border sat at 1.24:1 against the page - just under the 1.25 the tab
     test asks for, and the card leans on that border rather than on a fill
     of its own (the card is only 1.07:1 against the page). A border that
     faint means the card has no edge at all. This is 1.31:1. */
  --line: #dbd5c8;
  --text: #16150f;
  --dim: #6b6558;
  --dimmer: #8b8474;
  /* Bone white instead of Solana green. The reason is not taste: the name
     colors are blue, violet, rose and sand, and a saturated accent always
     comes too close to one of them - steel blue was even the exact color
     of .h.t0. A neutral accent collides by definition with none of the
     identity colors, and at 16.3:1 on the button it has the highest
     contrast of every tone tested (green: 12.6:1).

     So every color here has exactly one job: neutral is action, gold
     is Ansem, red is a problem, the four muted tones are people. */
  --accent: #1f4e7a;
  /* The same tone again, as a plain number triple. Reason: rgba() cannot
     take a color variable, only channels. Without this, every translucent
     spot would have to spell out the color value - and that is exactly how
     it was: "20, 241, 149" sat hardcoded in six places, and recoloring the
     page would have missed all of them. Change --accent, and this changes
     with it. */
  --accent-rgb: 31, 78, 122;
  /* The same color, muted - for filled areas rather than strokes: the
     "Send"/"Start poll" button and your own bubble in a DM. Full bone white
     was too harsh there, not because of the hue but because of the area.
     A thin line at 86% brightness is an accent; a palm-sized block of it
     is a lamp.

     Two values used to live here: 60% for the button, 44% for the bubble,
     on the reasoning that a bubble is bigger and stacks in chains. The
     direction was right, but the button has since come down to 44% itself
     - which left the distinction with nothing left to distinguish. Two
     names for the same value are worse than one: the second one reads as
     an intent that does not exist.

     Why the button followed: the page background lost its glow. Before,
     a very faint gradient lit up the area around the button; on plain
     --bg the same area sits in more darkness. The button did not get
     brighter, its surroundings got darker.

     9.2:1 for the dark text on it - well above the 4.5:1 a control needs
     to count as readable. */
  --accent-fill: #24598c;
  /* Amounts. A tone of its own, because none of the existing ones fit:
     --dim (6.4:1) was too quiet - the amount is the core of this page and
     it was ending up the palest thing on the line. --text (16.2:1) is too
     loud, and not because of the value but because of the quantity: the
     inbox stacks twenty amounts in a column, all bold and in mono. What
     looks right for one number is a tracer line down the right edge for
     twenty.

     10.8:1 sits between - clearly brighter than message text, noticeably
     under white. It would have been tempting to reuse --accent-fill, which
     sits close by; but that tone belongs to filled areas. Recolor the
     button, and the amounts should not change color by accident. */
  --worth: #2b2a22;
  /* The border of the field you're currently typing into.

     The value shares its number with --dim, but is deliberately its own
     name: one is the color of secondary text, the other a state indicator.
     Move one and the other should not move with it. (Unlike --accent-fill
     above, where two names stood for the same JOB - that was a real
     duplication, this is not.)

     Why not --accent, which the fields used way back: full bone white
     makes the border the brightest thing in the box, brighter than the
     text inside it. But all it should say is "you're typing here".

     And why not 10.8:1 anymore, which sat here for a while: the value was
     right, its surroundings then moved down three times over. The
     background lost its glow, the bright button dropped from 12.2:1 to
     9.2:1, and the page runs in mono, which builds finer than the grotesque
     did. The border ended up brighter than the button next to it with no
     change of its own - a field you're typing in should not be louder than
     the button that submits it.

     6.4:1 sits well under the button and well over the 3:1 at which a
     border reliably reads as a state indicator. Resting state sits at
     1.4:1 - the jump stays large either way. */
  --fokus: #6b6558;

  /* --anteil used to live here, the color of the small line under the
     amount: first a vote count, later a percentage. Both are gone again,
     and the color went with them. Bring back a second line and you'll need
     it again: on the leading answer it sits on the blue fill, where
     --dimmer comes out at 1.25:1 and --dim at 2.37:1 - both unreadable.
     #c8ced9 landed at 4.60:1. What must NOT come back is the vote count:
     see .opt-num further down. */

  /* The fill of the answer bars - measured off X, value for value.
     ------------------------------------------------------------------------
     From a screenshot of an X poll, median value of the area:

       leading answer      #2b5988
       every other answer  #343639
       text on top         #e8e9ea   -> --text, 1.00:1 off

     Two things worth knowing before you touch this:

     1. #343639 is a FIFTH gray next to --bg .. --line, and it is neutral,
        while the rest of the palette leans blue. That is not carelessness
        here, it's the point: --bg-3 would land at 1.22:1 on the page
        background and be barely visible as a bar; X's gray sits at 1.62:1.
        The gap between the two is 1.33:1 - big enough that an existing
        tone does not do the same job here.

     2. It is NOT the same blue as in the DMs. X itself uses two: #4b9aea
        in the message bubble, #2b5988 in the poll bar, 2.46:1 apart. A
        bubble is small and should glow, a bar is large and sits under
        text. Pull both to one value and you get either a dull bubble or a
        bar that outshines the line of text on it.

     What sits on top, measured: --text 6.0:1 and --accent 6.3:1 on the
     blue; 10.0 and 10.5:1 on the gray. The edge where the fill ends sits
     at 1.62:1 against the page background - weaker than the 1.95:1 of the
     old translucent fill. That is the price of "exactly like X", and it's
     paid on purpose: X itself lands at 1.73:1, and the rest is missing
     only because our background is #0a0b0f and not pure black. */
  /* Both values moved one step darker than the ones picked off the preview,
     and the reason is measured rather than felt. On the dark palette the
     edge where a fill ended sat at ΔL* 17.0 (plain) and 31.3 (leading)
     against the card. The first light draft landed at 6.9 and 7.2 - the
     bars were still there, but the point where one stopped had become hard
     to find, and a poll IS that point. These two sit at 11.9 and 11.1, and
     keep the order the draft had right: the leading bar stays the more
     distinct of the two, because it separates by hue as well as by
     brightness (ΔE 16.9 against the card, versus 13.8). */
  --fuellung: #ded8c8;
  --fuellung-spitze: #ccdcef;
  /* The brand mark. A reference and not a copy, so the two can never drift
     apart - but it points at the filled BUTTON now, not at the winning bar.
     See .brand-mark for what changed and why. */
  --marke: var(--accent-fill);

  /* The message bubbles in the DMs - their own small set of colors.
     ------------------------------------------------------------------------
     They sit together here instead of scattered among the rules because
     they're a group: change one and the other two have to be looked at.

     The own bubble used to be a reference to --fuellung-spitze, and the
     note under it explained at length why the bubble should carry the
     leading bar's blue. That reference did not survive the move to paper,
     and it is worth writing down why, because the reference itself was not
     the mistake - the mistake would have been keeping it.

     On the dark palette the leading bar was #2b5988, a deep blue, and near
     white on it measured 7.27:1. Filling a bubble with it was free. On
     paper the leading bar has to be PALE, because it is a surface with a
     line of text lying on top of it - #cfdff1. The reference still
     resolved, so nothing broke loudly; it just quietly turned the own
     bubble into near-white text on near-white, 1.20:1. Unreadable, and
     unreadable in the one place a person reads most.

     So the bubble now takes --accent-fill, the same filled blue as the
     primary button. That is not a second copy of a value either - the note
     at --accent-fill says that tone belongs to filled areas, and a bubble
     is a filled area. What it costs, measured against the old set:

       own bubble against the incoming one   2.21 -> 5.74:1
       near white on the own bubble          1.20 -> 6.98:1
       the timestamp in it (white at .78)    1.18 -> 5.16:1

     Both went up, so this is not a trade this time. The reason there is no
     price is that the constraint changed underneath: on a dark page the
     bubble and the bar wanted the same blue, on a light one they want
     opposite ends of it, because only one of them has text lying on it. */
  --dm-eigen: var(--accent-fill);
  --dm-eigen-text: #fbfaf7;
  --dm-fremd: var(--bg-3);
  --dm-fremd-text: var(--text);
  --dm-zitat-text: var(--dim);
  --accent-2: #1f4e7a;
  --accent-2-rgb: 31, 78, 122;
  /* The dot next to the amount when a conversation is unread.
     ------------------------------------------------------------------------
     --accent-fill, the same blue as the buttons and the mark. Not a value of
     its own: the note at --accent-fill says that tone belongs to filled
     areas, and a 6px dot is nothing but filled area.

     It was an amber before, left over from the palette where blue was not
     available for it. 6.98:1 on the list background - a dot that small has
     to carry on colour alone, it has no shape to be recognised by.

     Not --fuellung-spitze: that one is 2.57:1 there, and at six pixels that
     is not a mark, it is a smudge. */
  --ungelesen: var(--accent-fill);
  --warn: #a83228;
  /* The text on a red border - one step lighter than the border itself, so
     the message stays readable without the box turning into a wall of red. */
  --warn-text: #7a1a15;
  --gold: #8a5a12;
  /* The same tone again as channels, for the same reason as --accent-rgb:
     the preview banner lays the gold over the page at 12% and draws its
     border at 45%, and rgba() cannot take a colour variable. */
  --gold-rgb: 138, 90, 18;

  /* Surfaces that are not a flat --bg-N
     ------------------------------------------------------------------------
     Three places let the background show through: the login card, the top
     bar, and the reply button under the pointer. They are translucent on
     purpose - two of them have a blur behind them - which is why they
     cannot simply be --bg-1.

     They used to sit hardcoded in the rule that used them, and the cost of
     that only showed when the palette was moved for a trial run: the ground
     changed and these three stayed where they were, because nothing
     connected them to it. Same story for the shadows and the four name
     tones below. Everything that carries a colour has a name here now, so
     that changing the page's colour is a change to this block and nothing
     else. */
  --karte-grund: rgba(251, 250, 247, .88);
  --topbar-grund: rgba(245, 242, 236, .90);
  /* A lift, not a colour: whatever this sits on gets one step brighter. */
  --auflage: rgba(22, 21, 15, .07);
  /* The border of the row you just copied. Brief, clearly stronger than
     --line, but it must not glow - see .pay-row.is-copied. */
  --betont-rand: rgba(31, 78, 122, .55);
  /* Two shadows, and they are not interchangeable: the first belongs to
     things that hover ABOVE the page (the timing list, the tooltip), the
     second to the toast, which is flatter and wider. */
  /* Die Farbe, mit der diese Seite abdunkelt: warmes Braungrau statt
     Schwarz, weil ein schwarzer Schleier auf Papier grau statt dunkel
     wirkt. Ein Wert, drei Verwendungen - die beiden Schatten und der
     Schleier hinter dem Dialog. */
  --schatten-rgb: 60, 52, 36;
  --schatten-hoch: 0 8px 20px rgba(var(--schatten-rgb), .12);
  --schatten-toast: 0 6px 18px rgba(var(--schatten-rgb), .14);
  /* What sits ON the filled button: the page ground, so the label is a
     cut-out rather than a second colour. Its own name because it is a job,
     not a coincidence - the day the button stops being light-on-dark, this
     line moves and --bg does not. */
  --knopf-text: var(--bg);

  /* The four people tones.
     ------------------------------------------------------------------------
     See .h.t0 further down for what they are and why there are four. The
     VALUES live here because they belong to the palette; the rules down
     there only hand them out. */
  --name-0: #8a5a12;   /* amber  */
  --name-1: #5b3fa0;   /* violet */
  --name-2: #a01f4a;   /* rose   */
  --name-3: #1f6b3f;   /* green  */
  /* The tint on an unread row: --name-0 at 11.5%. Not a fifth background,
     a wash - the row keeps its shape and only says "there is something
     here". */
  /* Die leicht getoente Zeile hinter einer ungelesenen Nachricht. Dieselben
     Kanaele wie --accent-fill (#24598c) - als Literal, weil es dafuer keinen
     -rgb-Namen gibt. Ein blauer Punkt auf einer bernsteinfarben getoenten
     Zeile saehe aus wie ein vergessener Wert; sichtbar ist der Unterschied
     kaum (1.13 gegen 1.14:1 zur Liste), stimmig schon. */
  --ungelesen-fuellung: rgba(36, 89, 140, .09);
  /* The timestamp inside your own bubble: the bubble's text colour, turned
     down. Its own entry because on a lighter bubble it would have to be
     turned down the other way. */
  --dm-eigen-zeit: rgba(255, 255, 255, .78);
  /* The text column in the inbox - every preview reaches this far, and
     stops there too. See .thread-prev. */
  --vorschau: 20ch;
  /* "You:" - four cells, plus the gap after it. Both as variables, because
     the preview width next to it is calculated by subtracting these: change
     the word or the gap without changing the math, and the edge frays again. */
  --du-breit: 4ch;
  --du-abstand: -.3rem;
  /* The corners.
     ------------------------------------------------------------------------
     Forty numbers used to sit in this file - 4, 5, 6, 8, 9, 10, 12, 16, 18 -
     each decided on its own, and no way to change the page's shape without
     going through all forty. They are three names now:

       --radius        containers: cards, panels, fields, buttons
       --radius-klein  small parts: chips, tags, scrollbar thumbs
       --radius-innen  the inner edge of a container that carries a border,
                       one pixel tighter than --radius. Its own name because
                       subtracting a pixel from zero goes negative, and a
                       negative radius is not zero - it is invalid, and the
                       browser silently drops the whole declaration.

     All three sit at 0: the page is right-angled. To get the rounding back,
     these three lines are the entire change - 12px, 6px, 11px.

     What is NOT in here, on purpose: 50% and 999px. A circle and a pill are
     shapes in their own right, not corners on a box - the loading spinner,
     the profile picture, the 6px unread dot, the preview banner. Round them
     down with everything else and you get a square dot, which does not read
     as a quieter dot, it reads as a bug. */
  /* The horizontal padding of a ghost button. It has a name because a rule
     further down pulls such a button back by exactly this amount, and two
     copies of the same number would drift apart the first time one of them
     is touched. */
  --knopf-luft: .7rem;
  --radius: 0;
  --radius-klein: 0;
  --radius-innen: 0;
  /* The only font on the page - and since the poll card followed suit,
     the only one in the images too. See the reasoning at font-family
     below.

     IBM Plex Mono ships with the site now; ui-monospace stays behind it as
     the fallback for the moment before the file has loaded, and for anyone
     who blocks web fonts.

     Why a file at all, when ui-monospace costs nothing: because it is not
     one font. It resolves to SF Mono on a Mac and an iPhone, to Roboto Mono
     on Android and to Consolas on Windows - three different faces with
     three different widths for the same amount. On a page whose content is
     a column of figures, that is not a detail: the column is only a column
     because every digit is the same width, and "the same width" was true
     per device, not across them. The image posted to X was drawn in
     whatever the poster's machine happened to have.

     The price is honest and small: four files, 59 KB in total, fetched once
     and then cached. No CSP change - font-src 'self' was already set, which
     is also why the files have to live here and cannot come from Google.
     The licence sits next to them in public/fonts/, because the OFL
     requires it to travel with the font. */
  --mono: 'IBM Plex Mono', ui-monospace, "SF Mono", Menlo, Consolas, monospace;
}

/* ---------------------------------------------------------------------------
   The font files
   ---------------------------------------------------------------------------
   Four weights, because the page asks for five - 400, 500, 600, 650, 700 -
   and 650 lands on the 700 cut by the CSS matching rules. Anything above
   500 looks upward first, so a separate 650 file would never be chosen.

   font-display: swap, not block. Block hides all text until the file is
   there, and on this page that would mean an empty payment screen with an
   amount somebody is waiting for. Swap shows the fallback first and
   exchanges it - the text reflows a little, which is the smaller of the two
   evils. The 400 weight is preloaded in index.html, so the swap usually
   happens before the first frame.
   --------------------------------------------------------------------------- */
@font-face {
  font-family: 'IBM Plex Mono';
  src: url('fonts/ibm-plex-mono-400.woff2') format('woff2');
  font-weight: 400; font-style: normal; font-display: swap;
}
@font-face {
  font-family: 'IBM Plex Mono';
  src: url('fonts/ibm-plex-mono-500.woff2') format('woff2');
  font-weight: 500; font-style: normal; font-display: swap;
}
@font-face {
  font-family: 'IBM Plex Mono';
  src: url('fonts/ibm-plex-mono-600.woff2') format('woff2');
  font-weight: 600; font-style: normal; font-display: swap;
}
@font-face {
  font-family: 'IBM Plex Mono';
  src: url('fonts/ibm-plex-mono-700.woff2') format('woff2');
  font-weight: 700; font-style: normal; font-display: swap;
}

* { box-sizing: border-box; }
[hidden] { display: none !important; }

html, body {
  margin: 0;
  background: var(--bg); color: var(--text);
  /* The whole page runs in the typewriter font, not just the numbers. The
     reason is the subject matter: every name here has an amount hanging
     off it, and mono is the font that lines numbers up underneath each
     other. A page that's half grotesque and half mono has two voices;
     this one has one.

     --sans is therefore removed with no replacement. It survived a while
     because the X poll card pulled it from here via cssWert('--sans') and
     wrote the question and answers onto the canvas with it - the card was
     the last surface still in grotesque. Since it followed suit, nobody
     uses it anymore, and a variable standing around only for the way back
     is exactly the kind of dead entry someone later mistakes for intent.

     To go back: change this line here and the font line in zeichnePoll()
     in app.js - and then bump KARTEN_VERSION in app.js AND in
     supabase/functions/og/index.ts, or every card already generated keeps
     its old look.

     The sizes stay as they are, and that's measured, not assumed. Mono
     builds about 19% wider than the grotesque, but exactly the same
     HEIGHT - the x-height of both fonts sits at the same 7px at a 15px
     base size. Readability depends on height, not width. Turn the size
     down "to compensate" and you actually make the text smaller while
     offsetting something that was never a problem. Lines now wrap
     earlier, nothing else happens. */
  font-family: var(--mono); font-size: 15px; line-height: 1.5;
  -webkit-font-smoothing: antialiased;
}

/* On a phone, 100% or 100vh is the wrong height: the browser's address bar
   slides in and out while scrolling, and the page stays at its old height
   regardless. The input field then slides under the edge of the screen.
   dvh follows the height that's actually visible; the 100% before it is
   the fallback for older browsers. */
/* app.js sets --sicht, and only while the keyboard is up.
   ---------------------------------------------------------------------------
   What was found on the device: type into a DM on a phone, and iOS shoves
   the WHOLE view upward to make room for the keyboard - header, tabs and
   the top edge of the conversation frame all scroll out of view with it.

   The reason is that 100dvh knows nothing about the keyboard. dvh follows
   the browser's own bars, not the keyboard; the page stays as tall as the
   whole screen while the bottom half of it sits hidden. Since the input
   field would then sit under the keyboard, iOS shoves everything up on
   its own. It is correcting a problem it sees itself - just in the wrong
   place.

   The right place is the height. If the page is exactly as tall as what's
   visible, iOS has nothing left to shove. And because .app is a flex
   stack where only the list has flex: 1, the shrink gets absorbed right
   there: header, tabs and frame stay put, and the conversation gets
   shorter. That is exactly what was wanted.

   When --sicht isn't set (keyboard down, or no visualViewport in the
   browser), the fallback behind it applies - everything as before. */
html, body { height: var(--sicht, 100%); }
@supports (height: 100dvh) {
  html, body { height: var(--sicht, 100dvh); }
}

/* The page itself does NOT scroll - scrolling only happens in the lists
   inside it.
   ---------------------------------------------------------------------------
   overflow-x: hidden with overscroll-behavior-y: none used to sit here.
   That stops the page from rubber-banding at its own end, but not the
   case this is actually about: swipe to the end inside the DM window or
   the polls, and from that point the swipe gets handed off to the PAGE -
   and the whole view slides up or down. On the home screen it looks like
   the app is coming loose from the edge.

   Two lines against it, and both are needed: overflow: hidden takes away
   the page's ability to scroll at all, overscroll-behavior: contain on
   the lists (further down at .scroll) blocks the hand-off. The first alone
   doesn't help, because the hand-off still tugs at the edge; the second
   alone doesn't either, because an over-tall page keeps scrolling.

   Said plainly: overscroll-behavior: none here is NOT provable from this
   end. It targets Safari's rubber-band on iPhone - the whole page can be
   dragged a bit past its edge and snaps back - and the Chromium the tests
   run on doesn't show that. A control test (pull the line, run the test)
   therefore proves nothing. It stays anyway: it's the documented fix for
   exactly the bug seen on the device, and it costs nothing. It can only
   be verified on a real iPhone. */
html, body { overflow: hidden; overscroll-behavior: none; }

/* The gray flash box on tap reads as a bug on a dark background. The
   visible feedback instead comes from :active. */
* { -webkit-tap-highlight-color: transparent; }

/* No more gradient in the background. Two soft lights used to sit here -
   violet top right, bone white bottom left.

   Two reasons. The violet was the last trace of Solana purple on the
   page; it's long gone from the poll cards, and one lone leftover of a
   dropped color isn't a decision, it's a residue.

   And it was barely ever visible anyway: the page's panels are 1180px
   wide and on desktop cover exactly the spot where the lights sit.
   Checked against a comparison screenshot - "with" and "without" were
   hard to tell apart. What you can't see doesn't need loading or
   maintaining.

   The background is now whatever --bg says, and nothing else. */

button, input { font: inherit; color: inherit; }
code { font-family: var(--mono); }

/* ---------------- Focus ----------------
   The ring the browser draws on its own is blue - and not as anyone's
   decision, but because Chrome takes the operating system's accent color.
   On macOS that's blue by default, on Windows something else, and the
   user can change it. A page that doesn't claim this spot itself ends up
   with a color nobody chose, one that changes from machine to machine. On
   this near-black background it was also the loudest point on the whole
   page.

   Hence --fokus, the same tone a text field uses to brighten its border
   while typing. A button has no border to brighten - so the outline has
   to stay here, but at least it speaks the same language.

   :focus-visible instead of :focus is the whole trick: the browser then
   only shows the ring when it was reached via keyboard. Click, and you
   already know where you clicked, so no ring; tab through the page with
   the keyboard, and you can see where you stand at all times.

   This isn't cosmetic: "outline: none" with no visible replacement makes
   a page unusable by keyboard. Text fields have their own replacement -
   they recolor their border on tab - and are excluded here for that
   reason, or the border change and the ring would sit on top of each
   other.

   On fields, though, :focus-visible alone is NOT enough. The browser
   always counts a text field as visibly focused, even after a mouse click
   - measured in Chromium: button after click, no; text field after click,
   yes. The distinction there therefore comes from app.js, which sets
   data-tastatur on <html> on a tab keypress and removes it again on the
   next mouse click. Remove those few lines and the page loses its
   keyboard cue on every field at once.

   No border-radius here: the outline follows the element's own rounding
   on its own, and a fixed value would turn round buttons square when
   focused. */
:focus-visible:not(input):not(textarea) {
  outline: 2px solid var(--fokus);
  outline-offset: 2px;
}
.dim { color: var(--dim); font-weight: 400; }

/* ---------------- Buttons ---------------- */
.btn {
  border: 1px solid transparent; border-radius: var(--radius);
  padding: .6rem 1rem; cursor: pointer; background: var(--bg-3);
  transition: filter .15s, border-color .15s, background .15s;
  text-decoration: none; display: inline-flex; align-items: center; justify-content: center; gap: .4rem;
}
.btn:hover { filter: brightness(1.18); }
.btn:disabled { opacity: .5; cursor: not-allowed; }

/* The loading spinner INSIDE the button.
   ---------------------------------------------------------------------------
   Between "Continue" and the payment there's a call to the function that
   can easily take a second over a mobile network. Until now the button
   just went pale during that - that looks broken, not busy, and someone
   who sees nothing happen taps again.

   The label stays in the button and is only switched invisible: that way
   it keeps EXACTLY its width, and nothing jumps when it toggles. */
.btn.laedt { position: relative; color: transparent; pointer-events: none; }
.btn.laedt > * { visibility: hidden; }
.btn.laedt::after {
  content: ''; position: absolute; width: 1.05em; height: 1.05em;
  border: 2px solid currentColor; border-top-color: transparent;
  border-radius: 50%; animation: kreisel .7s linear infinite;
  /* currentColor is switched transparent on the button - the ring
     therefore gets its color from the border color, which isn't touched
     by that. */
  border-color: var(--knopf-text); border-top-color: transparent;
}
.btn-ghost.laedt::after, .btn:not(.btn-primary).laedt::after {
  border-color: var(--text); border-top-color: transparent;
}
@keyframes kreisel { to { transform: rotate(360deg); } }
@media (prefers-reduced-motion: reduce) {
  .btn.laedt::after { animation-duration: 2.4s; }
}

/* The text on the light button is the page background itself, not a tone
   of its own - it used to be a very dark green, which would pick up a
   tint next to bone white. */
/* No brightening under the pointer. The button is the brightest area on
   the page, and while typing the pointer sits on top of it almost the
   whole time - so the brightening wasn't an occasional effect, it was the
   normal state. The other buttons keep it: they're dark, and it lifts
   something that was barely visible before.

   So on desktop the button no longer has its own feedback on hover - just
   the mouse cursor. It still responds on press (.btn:active), and on
   phone, where there's no hover at all, nothing changes either way. */
.btn-primary { background: var(--accent-fill); color: var(--knopf-text); font-weight: 650; }
.btn-primary:hover { filter: none; }
.btn-outline { background: transparent; border-color: var(--line); color: var(--text); }
.btn-ghost { background: transparent; color: var(--dim); border-color: transparent; padding: .45rem var(--knopf-luft); }
/* A ghost button standing alone in a column gets pulled back by its own
   padding.
   ---------------------------------------------------------------------------
   A ghost button has no visible edge, so its padding is invisible too - and
   invisible padding is what made "Start over" sit 10.5 px to the right of
   every line above it in the login card. Measured: the paragraph, the two
   copy rows and the waiting line all start at the card's inner edge, the
   button's LABEL started one padding further in. Nothing looked broken, it
   just looked as if someone had indented one line by accident.

   The pull-back moves the box, not the label: the label lands on the column,
   while the button keeps its full size and its 44 px tap target. The hover
   background moves out with it, which is right - the surface belongs to the
   whole target, not to the letters.

   Deliberately NOT .btn-ghost in general: in a row of buttons the padding is
   doing its job, the buttons sit next to each other and nothing has to line
   up with a text column. Only where such a button IS the column.

   The + 1px is the transparent border every .btn carries (see .btn above).
   Invisible, but it takes up space like any other - without it the label
   landed one pixel right of the column, which measured as 332 against 331.
   Not visible to the eye; visible to the check in scripts/test-anmeldung.mjs
   that compares the two edges, and that check is right to be strict: it can
   only stay meaningful if "lines up" means the same number. */
.step > .btn-ghost { margin-inline-start: calc((var(--knopf-luft) + 1px) * -1); }
.btn-ghost:hover { color: var(--text); background: var(--bg-2); }
.icon-btn {
  background: transparent; border: 1px solid var(--line); color: var(--dim);
  width: 30px; height: 30px; border-radius: var(--radius); cursor: pointer; line-height: 1;
}
.icon-btn:hover:not(:disabled) { color: var(--text); border-color: var(--dim); }
/* Disabled means visibly disabled - without this difference a button
   would look like always and you'd be pressing into nothing.
   This rule used to be justified by the refresh button; that button no
   longer exists. It stays because it applies to every .icon-btn: a poll's
   delete button and the share button also lock briefly. */
.icon-btn:disabled { opacity: .45; cursor: default; }

/* The brand mark in the header is the link to the official X account. No
   separate icon next to it: two targets side by side that do the same
   thing are just two chances to mistap on a phone. No underline either,
   for the same reason - it should look like the brand mark, not a link,
   and the cursor alone says enough. */
.brand.brand-link {
  display: inline-flex; align-items: center; gap: .35rem;
  color: inherit; text-decoration: none; flex: none;
  padding: .25rem var(--marke-luft); margin-left: calc(-1 * var(--marke-luft));
  border-radius: var(--radius);
  transition: background .15s;
}
.brand.brand-link:hover { background: var(--bg-2); }

/* ---------------- Login ---------------- */
/* The safe zone here too - same reason as .topbar. From the home screen,
   the login card would otherwise bump into the clock at the top. */
/* Since html/body no longer scroll, this screen has to be able to on its
   own. Otherwise on a short device - or held sideways - the bottom part
   of the card, button included, would simply be unreachable.
   It still stays centered - but via margin-block: auto on the card, and
   NOT via justify-content: center.
   ---------------------------------------------------------------------------
   The difference shows up when the card is taller than the screen (short
   device, or landscape): justify-content: center then pushes the start of
   the card UP AND OUT of the scrollable area - the top of the card
   becomes unreachable, and permanently so, because you can't scroll into
   negative territory.

   An automatic margin can't do that: if there's room, it distributes it
   and the card sits centered; if there's none, it collapses to zero.
   Measured at 375x320 (card is 331px, so it doesn't fit): nothing clipped
   at the top, everything reachable at the bottom.

   Tried on the side and dropped again: justify-content: safe center. It
   solves the same problem, but in Chromium it also behaves like
   flex-start whenever there IS enough room - the card ended up stuck at
   the top on every device. */
.login {
  min-height: 100%; height: 100%; overflow-y: auto; overscroll-behavior: contain;
  display: flex; flex-direction: column;
  align-items: center; justify-content: flex-start; gap: 1.2rem;
  padding: calc(2rem + env(safe-area-inset-top, 0px))
           calc(1rem + env(safe-area-inset-right, 0px))
           calc(2rem + env(safe-area-inset-bottom, 0px))
           calc(1rem + env(safe-area-inset-left, 0px));
}
/* ---------------- Not yet ----------------
   The screen before launch. It reuses .login and .login-card instead of
   getting its own dimensions: it's the same layout - a card, centered,
   with the brand mark on top - and one sentence doesn't justify a second
   rule set.

   A line saying what SIZED is used to sit under "Launching soon." It's
   gone: anyone visiting the page before launch either already knows or
   isn't meant to find out here. */
/* The button sits BELOW the card and doesn't look like a button.
   ---------------------------------------------------------------------------
   It's not an invitation to the visitor, it's a shortcut for the handful
   of people currently building the page. A button shaped like a button
   would be the most eye-catching spot on the screen and the only one
   inviting a tap - of all things, the one that's nobody else's business.

   It stays thumb-sized regardless (44px), or it wouldn't be hittable on
   a phone. */
.soon-link {
  appearance: none; background: none; border: 0;
  color: var(--dimmer); font: inherit; font-size: .82rem;
  letter-spacing: .04em; cursor: pointer;
  min-height: 44px; padding: 0 .8rem; border-radius: var(--radius);
}
.soon-link:hover, .soon-link:focus-visible { color: var(--dim); }

.login-card {
  /* Centers the card without clipping it when space runs short - see the
     reasoning at .login above. */
  margin-block: auto;
  width: min(520px, 100%); background: var(--karte-grund);
  border: 1px solid var(--line); border-radius: var(--radius); padding: 2rem;
  -webkit-backdrop-filter: blur(12px); backdrop-filter: blur(12px);
}
/* Brand mark: glyph and word in one row. As flex, not inline text, because
   the glyph is now an SVG - a graphic sitting on the baseline always sits
   a touch too low, and with align-items it lines up with cap height
   without any manual nudging. */
.brand {
  display: flex; align-items: center; gap: .42em;
  font-size: 1.5rem; font-weight: 700; letter-spacing: .16em; margin-bottom: 1.4rem;
}
.brand.small { font-size: .95rem; margin: 0; letter-spacing: .14em; white-space: nowrap; }
.brand.brand-link { gap: .42em; }
/* The glyph scales with the text: 1em means it's always as tall as a
   capital letter next to it - in the header as on the login screen,
   without maintaining two sizes.
   fill="currentColor" in the SVG means: the color comes from here, and a
   change to the accent color reaches the glyph along with it. */
/* The mark carries the colour of the filled buttons.
   ---------------------------------------------------------------------------
   Not a value of its own: literally --accent-fill, the same blue as Continue
   and Send, so the two can never drift apart. Recolour the button and the
   logo follows.

   It pointed at --fuellung-spitze before, and the reasoning was good on its
   own terms - the logo is the colour of a poll that has been won. What that
   decision could not survive was the number: the winning bar has to stay pale
   because it is a surface with a line of text lying on it, and a pale fill is
   1.25:1 against the page. As a bar that is correct. As a mark it is a shape
   with nothing on top, and at 1.25:1 there is no shape.

   So the mark went to the other end of the same blue. --accent-fill is
   6.52:1 on the page ground, 6.98:1 on a card, 6.17:1 on the darker surface
   the top bar shows under a hover - it reads everywhere, and it still ties
   the logo to something rather than being a colour nobody can account for.

   The bar keeps its own value; the two are simply no longer the same thing. */
.brand-mark {
  display: block; height: 1.05em; flex: none;
  color: var(--marke);
}
/* Fix the height, let the width follow: the glyph is portrait (42 to 64),
   and a square box would frame it at the sides instead of letting it
   fill the space. 1.05em instead of 1em because a capital letter is
   shorter than the line - this makes glyph and text sit at the same
   optical height. */
.brand-mark svg { display: block; height: 100%; width: auto; }
.brand-dim { color: var(--dimmer); }
.lede { color: var(--dim); margin: 0 0 1.3rem; font-size: .93rem; }
.field { display: block; margin-bottom: 1rem; }
.field span { display: block; font-size: .78rem; text-transform: uppercase; letter-spacing: .08em; color: var(--dimmer); margin-bottom: .4rem; }
.field input {
  width: 100%; padding: .75rem .9rem; border-radius: var(--radius);
  background: var(--bg); border: 1px solid var(--line); font-family: var(--mono); font-size: .9rem;
}
.field input:focus { outline: none; }
:root[data-tastatur] .field input:focus { border-color: var(--fokus); }

/* ---------------- Autofilled by the browser ----------------
   Submit a field once, and next time you get a suggestion under it. Tap
   it, and Chrome paints the field light blue - a white box in the middle
   of a form that's dark everywhere else.

   This CANNOT be undone with background-color: Chrome sets the background
   through an internal rule a page can't reach. What a page can reach is
   an inset box shadow - it sits OVER the background and covers it. Hence
   the absurd 100px value: it has to fill the whole field no matter how
   tall it is.

   The same lock applies to the text color, hence -webkit-text-fill-color
   instead of color, and to the caret, hence caret-color.

   Both spellings of the shadow are there. The prefixed one is the one
   copied everywhere; the other is the correct one. Which one takes effect
   depends on the Chrome version, and having both costs nothing.

   Deliberately spelled out for each field on the page separately, because
   the reasons differ: the login and poll fields sit on --bg, the message
   line and Ansem's threshold field on --bg-1. One shared value would be
   wrong at half the spots.

   WARNING when changing this: this state cannot be reproduced in a test -
   autofill needs a real browser profile with saved values. What
   test-fokus.mjs checks is therefore only that there's a matching entry
   here for EVERY field background. Whether it actually works can only be
   seen in production. */
.field input:-webkit-autofill,
.field input:-webkit-autofill:hover,
.field input:-webkit-autofill:focus,
.poll-admin input:-webkit-autofill,
.poll-admin input:-webkit-autofill:hover,
.poll-admin input:-webkit-autofill:focus {
  -webkit-text-fill-color: var(--text);
  caret-color: var(--text);
  -webkit-box-shadow: 0 0 0 100px var(--bg) inset;
  box-shadow: 0 0 0 100px var(--bg) inset;
}
.composer input:-webkit-autofill,
.composer input:-webkit-autofill:hover,
.composer input:-webkit-autofill:focus,
.filter-group input:-webkit-autofill,
.filter-group input:-webkit-autofill:hover,
.filter-group input:-webkit-autofill:focus {
  -webkit-text-fill-color: var(--text);
  caret-color: var(--text);
  -webkit-box-shadow: 0 0 0 100px var(--bg-1) inset;
  box-shadow: 0 0 0 100px var(--bg-1) inset;
}
.login .btn-primary, .login .btn-outline { width: 100%; margin-bottom: .5rem; }
.error { color: var(--warn); font-size: .85rem; margin: .6rem 0 0; }
/* The note sits below the card and should look like it belongs there:
   same width, centered. It used to be a plain paragraph that took its
   width from the text - for this sentence that's 588px against a 520px
   card, so it stuck out on both sides. By the numbers it was centered, but
   visually it sat crooked against the window above it.

   text-align here, not just on the phone: as soon as the sentence wraps,
   the second line would otherwise sit flush left - and that's exactly
   when it looks off. */
/* One line, centered, and if it overruns the card, then equally on both
   sides. No fixed width: that forced the sentence onto two lines.

   text-align stays anyway - it costs nothing as long as the sentence fits
   on one line, and it saves the case where it doesn't. Without it, a
   wrapped second line would sit flush left, and that's exactly what
   looked off. */
.foot-note {
  color: var(--dimmer); font-size: .78rem;
  max-width: 100%; text-align: center; margin-top: 0;
}

/* The payment note doesn't belong on the home-screen step.
 *
 * That step is about adding to the home screen; there's no mention of a
 * payment yet at that point. The sentence arrives one step later, where
 * the address is entered - it appears exactly at the moment it matters.
 *
 * Via :has() and not via JavaScript, deliberately: the switch between
 * steps happens at five different spots in app.js. A line that's present
 * at four of them and missing at the fifth goes unnoticed by everyone.
 * This rule hangs off the state itself and can't be forgotten. If a
 * browser doesn't know :has(), the sentence just stays visible - that's
 * the harmless failure mode. */
.login:has(#step-install:not([hidden])) .foot-note { display: none; }

.field textarea {
  width: 100%; padding: .7rem .9rem; border-radius: var(--radius); resize: vertical;
  background: var(--bg); border: 1px solid var(--line);
  font-family: var(--mono); font-size: .82rem; overflow-wrap: anywhere;
}
.field textarea:focus { outline: none; }
:root[data-tastatur] .field textarea:focus { border-color: var(--fokus); }

.sign-block {
  background: var(--bg); border: 1px solid var(--line);
  border-radius: var(--radius); padding: .6rem .8rem; margin-bottom: .9rem;
}
.sign-head { display: flex; align-items: center; justify-content: space-between; margin-bottom: .35rem; }
.sign-message {
  display: block; font-size: .76rem; line-height: 1.55;
  overflow-wrap: anywhere; color: var(--accent);
}

.how { margin-bottom: 1rem; border-top: 1px solid var(--line); padding-top: .8rem; }
.how summary { cursor: pointer; color: var(--dim); font-size: .86rem; list-style: none; }
.how summary::before { content: '▸ '; color: var(--dimmer); }
.how[open] summary::before { content: '▾ '; }
.how summary:hover { color: var(--text); }
.how p { font-size: .84rem; color: var(--dim); margin: .8rem 0 .5rem; }
.how code { font-size: .8rem; color: var(--text); }
.how .sign-message { color: var(--accent); }

/* The whole row is the copy button - hence a <button>, not a <div>. A
   small "Copy" next to a 44-character address is a mistap magnet on a
   phone, and there's a payment riding on this one. */
.pay-row {
  display: grid; grid-template-columns: 1fr auto; align-items: center;
  gap: .25rem .6rem; width: 100%; text-align: left;
  background: var(--bg); border: 1px solid var(--line); border-radius: var(--radius);
  padding: .7rem .8rem; margin-bottom: .6rem; cursor: pointer;
  font: inherit; color: inherit;
  transition: border-color .15s, background .15s;
}
.pay-row:hover { border-color: var(--dim); }
/* Copied: just the outline, brief, and not garish.
   ---------------------------------------------------------------------------
   border-color: var(--accent) plus a filled background used to sit here.
   Two signals for one action, and the accent color is the brightest thing
   on the page - the whole row flashed on a phone. The word for that was
   "garish".

   Now a translucent white on the outline, and nothing else. It's brighter
   than --line, so clearly recognizable as feedback, but it doesn't glow:
   the background stays as it is, and the row keeps its shape.

   How brief, that lives in app.js (KOPIERT_MS). */
.pay-row.is-copied { border-color: var(--betont-rand); }
/* Nothing gets selected in the copy row.
   ----------------------------------------------------------------------------
   The whole row is a button: tap it, it copies. A blue selection next to
   that isn't a help, it's a second response to the same action - you tap
   once and get "Copied" AND a blue block.
   user-select: all used to sit here on the value. The idea was that a
   click would grab the whole value, so nobody had to manually select a
   44-character address. But that's redundant now that the row itself
   copies - all that was left of it was the blue block. */
.pay-row { -webkit-user-select: none; user-select: none; }

/* The one exception, and it matters: if copying fails - no HTTPS, an
   embedded browser with no clipboard - app.js selects the value itself so
   it can be grabbed by hand. For that it has to be selectable at exactly
   that moment, or the message "the text is selected for you" would be
   pointing at nothing.
   That's why the error path sets this class; without it, the fallback
   would be a claim with nothing behind it. */
.pay-row.zum-markieren .pay-value {
  -webkit-user-select: all; user-select: all;
}

.pay-label { font-size: .72rem; text-transform: uppercase; letter-spacing: .08em; color: var(--dimmer); }
.copy-hint {
  grid-row: 1; grid-column: 2; justify-self: end;
  font-size: .72rem; color: var(--dimmer); white-space: nowrap;
}
/* The hint switches its text ("Copied ✓") and needs no color change
   on top of that - a second signal in the page's brightest color was
   exactly what made it flash before. */
.pay-row.is-copied .copy-hint { color: var(--dim); }
.pay-value { grid-column: 1 / -1; overflow-wrap: anywhere; }
/* The amount carries no colour of its own - the same ink as the address
   below it.
   ---------------------------------------------------------------------------
   It used to be --accent, on the reasoning that the amount is the core of
   this screen. It is, and it stays the most prominent thing here: 1.3rem
   against .88, and 600 against 400. Size and weight say that; a third
   signal on top said it again in a different language.

   What the colour cost: the two rows are the same KIND of thing - a value
   to copy, one per row - and colouring one of them made them look like two
   different kinds. And on a light ground it got worse rather than better,
   because the accent there is a warm ochre and the address next to it is
   near-black. */
.pay-value.big { font-size: 1.3rem; color: var(--text); font-weight: 600; letter-spacing: .01em; }
.mono-sm { font-size: .88rem; color: var(--text); line-height: 1.6; letter-spacing: .02em; }

/* ---------------- Startbildschirm-Schritt ---------------- */
.why {
  background: var(--bg); border: 1px solid var(--line);
  border-left: 3px solid var(--accent);
  border-radius: var(--radius); padding: .75rem .9rem; margin-bottom: 1rem;
}
.why p { margin: 0; font-size: .88rem; color: var(--dim); }
.why strong { color: var(--text); }

/* The home-screen instructions - as a list of rows, not a numbered list.
 *
 * It used to be a plain <ol> with hanging digits. At 375px, two of the
 * three steps wrapped ("Add to Home / Screen"), and the second line then
 * slid left, under the number. Result: three rows of different heights
 * and a ragged left edge - on the very first screen a phone user ever
 * sees.
 *
 * Now every step gets its own strip with a fixed indent, like a settings
 * list on a phone. A wrap no longer stands out: the number sits in its
 * own column, the text starts at the same edge on all three, and stays
 * there on the second line too.
 */
.steps {
  list-style: none; margin: 0 0 1rem; padding: 0; counter-reset: schritt;
  border: 1px solid var(--line); border-radius: var(--radius);
  background: var(--bg-1); overflow: hidden;
  color: var(--dim); font-size: .92rem; text-align: left;
}
.steps li {
  counter-increment: schritt;
  display: flex; align-items: flex-start; gap: .7rem;
  padding: .7rem .85rem; border-bottom: 1px solid var(--line);
}
.steps li:last-child { border-bottom: 0; }
.steps li::before {
  content: counter(schritt);
  flex: none; width: .9rem;
  font-family: var(--mono); font-size: .78rem; line-height: 1.55;
  color: var(--dimmer);
}
.steps strong { color: var(--text); }
.glyph {
  display: inline-flex; vertical-align: -.2em; color: var(--accent);
  background: var(--bg-3); border: 1px solid var(--line);
  border-radius: var(--radius-klein); padding: .1rem .25rem; margin: 0 .1rem;
}

.waiting { display: flex; align-items: center; gap: .55rem; margin: 1.1rem 0 .8rem; font-size: .88rem; color: var(--dim); }
/* A pulsing dot in the accent color used to sit next to "Waiting for
   payment". It's gone, by request, and the reasoning holds even without
   the request: the sentence already says that waiting is happening. The
   dot said the same thing again, just blinking - and the accent color is
   the most eye-catching one on the page. A blinking bright dot reads as a
   warning, not calm, in the one spot where someone has just sent money
   and is already on edge. */

/* ---------------- App shell ---------------- */
/* app.js sets --versatz, and only while the keyboard is up.
   ---------------------------------------------------------------------------
   Height (--sicht) alone didn't fix the bug, and the reason is that iOS
   does TWO things: it shrinks the visible viewport, and it shoves it up
   over the page, which stays still. No height fixes the second part - the
   view has to move by the same amount.

   That's exactly what --versatz is: visualViewport.offsetTop, unchanged.

   A side effect that's wanted here: transform makes .app the reference
   point for everything inside it that's position: fixed - the toast at
   the bottom and the preview hint. Both then sit above the keyboard
   instead of under it.

   The DM menu sits outside .app and isn't affected by this; it
   recalculates its position on every open anyway. */
.app {
  height: 100%; display: flex; flex-direction: column;
  max-width: 1180px; margin: 0 auto;
  transform: translateY(var(--versatz, 0px));
}
/* Three columns, not a flex strip - so the tabs are ACTUALLY centered.
   ---------------------------------------------------------------------------
   This used to be a flex row with .tabs at margin-inline: auto. But that
   doesn't center a flex item in the box, it centers it in the LEFTOVER
   SPACE between its neighbors: the free space gets split evenly to its
   left and right. The tabs ended up centered between the brand mark and
   the block on the right - and that midpoint moves as soon as either side
   gets wider.

   Visible when switching between Ansem and a normal user: he has an
   avatar and "$12.4M", a user has three characters and "$153". Two
   different widths on the right, two different tab positions.

   But it's not just him, and that's the real reason for the rebuild: the
   amount on the right gets refreshed every minute. "$153" becomes "$1.2K",
   and the tabs shift along with it - without anyone touching anything.

   1fr auto 1fr gives both outer columns the same width. The middle one
   then sits at the center of the header, whatever is on the left or
   right.

   That doesn't hold on the phone: there the header wraps to two lines,
   the tabs sit as their own row below and take the full width. The grid
   is therefore switched back to flex inside the phone breakpoint. */
/* The safe zone.
   ---------------------------------------------------------------------------
   index.html sets viewport-fit=cover and black-translucent. Together they
   mean: launched from the home screen, the page starts RIGHT AT THE TOP,
   under the clock, signal bars and battery icon. Without compensation,
   "SIZED" and the balance next to it sat right underneath them.

   env(safe-area-inset-top) is 0 in a browser tab and only nonzero when
   running from the home screen - so the same rule works for both, with no
   check needed.

   Left and right too: held sideways, the camera cutout pushes content to
   the side, and without this the brand mark would sit half under it. */
.topbar {
  display: grid; grid-template-columns: 1fr auto 1fr;
  align-items: center; gap: 1rem;
  padding: calc(.7rem + env(safe-area-inset-top, 0px)) calc(1rem + env(safe-area-inset-right, 0px)) .7rem calc(1rem + env(safe-area-inset-left, 0px));
  border-bottom: 1px solid var(--line); background: var(--topbar-grund);
  -webkit-backdrop-filter: blur(10px); backdrop-filter: blur(10px); position: sticky; top: 0; z-index: 5;
}
/* Without this, the grid cell would stretch the brand mark to the whole
   column width - a link whose clickable area runs straight through empty
   space. */
.topbar > .brand-link { justify-self: start; }
.topbar > .me { justify-self: end; }

/* --nah: the tight gap on the right, between the amount and the logout
   button. It used to also sit on the left, back when a badge with
   Ansem's picture sat next to the brand mark. That's gone again; the
   value stays as its own name because "tight" will be needed in more than
   one place. */
:root { --nah: .5rem; --marke-luft: .35rem; }

.tabs { display: flex; gap: .25rem; }
.tab {
  background: transparent; border: 1px solid transparent; border-radius: var(--radius);
  padding: .45rem .95rem; color: var(--dim); cursor: pointer; position: relative;
}
/* The "s" in "DMs". For everyone except Ansem it disappears, space and
   all.
   ---------------------------------------------------------------------------
   visibility: hidden used to sit here - invisible, but the space stayed
   reserved. The reason was: otherwise the tab would be one character
   narrower, and Polls next to it would sit at a different spot for Ansem
   than for a regular user.

   That created a visible bug, and right where the tab shows off the most:
   when it's selected, it carries a border and a filled background. Those
   were sized for "DMs", but the text inside actually said "DM" - so 4.5px
   of air on the right and none on the left. A box whose content doesn't
   sit in its own center.

   The two bugs aren't worth the same, and that's the whole point:

     The lopsided box shows up for EVERY user, EVERY time the tab is
     selected.

     Nobody sees the shift, because nobody sees both roles at once. You'd
     have to put two screens side by side.

   And the shift is also more harmless now than it was when the old rule
   was written: since the header became a grid (1fr auto 1fr), the tabs
   sit centered in the HEADER, not in the leftover space between their
   neighbors anymore. So the group just gets narrower instead of moving -
   the CENTER stays where it is. Measured: 137px against 128px, so 9px
   narrower, 4.5px per side. (The old comment talked about a 4.5px group
   width; that was half the truth - one cell of the mono font is about 9px
   wide at 15px.) */
.dm-s { display: none; }
.tab.zeigt-s .dm-s { display: inline; }
.tab:hover { color: var(--text); }
.tab.is-active { color: var(--text); background: var(--bg-2); border-color: var(--line); }
.me { display: flex; align-items: center; gap: var(--nah); }
/* Same as everywhere else: just the colored name, no box around it. */
.handle { font-family: var(--mono); font-weight: 700; }
/* .handle.adresse used to sit here: the shortened address top right, in
   the color of the amount next to it. Both are reverted now - the three
   characters in their own color (.h.t0-.t3) sit there again, the exact
   same identifier as in the DMs and the inbox. See yourself under one
   name top right and another name next to your own messages, and you're
   seeing yourself under two names. */

/* ---------------- The hint next to the avatar ----------------
   Ansem sees his picture top right, no address. A tap on it tells him
   which account he's on here - the information everyone else can read
   off without pressing anything.

   The reference point is .me-ich, not .me: the hint should open to the
   left of the PICTURE, not to the left of the whole group of picture,
   amount and logout button. */
.me-ich { position: relative; display: inline-flex; align-items: center; }
.me-info {
  display: none;
  position: absolute; right: calc(100% + .55rem); top: 50%;
  transform: translateY(-50%);
  z-index: 30; white-space: nowrap;
  background: var(--bg-1); border: 1px solid var(--line); border-radius: var(--radius);
  padding: .45rem .7rem;
  font-size: .8rem; color: var(--dim);
  box-shadow: var(--schatten-hoch);
}
/* It appears while the pointer sits on the picture, and disappears the
   moment it moves off. One rule, no state - a class set by a click used
   to sit here for a while, plus an outside-click handler and the escape
   key. All that for a box that shows itself.

   Only where a pointer actually EXISTS: on a phone, :hover sticks around
   after a touch in some browsers, and the box would then sit somewhere
   else until the next touch. So with no pointer, it doesn't exist at all
   - the screen reader still gets the sentence, via aria-describedby on
   the picture.

   On the PICTURE, not on the box around it, and that's not a fine point:
   the box exists for everyone, .admin-name only for Ansem. Hang the rule
   on the box, and a regular user hovers their own address and reads "You
   are logged in as Ansem". The adjacent-sibling selector ties it to the
   element that only sits there for him. */
@media (hover: hover) {
  #me-handle.admin-name:hover + .me-info { display: block; }
}
/* Your own balance next to it. No outline, same size as the handle - the
   two belong together and read as one unit: who, and how much.

   For a long time the reasoning here was that size and shape should come
   entirely from .worth, so the header would show the same amount style as
   the list below it, not a second version of it. The sentence meant well
   and was still backwards: in a list, the amount sits in its own column
   next to twenty others and takes its cue from them. In the header it
   stands alone next to exactly one handle and takes its cue from that.
   Two different neighbors, two different measurements - that's not a
   second version, it's the same thing in a different place.

   What actually holds in common stayed: the mono font and the amount
   color. That's the shared thread, not the size.

   Weight 600 instead of the handle's 700: at the same size AND the same
   weight, the name and the number would fuse into one word. */
.holdings {
  font-size: 1rem; font-weight: 600; color: var(--worth);
  background: none; border: 0; border-radius: 0;
  height: auto; line-height: inherit; padding: 0;
}

/* Same gap on top as on the bottom and the sides. This used to be zero,
   and a panel butted straight up against the line under the header - the
   two edges sat right on top of each other and cut into each other at the
   rounded corners. Polls and DMs each used to bring their own spacing;
   now it comes from here for both. */
.pane { flex: 1; min-height: 0; display: flex; flex-direction: column; padding: 1rem; }
/* The scrollbar is an indicator, not a control: scrolling happens with a
   wheel or a finger, dragging it practically never happens. All it says
   is where you are in the stack - and a thin line is enough for that.
   Eight pixels used to sit here; in a 290px-narrow column that's three
   percent of the width spent on information you only glance at. */
/* overscroll-behavior: contain - the swipe ends here.
   Without this, a list hands off the rest of the motion to the page at
   its top or bottom edge, and the whole view moves along with it. */
.scroll { overflow-y: auto; overscroll-behavior: contain;
          scrollbar-width: thin; scrollbar-color: var(--bg-3) transparent; }
/* No scrollbar - neither in the conversation nor in the polls.
   ---------------------------------------------------------------------------
   In the conversation the line says nothing: the newest is at the bottom,
   and that's where you almost always are. It would sit there next to
   every single bubble for no reason.

   For the polls it was meant as information ("where am I in the stack").
   It should still go, and the reasoning holds: on a phone the list IS the
   whole view, scrolling happens with a finger, and a gray line at the
   edge stops being a hint and becomes a leftover trace of a window, in a
   view that doesn't want to be one.

   Both ways are declared, because which one wins is Chrome's own call:
   since version 121, scrollbar-width beats the old ::-webkit- rules; in
   older browsers it's the other way around. */
.chat-list, .poll-list { scrollbar-width: none; }
.chat-list::-webkit-scrollbar,
.poll-list::-webkit-scrollbar { width: 0; height: 0; }

.scroll::-webkit-scrollbar { width: 5px; }
.scroll::-webkit-scrollbar-thumb { background: var(--bg-3); border-radius: var(--radius-klein); }
/* And in the inbox, no browser scrollbar at all, but a custom line of our
   own next to it.
   ----------------------------------------------------------------------------
   Why build it ourselves

   The browser's own bar has no freely chosen length: it's the ratio of
   what's visible to the whole. With three conversations it fills nearly
   the whole track, and it's never "small" then. That's exactly what was
   wanted here - a small marker that shows where you are, not how much
   there is.

   A second reason came on top of that, and it weighs more: Chrome decides
   on its own whether a bar is drawn as an overlay or eats into the width,
   and since version 121 scrollbar-width wins over the old ::-webkit-
   rules. Which of these lands where wasn't measurable from here - the
   Chromium used in tests only draws overlay bars. A custom little box, by
   contrast, can be measured like any other element, and it looks the same
   everywhere.

   So the browser's own bar is turned off entirely - both ways, since it's
   not predictable which one applies. */
#thread-items { scrollbar-width: none; }
#thread-items::-webkit-scrollbar { width: 0; height: 0; }

.thread-roll { position: relative; flex: 1; min-height: 0; display: flex; }
.thread-roll > #thread-items { flex: 1; min-height: 0; }

/* Four pixels wide, rounded. app.js sets the HEIGHT: it follows the ratio
   of what's visible to the whole, capped at 64px with 24px as the floor
   (STRICH_MAX / STRICH_MIN). The value here is just the initial state,
   before scrolling has happened for the first time.

   The color is 70% --dimmer: --bg-3 on --bg-1 sits at 1.17:1 - still just
   about something as a wide area, nothing at all as a thin marker. This
   way it's 2.19:1: quiet, but there. */
#thread-strich {
  /* -1px: the marker sits ON the edge, not next to it - for its length,
     it IS the edge. Sitting inside, it looked like something floating in
     the column; this way it belongs to the border. */
  position: absolute; right: -1px; top: 0;
  width: 4px; height: 40px; border-radius: var(--radius-klein);
  background: color-mix(in srgb, var(--dimmer) 70%, var(--bg-1));
  pointer-events: none;
  opacity: 0; transition: opacity .35s ease;
}
/* Visible only while scrolling. app.js sets the class and takes it away
   again after the last movement; no transition on appearing, so the
   marker is there instantly, and a transition on the way out. */
#thread-items.rollt + #thread-strich { opacity: 1; transition: none; }

/* ---------------- Usernames ----------------
   Five tones, deliberately no more: a list of ten colors reads like a
   warning panel. The accent color is reserved for Ansem and must not show
   up here - or any random user would look like him.

   Muted version: closer to gray, so names don't outshine the messages.
   Contrast on the dark background at least 7.7:1 - the best of every set
   tested - with a gap between each other of at least Delta E 27.

   Known limit: blue and violet nearly merge under red-green color
   blindness (Delta E 5). That only matters when two different wallets
   share the same three characters - only there is color the sole way to
   tell them apart. With 58 cubed = 195,112 possible handles, that only
   starts happening at a noticeable rate at a few thousand users, and the
   full address is in the message's tooltip. */
.h.t0 { color: var(--name-0); }
.h.t1 { color: var(--name-1); }
.h.t2 { color: var(--name-2); }
.h.t3 { color: var(--name-3); }

/* Ansem shows the same three characters as everyone else - but always in
   this one color, the accent. It's the brightest one on the page and
   otherwise belongs to the brand mark itself; that reads as "the house is
   speaking here".

   It's deliberately NONE of the four tones .h.t0-.t3. Those get computed
   from the address, and anyone can randomly get any of them. This one is
   fixed to a single address and therefore can't be faked: to pass as
   Ansem you'd have to own his wallet.

   The color shows up in quotes and in the inbox, where Ansem keeps his
   handle. In the header he has his avatar instead - see .admin-name
   further down. He doesn't need the color for himself there anyway; it
   tells EVERYONE ELSE who wrote it.

   Otherwise nothing special applies to him anymore. No spelled-out name
   that has to be squeezed into the handle column, no empty amount column
   - his balance sits next to his messages just like everyone else's. */
.admin-name { color: var(--accent); }

/* ---------------- Number field with unit ----------------
   This was originally built for the chat's filter bar. The chat is gone,
   but the building blocks stayed: Ansem's DM threshold uses them, and a
   poll's duration was once modeled on the same shape; it's since become
   its own block of three select fields (see .lz-block). The preset
   buttons (.presets/.chip) disappeared along with the chat - they had
   fixed amounts there, and nothing else on the page has those. */
.filters {
  display: flex; align-items: center; gap: .6rem; flex-wrap: wrap;
  padding: .7rem 0; border-bottom: 1px solid var(--line); font-size: .82rem; color: var(--dim);
}
.filters-label { color: var(--dimmer); }

/* No up/down arrows on number fields. They're tiny, unhittable on a
   phone, and invite clicking your way up in steps of a hundred. */
input[type=number] { -moz-appearance: textfield; appearance: textfield; }
input[type=number]::-webkit-outer-spin-button,
input[type=number]::-webkit-inner-spin-button { -webkit-appearance: none; margin: 0; }
.filter-group {
  display: flex; align-items: center; gap: .3rem;
  background: var(--bg-1); border: 1px solid var(--line); border-radius: var(--radius); padding: .2rem .5rem;
}
.filter-group input { width: 86px; background: transparent; border: 0; outline: none; font-family: var(--mono); font-size: .82rem; }
/* And the replacement for that outline: none - it was missing for a long
   time.
   ---------------------------------------------------------------------------
   The field has no border of its own; the border belongs to the group
   around it. With outline: none on the field and no rule on the group, a
   tab jump showed NOTHING AT ALL - checked directly: border unchanged,
   outline none, no glow. Move through the inbox by keyboard and you lose
   track right there.

   Exactly the case the comment at :focus-visible further up warns about.
   It only surfaced when a separate check counted up the page's field
   rules - not by looking at it.

   focus-within, not focus: the focus sits on the FIELD, but the group is
   what should change color. */
:root[data-tastatur] .filter-group:focus-within { border-color: var(--fokus); }
.unit { color: var(--dimmer); font-size: .74rem; }
.unit-token { white-space: nowrap; padding-left: .1rem; }

/* ---------------- Message history ----------------
   .chat-list keeps that name even though it only carries the DM bubble
   threads now: the name appears in three places in this file and one in
   the HTML, and renaming it would be a four-spot change for zero gain.

   Air above and below, gap between messages. Without it, the last bubble
   in an open conversation would butt right up against the reply bar's
   line and look like they were cutting into each other. */
.chat-list { flex: 1; padding: .9rem 0; display: flex; flex-direction: column; gap: .5rem; }
/* Dock messages at the bottom while the history is shorter than the
   window. Via margin-top rather than justify-content, or scrolling
   upward breaks. */
.chat-list > *:first-child { margin-top: auto; }

/* What's left of the chat row.
   ---------------------------------------------------------------------------
   A four-column grid used to sit here - handle, text, amount, timestamp -
   with a divider, a hover area and its own row for the quote. It
   disappeared along with the chat.

   .msg now only carries the DM message bubbles, and those bring their own
   grid (.msg.dm). What stayed is exactly what both forms shared: the soft
   transition for .flash, and the assignment of the two grid areas that
   .msg.dm occupies. Add something here and you're changing the bubbles -
   there's no second place .msg shows up anymore. */
.msg { transition: background .3s; }
.msg .body { grid-area: body; }
.msg .meta { grid-area: meta; display: inline-flex; align-items: baseline; gap: .3rem; }
/* Brief flash when jumping to the original message via a quote. */
.msg.flash { background: rgba(var(--accent-rgb), .12); }

/* ---------------- Quote and replies ---------------- */
/* No more box, just the line.
   A tinted area with rounded corners used to sit here, .85rem, in --dim.
   That was right once: back then message text sat at .78rem and pale, and
   the quote was the smaller piece of information. Since text moved to
   .86rem and brighter, the quote had quietly become the loudest part of
   the reply - and measured, the box made a reply 83% taller than a normal
   line.

   A quote is something that already stood there once; the reply is the
   new part. The lead-in shouldn't be louder than the thing itself.

   The line on the left stays: it's what says "this belongs together", and
   it costs no height. The box said the same thing again, just louder.

   The whole row stays clickable and jumps to the quoted message - that's
   why this is still a <button>, not a <span>. */
.quote {
  display: flex; align-items: baseline; gap: .4rem; width: 100%;
  font: inherit; text-align: left; cursor: pointer; overflow: hidden;
  background: none;
  border: 0; border-left: 2px solid var(--line); border-radius: 0;
  padding: .1rem 0 .1rem .5rem; margin-bottom: .15rem;
  color: var(--dimmer); font-size: .78rem;
  transition: color .15s, border-color .15s;
}
/* With no fill, the hover cue has to come from the text. */
.quote:hover { color: var(--dim); border-left-color: var(--dim); }
.quote .h { font-family: var(--mono); font-weight: 700; font-size: .78rem; flex: none; }
/* min-width: 0, or the ellipsis never shows up: as a flex item,
   .quote-body won't shrink below its content, and .quote clips
   mid-word instead. */
.quote-body { min-width: 0; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.quote.is-gone {
  width: auto; cursor: default; font-style: italic;
  color: var(--dimmer); border-left-color: var(--bg-3); background: none;
}

/* The reply arrow sits directly after the message's last word and flows
   with the text - that's why it lives INSIDE .body, not in its own grid
   cell.
   Invisible also means untouchable here (pointer-events), or you'd catch
   it by accident while selecting text. */
.reply-btn {
  display: inline-flex; align-items: center; justify-content: center;
  margin-left: .3rem; vertical-align: -.15em;
  background: none; border: 0; color: var(--dimmer); cursor: pointer;
  font-size: .95rem; line-height: 1; padding: .1rem .3rem; border-radius: var(--radius-klein);
  opacity: 0; pointer-events: none;
  transition: opacity .12s, color .12s, background .12s;
}
/* Hover and tap live over at the DMs (.dm-row:hover / .dm-row.is-active);
   the chat-row rules went away with the chat row. Keyboard focus stays
   here: it applies to every reply arrow, no matter where one sits. */
.reply-btn:focus-visible { opacity: 1; pointer-events: auto; }
/* A touch of white instead of --bg-2: the button sits on backgrounds of
   different brightness. --bg-2 is darker than --line - on the brighter
   surface, the button would be a dark hole instead of a highlight. A
   translucent white is one step brighter than whatever it sits on, on any
   background. */
.reply-btn:hover { color: var(--accent); background: var(--auflage); }
/* No permission to write, no need for a reply button either. */
.no-write .reply-btn { display: none; }
/* As long as the database doesn't have the reply column, there's no
   button for it either - otherwise it would lead nowhere. */
.no-replies .reply-btn { display: none; }

/* ---------------- By touch: hold and swipe ----------------
   On a phone there used to be only one way to reply: tap the message so
   the arrow appears, then hit the arrow. Two taps on a 44px target, and
   in between, a long press selected the text and pulled up iOS's own menu
   ("Copy / Look Up / Share") - which has nothing to do with ours.

   Now, as in X's DMs: swipe right to reply directly.

   A small popover on long press ("Reply / Copy") lived here for a while
   and is gone again, by request. */

/* No text selection in a message - everywhere, not just by touch.
   ---------------------------------------------------------------------------
   Specifically: ONLY by touch.
   ---------------------------------------------------------------------------
   This check used to sit here once, got dropped ("gone entirely"), and is
   back now - this time with the difference between the two as the reason:

     By touch, selecting is an accident. Hold a bubble a little too long
     and you get text selected plus iOS's system menu on top of it. Nobody
     wanted that, they just pressed.

     With a mouse, it's deliberate. You drag over exactly the words you
     want. Forbid that, and you're taking away something someone is doing
     on purpose - and since "Copy" was removed, there's no other way left
     to copy a message's text.

   (hover: none) separates the two cleanly enough: a device a pointer can
   hover over has a mouse.

   -webkit-touch-callout additionally suppresses iOS's system menu ("Copy
   / Look Up / Share"), which would otherwise sit over the page on a long
   press. */
/* The whole ROW, not just the bubble.
   ---------------------------------------------------------------------------
   .msg.dm used to sit here, so just the message text. The reply arrow
   next to it stayed selectable that way: hold it too long on a phone and
   you'd get it selected plus a copy menu for a single character. Found on
   the device, not here.

   .dm-row covers the bubble, timestamp, quote and arrow all at once - and
   is therefore also the rule that keeps working if someone adds something
   else to the row later. */
@media (hover: none) {
  .dm-row { -webkit-touch-callout: none; -webkit-user-select: none; user-select: none; }
}

/* The bubble follows the finger.
   The transition is for snapping back; while dragging, it's turned off
   (.wischt), or the bubble would lag behind the finger. */
.dm-block { transition: transform .18s ease; }
.dm-row.wischt .dm-block { transition: none; }
/* The arrow is visible while swiping - how far, app.js sets on the
   element, so it appears along with the finger and not all at once. */
.dm-row.wischt .reply-btn { pointer-events: none; }



/* The bar hugs the quoted message instead of taking the full width. It
   used to run to the edge, the text ended after a third of it, and the
   close cross sat alone all the way on the right - an empty stretch in
   between that looked like something was missing there.

   max-width holds the limit: a long quote grows to the edge and then
   truncates with an ellipsis, just as before. Same rule as the quote
   above a message - two displays of the same thing should behave the
   same way too. */
.reply-bar {
  display: flex; align-items: center; gap: .5rem;
  width: fit-content; max-width: 100%;
  padding: .5rem .7rem; margin-top: .7rem;
  background: var(--bg-1); border: 1px solid var(--line);
  border-left: 3px solid var(--accent); border-radius: var(--radius);
  font-size: .86rem; color: var(--dim); min-width: 0;
}
.reply-bar-mark { color: var(--accent); flex: none; }
.reply-bar-inner { display: flex; align-items: baseline; gap: .45rem; min-width: 0; }
.reply-bar-inner .h { font-family: var(--mono); font-weight: 700; flex: none; }
.reply-bar-text { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.reply-bar .icon-btn { margin-left: auto; flex: none; }
/* ---------------- Ansem's avatar ----------------
   Instead of the three characters, at the one spot where his name stands
   for IDENTITY: top right, when he's the one signed in. In quotes, the
   inbox and the reply bar, the characters stay - there the name is a
   reference to a message, not a sender, and a circle in the middle of a
   quote line would make it taller without adding anything.

   The second spot was his chat row, with a 20px picture that fit exactly
   into the 26px-wide handle column. That's gone with the chat; 22px takes
   its cue from the balance next to it.

   This spot is deliberately called out ON ITS OWN, not as "everywhere
   except here and here". The same class also hangs on the quote, the
   inbox and the reply bar; an exception list is exactly the kind of rule
   that silently does the wrong thing at the next new spot. */
.me .handle.admin-name {
  --bild: 20px;
  display: inline-flex; align-items: center; justify-content: center;
}
.me .handle.admin-name::before {
  content: ''; flex: none;
  width: var(--bild); height: var(--bild); border-radius: 50%;
  background: url('ansem.jpg') center/cover no-repeat;
}
/* It takes its cue from the balance next to it, sitting in the same row. */
.me .handle.admin-name { --bild: 22px; }
/* Your own balance top right - since the chat was removed, the only spot
   still carrying .worth. Size, weight and alignment come from the header
   itself (see .holdings): they take their cue from the handle next to
   them.

   A full pill used to sit here - outline, fill, fixed height, its own
   padding against the dollar sign - and below it a second rule that undid
   every one of those properties again inside the chat. Both are gone. */
.worth { font-family: var(--mono); white-space: nowrap; flex: none; }

.msg .body { overflow-wrap: anywhere; }
/* Clickable links in message text.
   Underlined, not just colored: on a page made almost entirely of
   grayscale, color alone doesn't carry the message "this is clickable" -
   and anyone who has trouble telling colors apart would see nothing at
   all.

   The address always shows up as plain text, because the text has no
   formatting: there's no way to write "sized.gg" and link it somewhere
   else. In a space where scam attempts are to be expected, that's worth
   more than any warning you could write next to it.

   overflow-wrap already sits on .body - so a long address doesn't blow
   out the line. */
.msg .body a {
  color: var(--worth); text-decoration: underline;
  text-underline-offset: 2px; text-decoration-thickness: 1px;
}
.msg .body a:hover { color: var(--text); }
/* An exception used to sit here for your own bubble: it was light and
   carried dark text, so the link took the page background color. Since
   both bubbles are dark now, the same rule applies everywhere - one fewer
   exception. */
.msg .time { font-size: .7rem; color: var(--dimmer); flex: none; }

.composer { display: flex; gap: .5rem; padding-top: .7rem; border-top: 1px solid var(--line); }
/* min-width: 0 - without it, "Send" runs off screen at 320px.
   An <input> has an implicit size="20", which in the mono font is a
   minimum width of about 239px. Measured at a 320px window: the button
   sat 23px off-screen, the label was cut to "Sen", and a quarter of its
   hit area wasn't tappable. Affects iPhone SE 1, old Android devices,
   split screen, and iOS display zoom. */
.composer input {
  flex: 1; min-width: 0; padding: .7rem .9rem; border-radius: var(--radius);
  background: var(--bg-1); border: 1px solid var(--line);
}
.composer input:focus { outline: none; }
:root[data-tastatur] .composer input:focus { border-color: var(--fokus); }

.empty { color: var(--dimmer); text-align: center; padding: 2.5rem 1rem; font-size: .9rem; }

/* Not enough held: instead of an input field that answers with an error
   message on submit, this states the reason up front. */
.composer.locked { display: none; }
.gate {
  display: flex; align-items: center; gap: .6rem;
  padding: .8rem .9rem; margin-top: .7rem;
  border: 1px solid var(--line); border-top-color: var(--line);
  border-radius: var(--radius); background: var(--bg-1);
  font-size: .9rem; color: var(--dim);
}
.gate .lock { color: var(--dimmer); flex: none; display: inline-flex; }
/* The quiet hint above the input (.gate.hint) used to sit here - "No
   links allowed here", the rate limit. It belonged to the chat and left
   with it.

   Worth noting what it solved, in case it ever comes back: as a normal
   element in the column, it ate up height when it appeared, the list
   above it is flex: 1, and the whole history jumped upward - right at the
   moment you wanted to read the message the hint was about. The fix was
   position: absolute on the input row, bottom: 100%. In the DMs a
   rejected message still reports through the red toast today; that's the
   spot where a box like this would be needed next. */
/* The amount already stands there bold - brightening it further turns
   one sentence into two levels, even though it's meant to be read in one
   pass. */
.gate strong { color: inherit; font-weight: 650; }

/* ---------------- Polls ---------------- */
/* No fill of its own, like the polls below it - see .poll. */
.poll-admin {
  border: 1px solid var(--line); border-radius: var(--radius);
  /* No margin of its own on top: .pane already brings one, and the two
     together doubled up - the box hung further from the header's line
     than the panels in the DMs. It stays on the bottom, where it
     separates the box from the list below. */
  /* 1.1rem, not 1rem: the same padding as .poll below. That way "NEW
     POLL" starts on the same vertical line as the poll questions in the
     list. */
  padding: 1.1rem; margin: 0 0 1rem; display: flex; flex-direction: column; gap: .5rem;
}
.poll-admin h3 { margin: 0 0 .3rem; font-size: .82rem; text-transform: uppercase; letter-spacing: .09em; color: var(--dimmer); }

/* ---- The box stays closed as long as nobody needs it ---------------------
   Collapsed, a single row remains, and the border is invisible: a border
   around one lone button would be a box that claims to contain something.

   Invisible here specifically means "transparent", not "gone", and the
   padding stays too. The reason is the motion on opening: strip out
   border and padding, and "NEW POLL" jumps 1.1rem right and down when it
   opens - the thing you just tapped runs out from under your finger. This
   way it stays pinned to the exact pixel it was at, and only the content
   below it unfolds.

   The one exception is the gap at the BOTTOM: that doesn't move the row,
   it would just be empty space under a lone heading. */
.poll-admin:not(.offen) { border-color: transparent; padding-bottom: 0; }
/* A rule used to sit here that made the duration invisible while
   collapsed without taking away its space - otherwise "NEW POLL" slid
   12px downward on opening. Since the duration moved to the bottom of the
   box, the header only contains the button, and the question doesn't
   come up anymore. */
.poll-admin:not(.offen) h3 { margin: 0; }

/* The toggle continues the look of the heading it sits in: same size,
   same letter spacing. It's brighter than the heading (--dim instead of
   --dimmer), because unlike a heading, it actually does something. */
.poll-neu {
  font: inherit; font-size: inherit; letter-spacing: inherit; text-transform: inherit;
  color: var(--dim); background: none; border: 0; padding: 0; cursor: pointer;
  display: inline-flex; align-items: baseline; gap: .4rem;
  transition: color .15s;
}
.poll-neu:hover { color: var(--text); }
/* Thumb-sized without touching the heading.
   ---------------------------------------------------------------------------
   Measured at 84 x 18px. This is the switch that gets Ansem to the form
   at all - on a phone, a target you tap three times before it lands. 18px
   is less than half the 44 a fingertip needs.

   The first attempt was min-height: 44px. That really did make the
   button bigger, and in doing so broke two guarantees this tab already
   had: the label lines up with the questions in the list below, and the
   box must not jump when it opens. Measured: 7.5px off the line, a 17px
   jump. Both without ever seeing the button grow - the row around it grew
   along with it.

   So only the HIT AREA grows, not the box. An absolutely positioned
   ::before sits outside the text flow: it shifts nothing, takes no space,
   and still catches the finger. 13px above and below turn 18px into
   exactly 44. */
.poll-neu { position: relative; }
.poll-neu::before {
  /* -15px, not -13px: 18 + 2x13 is exactly 44, and exact cuts it too close
     here. The button sits on a fractional pixel line, so the browser
     rounds the area down to 43 depending on the screen - measured in the
     phone test run, not guessed. Two extra pixels of slack cost nothing,
     since the area is invisible and doesn't touch the layout. */
  content: ''; position: absolute; inset: -15px -10px;
}

/* The glyph rotates into a minus instead of being swapped out: a glyph
   that moves says "same thing, different state". Two different glyphs
   would look like two different buttons at the same spot. */
.poll-neu .zeichen {
  display: inline-block; font-size: 1.15em; line-height: 1;
  transition: transform .18s ease;
}
.poll-admin.offen .poll-neu .zeichen { transform: rotate(45deg); }
@media (prefers-reduced-motion: reduce) {
  .poll-neu .zeichen { transition: none; }
}

/* While the form is open, the whole area scrolls.
   ---------------------------------------------------------------------------
   The finding, measured on an iPhone with five answers and the keyboard
   open: 508px are visible, the form ends at 604 - "Start poll" sits 95px
   below the edge, with no way to reach it. .pane is a flex stack with no
   scrolling of its own, and the list below it (#poll-list) does scroll,
   but it isn't what's overflowing.

   So the area itself scrolls - but only while the form is open:

     :has(#poll-admin-felder:not([hidden]))

   Two nested scroll areas would otherwise be a trap. Go through the list
   with a finger on a poll, and the list scrolls; grab next to it, and the
   area scrolls. As long as Ansem isn't creating anything, there's still
   exactly one.

   And #poll-list gives up its own scrolling during this time: otherwise
   the list would be trapped in a fixed-height box while the area above it
   scrolls. */
.pane:has(#poll-admin-felder:not([hidden])) {
  overflow-y: auto; overscroll-behavior: contain;
}
.pane:has(#poll-admin-felder:not([hidden])) #poll-list {
  /* flex: 1 0 auto, not flex: none.
     -------------------------------------------------------------------------
     With "none" the list shrinks to its content as soon as the form
     opens - and the "no polls yet" message centered inside it jumps nine
     pixels. Caught by test-polls-tab.mjs, which already had exactly this
     guarantee: it stays put when the form opens.

     1 0 auto means: if there's space left over, the list fills it (the
     message stays centered); if the content is bigger, it grows past that
     and the area scrolls. Both are needed, exactly. */
  overflow: visible; flex: 1 0 auto;
}

#poll-admin-felder { display: flex; flex-direction: column; gap: .5rem; }
.poll-admin input {
  padding: .6rem .8rem; border-radius: var(--radius); background: var(--bg); border: 1px solid var(--line);
  transition: border-color .15s;
}
/* These three fields were the only ones on the page with no focus rule of
   their own - and so the only ones still showing the browser's blue
   system ring. Login and both DM fields had already claimed that spot.

   Nothing gets added around the field: no ring, no glow. A second outline
   next to the border the field already has is two lines for one piece of
   information. The border itself just gets brighter - that's all it
   needs, and anything more draws more attention than the information is
   worth. */
.poll-admin input:focus { outline: none; }
:root[data-tastatur] .poll-admin input:focus { border-color: var(--fokus); }
#poll-options { display: flex; flex-direction: column; gap: .4rem; }

/* ---------------------------------------------------------------------------
   The character counter in the field

   As on X: a small "12 / 60" on the right inside the field, not below it.
   Below the field it would need its own row per answer - with ten
   answers, that's ten rows for a number nobody usually cares about.

   The label is the reference point; it wraps both field and number, so
   the number can sit absolutely positioned inside it. A label, not a div,
   because a click on the number then lands in the field instead of going
   nowhere.

   When it's visible:
     * while you're in the field (:focus-within)
     * and also as soon as it gets tight - the last ten characters.
   The second case is the more important one. Paste in a long text and
   click away, and you should see you're at the limit without clicking
   back in. Always visible, on the other hand, would be ten instances of
   number clutter next to ten empty fields.

   visibility instead of display: none, so the number doesn't rearrange
   anything on entering the field - same pattern as .dm-s.

   The color is --dim, not --dimmer, even though --dimmer is otherwise the
   color for secondary things: measured 3.38:1 on --bg. For 10.8px text
   that's under every threshold, and of all moments, this number shows up
   exactly when it's meant to be read. --dim is 6.40:1, --warn at full is
   7.09:1.
   --------------------------------------------------------------------------- */
.zaehl-feld { position: relative; display: block; }
.zaehl-feld > input { width: 100%; }
.zaehler {
  position: absolute; right: .7rem; top: 50%; transform: translateY(-50%);
  font-size: .72rem; font-variant-numeric: tabular-nums;
  color: var(--dim); pointer-events: none;
  visibility: hidden;
}
.zaehl-feld:focus-within .zaehler,
.zaehl-feld.ist-knapp .zaehler { visibility: visible; }
.zaehl-feld.ist-voll .zaehler { color: var(--warn); }

/* "+ Option" on the left, "Start poll" on the right.
   "Start poll" hangs off margin-left: auto, not space-between, and that's
   the whole difference: at the last allowed answer, the plus disappears
   (display: none), and with space-between, "Start poll" would then be the
   only child - so it would sit flush left. The button would jump clear
   across the row the moment the fourth answer was added, exactly when
   you're about to press it next. With margin-left: auto it stays put. */
.row { display: flex; gap: .5rem; }
.row #btn-create-poll { margin-left: auto; }

/* Heading on the left, duration on the right.
   Wrapping, because on a phone "NEW POLL" and five steps don't fit on
   one line. On wrap, the group slides under the heading and stays flush
   right there - margin-left: auto works in both cases. */
.poll-admin-kopf {
  display: flex; flex-wrap: wrap; align-items: center;
  gap: .5rem .8rem; margin-bottom: .3rem;
}
.poll-admin-kopf h3 { margin: 0; }
/* This hint belongs to the phone - on desktop it's wrong, since it works
   fine there. It's hidden with display: none and not the hidden
   attribute: that attribute belongs to app.js, and two places toggling
   the same element eventually get into a fight. */
.poll-nur-rechner { display: none; }

/* The duration: days, hours, minutes.
   ---------------------------------------------------------------------------
   A single value in hours used to sit here, framed like Ansem's threshold
   field, with a plus and a minus on either side. It sat in the header of
   the create box, next to "+ New poll".

   Three boxes no longer fit there - tight on desktop, not at all at
   390px - so the block moved down into the box, like on X.

   The shape of a field: small label on top, big value below, arrow on
   the right. This is the only spot on the page with a select field, which
   is why the label sits INSIDE the box instead of next to it: a select
   that shows only a number doesn't say what it's a number of. */
.lz-block { display: flex; flex-direction: column; gap: .35rem; }
/* Same size as "Runs for" before, for the same reason: it labels the
   block, it isn't a footnote to it. */
.lz-titel { font-size: .82rem; color: var(--dimmer); }
.lz-reihe { display: flex; gap: .5rem; }
/* Equal width, growing together. Fixed widths would be tempting here -
   the longest label is "Minutes" - but they didn't add up at 390px. */
.lz-feld {
  flex: 1; min-width: 0; position: relative;
  background: var(--bg); border: 1px solid var(--line); border-radius: var(--radius);
  transition: border-color .15s;
}
.lz-name { display: block; font-size: .72rem; color: var(--dimmer); }
.lz-zahl { display: block; }
/* The button is the whole box, label included.
   ---------------------------------------------------------------------------
   A <button>, not a <select>, and that's the core of this round: a
   <select> counts as :focus-visible after a mouse click, a button
   doesn't - which means the page's focus ring no longer applies there.

   The button filling the whole box instead of just the number is the
   second part of it: otherwise the tappable target on a phone would be
   24px tall, inside a box that looks like 44.

   padding-right leaves room for the arrow: without it, a two-digit
   number runs under it. */
.lz-wahl {
  appearance: none; background: none; border: 0;
  padding: .35rem 1.5rem .35rem .6rem;
  font: inherit; font-family: var(--mono); font-size: .95rem; color: var(--text);
  cursor: pointer; text-align: left; width: 100%; display: block;
  line-height: 1.35;
}
/* The arrow is drawn, not a character: a "v" from the mono font sits too
   high, is too wide, and carries the font's serifs with it. Two edges of
   a rotated square are the same shape at any size. */
.lz-feld::after {
  content: ''; position: absolute; right: .6rem; top: 50%;
  width: 7px; height: 7px; margin-top: -5px;
  border-right: 1.6px solid var(--dimmer); border-bottom: 1.6px solid var(--dimmer);
  transform: rotate(45deg); pointer-events: none;
  transition: transform .12s;
}
/* Open, it points up. Same motion as the plus at "New poll" - the glyph
   rotates instead of being swapped out. */
.lz-feld:has([aria-expanded="true"])::after {
  transform: rotate(-135deg); margin-top: -2px;
}

/* The dropdown.
   ---------------------------------------------------------------------------
   It's the second reason there's no <select> here: the OS draws that
   dropdown, and its height follows the number of entries. Eight for
   Days, twenty-four for Hours, sixty for Minutes - three different-height
   dropdowns under three identical-looking boxes.

   Here the height is FIXED, the same for all three. It's chosen so that
   even the shortest list (Days, eight entries) overflows - otherwise Days
   would be the exception again. Anything beyond that scrolls.

   Above the rest of the page (z-index), with a background that covers
   what's underneath: it sits over the poll list below it. */
.lz-liste {
  position: absolute; top: calc(100% + .25rem); left: 0; right: 0; z-index: 20;
  /* 11.5rem was four entries, now that these are 44px tall - a lot of
     scrolling for a single number out of sixty minutes. 14rem is five and
     leaves some air at the bottom: the list opens DOWNWARD, and at 17rem
     its edge sat ten pixels past the bottom of the screen. */
  max-height: 14rem; overflow-y: auto; overscroll-behavior: contain;
  background: var(--bg-1); border: 1px solid var(--line); border-radius: var(--radius);
  padding: .25rem; box-shadow: var(--schatten-hoch);
  scrollbar-width: thin; scrollbar-color: var(--bg-3) transparent;
}
.lz-liste::-webkit-scrollbar { width: 8px; }
.lz-liste::-webkit-scrollbar-thumb { background: var(--bg-3); border-radius: var(--radius-klein); }
/* Full width, thumb height.
   ---------------------------------------------------------------------------
   Measured at 27 x 32px - a target the size of a pea, sixty of them
   stacked up when choosing minutes. The width came from the entry only
   being as wide as its digit; you'd only hit it by tapping exactly on the
   number, not the row.

   display: block takes the full width, min-height the height. On desktop
   this changes nothing visible except a bigger hit area. */
.lz-eintrag {
  display: block; width: 100%; text-align: left;
  min-height: 44px; padding: .28rem .6rem; border-radius: var(--radius-klein); cursor: pointer;
  font-family: var(--mono); font-size: .88rem; color: var(--dim);
}
/* The marker is what the arrow keys point at - not the selection. It
   also follows the mouse cursor, so mouse and keyboard don't show two
   different "here I am"s. */
.lz-eintrag.ist-marke { background: var(--bg-2); color: var(--text); }
/* The selected value stands out brighter, even when the marker sits
   elsewhere. */
.lz-eintrag[aria-selected="true"] { color: var(--text); font-weight: 650; }
/* Disabled means: this value, combined with the other two, would push
   the total past a week. It still shows up - so you can see the limit is
   a limit, not a hole in the list. */
.lz-eintrag[aria-disabled="true"] {
  color: var(--line); cursor: default; background: none; font-weight: 400;
}

.lz-hinweis { margin: 0; font-size: .74rem; color: var(--dimmer); }

/* The polls sit right on the page background, no box around them.
   ---------------------------------------------------------------------------
   A border used to sit around the whole tab for a while, like in the DM
   tab. It's gone again. What DID stay from that round, because it's
   right independently of that: the border as a shared class (.rahmen) for
   the three panels in the DM tab, and centering the empty-state message
   further down. */
.poll-list { flex: 1; min-height: 0; display: flex; flex-direction: column; gap: 1rem; padding-bottom: 1rem; }
/* The empty-list message sits centered - centered in the PAGE, not the
   list.
   ---------------------------------------------------------------------------
   The difference only shows up for Ansem, but there it shows up
   constantly: open the create box, and it grows by a good 200px, the list
   below it shrinks accordingly, and its center shifts down by half of
   that. Measured, that's 107px - the sentence visibly slides away when
   the box opens, even though nothing about the sentence itself changed.

   So the calculation works backward from the list's center by half the
   height of the box above it; --poll-kopf carries that value and is
   maintained in app.js (see beobachteKopfHoehe). Without the variable -
   for a user who doesn't have the box at all - it stays the list's
   center, and that IS the page's center there.

   The max() is the cap toward the top: on a short screen the page's
   center would otherwise land in the middle of the form, and the sentence
   would sit inside the input field. Then the distance from the top wins,
   and it does slide after all - shifted is better than overlapping.

   A version with "safe center" and a bottom padding used to sit here for
   a while. It came along with the border that sat around the whole tab
   for a while: that cost .9rem top and bottom, which ate into the space
   this calculation needed - the sentence sat with half its height inside
   the form. Without the border the space is back, and with it the
   version measured here, which is the longer-lived one.

   Deliberately only here and not on .empty itself: in an empty
   conversation the sentence intentionally hangs at the bottom, where the
   bubbles hang too (see .chat-list > *:first-child). A shared rule would
   pull it upward there too - a change to a different spot that nobody
   asked for. */
.poll-list { position: relative; }
.poll-list > .empty {
  position: absolute; left: 0; right: 0; margin: 0;
  top: max(2.2rem, calc(50% - var(--poll-kopf, 0px) / 2));
  transform: translateY(-50%);
}

/* The card has a fill - the same one the downloadable image has.
   ---------------------------------------------------------------------------
   History, so nobody rebuilds the old bug: this tab once had FOUR fills
   stacked on top of each other - page background, card, reply row, edge -
   at steps of 1.05 / 1.09 / 1.14:1. Each one on its own sits below what
   the eye reads as intentional. The result was "a lot of grays", none of
   which said anything, which is why all of it got thrown out back then.

   Exactly ONE of them comes back now, the one the image also carries:
   --bg-1 under the card. The reply row stays without a fill of its own,
   and so does the create box. That leaves page, card and fill stacked -
   three layers, not four - and the row inside the card still gets its
   shape from the edge.

   The real win isn't the fill itself, it's that the page and the image
   now show the SAME thing. Before, the card was borderless on the page
   and filled in the download; share an image and then open the page, and
   you'd see two different things.

   The price, measured: the edge of the bar fill sits at 1.54:1 on the
   card, instead of 1.62:1 on the page background - the card is brighter,
   so the difference to the fill is smaller. It's exactly the value the
   download image always had.

   --bg-3 also lives on in the tab in two small markers: the selected
   duration button and the CLOSED badge. That's deliberate - those are
   markers, not layers of the structure, and there the fill is what
   carries the meaning. */
.poll {
  border: 1px solid var(--line); border-radius: var(--radius); padding: 1.1rem;
  background: var(--bg-1);
}
/* Question on the left, the two buttons on the right. align-items:
   flex-start so they stay at the top with a two-line question instead of
   sliding to the middle - top right means top right. */
.poll-head { display: flex; align-items: flex-start; gap: .8rem; margin-bottom: .2rem; }
.poll h4 { margin: 0; font-size: 1.05rem; min-width: 0; }
.poll-tools { flex: none; margin-left: auto; display: flex; gap: .15rem; }
/* The buttons' border is invisible at rest: two more boxes next to a
   heading would be too much weight. It comes back on hover, so you can
   tell they're buttons, not just little pictures. */
.poll-tools .icon-btn { border-color: transparent; }
.poll-tools .icon-btn:hover:not(:disabled) { border-color: var(--line); }
.poll-tools .icon-btn.is-copied { color: var(--accent); border-color: var(--accent); }

/* The delete button looks like its neighbors - no warning color, neither
   at rest nor after the first tap. The confirmation lives in the icon:
   the trash can turns into a checkmark, and only a tap on THAT deletes.

   No rule of its own needed: the checkmark comes from the markup, and the
   states above it (border on hover) apply equally to all three buttons.

   In this state the button carries the class knopf-aktiv. It's
   DELIBERATELY unstyled: it only remembers which icon is currently
   showing, so knoepfeMalen() doesn't overwrite innerHTML every time the
   list rebuilds. Give it a color here and you turn a note into a signal -
   and then the delete button looks dangerous after the first tap after
   all. The visible signal for "copied" is is-copied, above. */

/* The brief highlight after a shared link: click it, land in a list, and
   you should see right away which card was meant. After two seconds
   that question is answered, then the highlight disappears again - a
   permanent one would be pure decoration past that point. */
.poll.is-linked { animation: poll-linked 2.4s ease-out 1; }
@keyframes poll-linked {
  0%, 55% { border-color: var(--accent); background: rgba(var(--accent-rgb), .05); }
  /* --bg-1 again, not transparent: the animation's end frame has to land
     where the card sits at rest. transparent used to sit here for a
     while - right as long as the card had no fill of its own, and wrong
     the moment it got one. Otherwise the card briefly flashes through to
     the page background at the end of the two seconds. */
  100%    { border-color: var(--line);   background: var(--bg-1); }
}
@media (prefers-reduced-motion: reduce) {
  .poll.is-linked { animation: none; border-color: var(--accent); }
}
.poll-meta { font-size: .76rem; color: var(--dimmer); margin-bottom: .9rem; display: flex; gap: .6rem; flex-wrap: wrap; align-items: center; }
.poll-meta .btn { margin-left: auto; font-size: .76rem; }
.opt { position: relative; margin-bottom: .5rem; cursor: pointer; }
.opt.locked { cursor: default; }
/* min-height instead of height: with long answers or big numbers, the
   text used to break out of the bar - on a phone, every other line was
   clipped. */
/* Also without a fill of its own - see the reasoning at .poll.
   A side effect that makes things better, not worse: the fill now sits
   on the page background instead of on --bg-3, and mixes darker as a
   result. The vote count on top of it isn't hurt by that, it looks
   better. test-polls-tab.mjs checks the math on every run. */
.opt-bar {
  position: relative; border: 1px solid var(--line);
  border-radius: var(--radius); overflow: hidden; min-height: 44px; display: flex; align-items: center;
  transition: background .15s;
}
/* Under the pointer, the AREA gets brighter, not the border.
   ---------------------------------------------------------------------------
   border-color: var(--dim) used to sit here - a bright outline flashing
   around the whole row. But the border already has a job on this page:
   it draws the shape. Making it flash too means using the same signal
   for two different things.

   So the row's background gets brighter instead - and only the EMPTY
   part. The fill stays as it is. The original reason was the vote count
   sitting on the fill: a filter: brightness(1.14) pushed it from 4.6:1
   down to 3.8:1. That number doesn't exist anymore, so neither does the
   argument - what stayed is the weaker but sufficient one: a fill that
   also brightens on hover looks like the value is changing.

   The consequence worth knowing: an answer at 100% has no empty part and
   so doesn't visibly react to the pointer. That's the case where there's
   nothing left to decide anyway. */
.opt:hover:not(.locked) .opt-bar { background: var(--bg-3); }

/* Solid color, hard edge - like the image the download button produces.
   A gradient into violet used to run here, fading out softly after 20px.

   The soft edge had a reason: the answer text sits over the bar, and a
   vertical line running straight through a word looked like a rendering
   glitch. At 24% opacity the text reads equally well on both sides of the
   edge, so that objection no longer holds - and the edge now says
   something the gradient used to swallow: the share ends exactly here.

   Since the switch to X's colors, the fill is OPAQUE instead of
   translucent. That brings back a bit of the old objection, but measured
   it doesn't hold up: the answer text sits on the fill at 10.0:1 (gray)
   and 6.0:1 (blue), and next to it on the empty part at 16.2:1 - readable
   on both sides of the edge, just at different brightness. The values
   themselves are listed above at --fuellung. */
/* The long form comes before the short form: browsers that don't know
   inset (iOS 14.0 to 14.4, Chrome below 87) skip the second line and keep
   the first. Without it, the fill has no height - checked directly: 228 x
   0px - and you see answer rows with no bar at all. Which answer is
   leading would then be impossible to tell. */
.opt-fill {
  position: absolute; top: 0; right: auto; bottom: 0; left: 0;
  inset: 0 auto 0 0;
  background: var(--fuellung);
  transition: width .5s cubic-bezier(.2,.8,.2,1);
}
/* The WINNING answer is blue - on X the only color signal in a poll.

   Blue only appears once the poll has closed. Before that, the bar
   lengths already say where things stand; a color on top of that turns it
   into an announcement, and an announcement about an intermediate result
   is a nudge. Which class gets applied here, and when, lives in app.js at
   fuehrenderAnteil() - this file doesn't decide it.

   It doesn't collide with the checkmark that says "this is how you
   voted". That's a mark IN the text, blue is a fill underneath it; both
   on the same line reads as "you voted for the winner", and that's
   legible because the two marks are different kinds of thing.
   A white border used to sit here for the same statement. It's gone: a
   border that sometimes means shape and sometimes means state ends up
   saying neither. */
.opt.leads .opt-fill { background: var(--fuellung-spitze); }
/* align-items: flex-start, not center.
   As long as the answer fits on one line, the two are the same. When it
   wraps - the normal case on a phone - center puts the amount at
   half-height, floating somewhere next to the second line. Aligned to
   the top, it sits where the answer starts, and every amount in a poll
   lines up on one line. */
.opt-text { position: relative; display: flex; width: 100%; align-items: flex-start; gap: .8rem; padding: .5rem .8rem; }
.opt-label {
  font-weight: 500;
  /* min-width: 0 and overflow-wrap, or a long answer eats the amount.
     -------------------------------------------------------------------------
     A flex item's minimum width is its content, and for text "content"
     means the longest WORD. An answer with no spaces - an address, a
     link - therefore never shrinks, pushes .opt-num right out of the bar,
     and .opt-bar clips it there. The amount isn't squeezed then, it's
     gone.

     Measured: at 320px, 17 characters with no spaces is enough; at 390px,
     25. The database allows 60 characters per answer.

     The same bug hits ordinary text too at 200% system font size: "$1,640,000"
     became "$1,640,0". A truncated number is worse than a missing one -
     it reads as a different amount. */
  min-width: 0; overflow-wrap: anywhere;
}
/* The checkmark on your own vote. It takes its color from the text above
   it - so it holds up equally well on any fill as the answer text itself,
   and needs no value of its own. The -.18em lifts it to the middle of the
   lowercase letters; on the baseline it would visibly sit too low. */
.opt-haken { margin-left: .4em; vertical-align: -.18em; }
/* Only for the screen reader, not there for the eye. Not display: none -
   that would also be skipped by the screen reader. */
.nur-vorlesen,
/* The three characters under Ansem's avatar. They stay in the document so
   a screen reader can say who this is - a picture with no text says
   nothing to it. Only where the picture is actually used: in the header.
   In the quote and the inbox, the characters stay visible. */
.me .handle.admin-name .kuerzel {
  position: absolute; width: 1px; height: 1px; overflow: hidden;
  clip-path: inset(50%); white-space: nowrap;
}
/* WARNING, and this applies to EVERY new spot for .nur-vorlesen: the
   parent element must be positioned.
   ---------------------------------------------------------------------------
   position: absolute looks for the nearest positioned ancestor. Find
   none, and the reference becomes the whole document - and then the
   element doesn't sit "1px inside the list", it sits 1px at the spot it
   would occupy in the UNSCROLLED history. With forty messages that's
   quickly 1500px below the window, and the document grows to match.

   What that looked like: the page suddenly became scrollable. You'd
   scroll through the history, the wheel would slip through onto the
   document at the end of the list, and the whole app would ride up and
   out of view. Measured: a 1493px document in a 738px-tall window - 755px
   that came from two dozen 1px spans, none of them visible. Back then it
   was Ansem's handles in the chat; in a long poll list (.opt-label) it
   would be the same thing.

   overflow: hidden and clip-path don't help against this. Both hide the
   CONTENT; the area the box occupies in the document stays where it is.
   That's why the reference point is spelled out here as its own rule
   rather than tucked into another one. test-seite-hoehe.mjs checks the
   math on every run. */
.me .handle.admin-name,
.opt-label { position: relative; }
/* The winning answer changes COLOUR and nothing else.
   ---------------------------------------------------------------------------
   There used to be a rule here setting .opt.leads .opt-label to 650, so the
   winning line was drawn bolder as well - the same thing X does. It is gone
   on purpose, and the amount beside it never had it either: a row that wins
   should not also start setting its own type.

   Worth writing down, because the old note argued the other way and the
   argument was not silly: the bold was the signal "that comes through even
   without colour". Losing it does not leave colour alone, though - the bar
   LENGTH marks the winner too, and it is the longest one by definition, and
   the largest amount is printed at the end of that row. Both survive any
   colour vision. Where colour does carry more weight now is a near-tie, when
   two bars are nearly the same length; there the amounts are what settle it,
   and they are text.

   What the class still does: the fill (see .opt.leads .opt-fill). And it is
   only ever set on a CLOSED poll - see leadingShare() in app.js, which
   returns null while a poll is running. That is deliberate: an ongoing poll
   should have no frontrunner at all, not even a quiet one. */

/* The amount sits on the right, and nothing else.
   A vote count sat here, then a percentage, one after the other. The
   percentage is a matter of taste and got thrown out again - it said the
   same thing as the bar length next to it.

   The vote count is not a matter of taste and must not come back:
   spread 100k across ten wallets and the amount doesn't change, but one
   vote becomes ten. The amount can't be inflated this way, because in
   this round one vote weighs as much as the balance behind it. */
/* flex: none also sat here briefly, so the amount wouldn't give up any
   width. No case could be made for it: neither a 60-character answer nor
   a single long word with no break point ever squeezed or wrapped the
   number. So it's out - a line claiming to hold something it never
   actually does is exactly the kind nobody dares touch the next time.
   The two checks for this live in scripts/test-polls-tab.mjs and stay
   there: if the case ever does show up, it'll surface there. */
/* flex: none - the amount is the last thing to give, if anything does.
   Combined with min-width: 0 on .opt-label, that means: the text yields,
   the number stays intact. Reversed, it would be wrong. */
/* line-height as a LENGTH, and the same length the answer next to it gets.
   ---------------------------------------------------------------------------
   1.25 sat here, left over from when this block held two stacked lines - the
   amount and the vote count under it. The second line is gone (see --anteil
   further up), and what stayed behind was a line box 18 px tall next to a
   label 23 px tall. Both start at the same top edge, because .opt-text aligns
   to flex-start - so the shorter box ended up with its middle 2 px higher
   than the answer's, and the amount looked like it was sitting up.

   Measured: label centre 22 px from the top of a 44 px bar, amount centre
   20 px.

   Not fixed with align-self: center, which would centre the amount in the
   ROW - fine on one line, but on a wrapped answer it would float beside the
   second line, which is the thing .opt-text avoids on purpose. A line box of
   the same height fixes the middle without moving the top: one line of body
   text, written as the root size times the body's line-height rather than as
   22.5px, so it follows if either one ever changes. */
.opt-num {
  margin-left: auto; flex: none; white-space: nowrap;
  font-family: var(--mono); text-align: right; line-height: calc(1rem * 1.5);
}
.opt-num .held { display: block; color: var(--text); font-size: .95rem; font-weight: 650; }
/* The leading amount keeps the colour every other amount has.
   ---------------------------------------------------------------------------
   It used to take the accent. That said the same thing twice: the bar under
   it is already the signal, and a coloured figure beside a coloured bar
   competes with it instead of adding to it. The row is the winner because it
   is filled, not because its number is a different colour. */
.closed-tag { font-size: .68rem; background: var(--bg-3); border: 1px solid var(--line); border-radius: var(--radius-klein); padding: 0 .3rem; }

/* The time remaining in a poll's header.
   Brighter than the rest of the row (--dim instead of --dimmer): what's
   next to it is information, "closes in 3h" is a reason to tap now
   instead of later. */
.frist-rest { color: var(--dim); }
/* Under an hour. The first use of --gold on the page - deliberately a
   color that appears nowhere else, so it can't be mistaken for anything
   else. No blinking, no motion: this line sits above a list you're
   reading, and nothing twitching in it gets read anymore. */
.frist-rest.is-bald { color: var(--gold); }

/* ---------------- DMs ---------------- */

/* The border every panel shares: Ansem's inbox, his open conversation, a
   regular user's window - and the poll list.
   ---------------------------------------------------------------------------
   Before, only Ansem had a box. A regular user saw the same bubbles
   floating free on the page background, and that read like two different
   programs: a window for him, a bare page for everyone else.

   So the line and the radius live in ONE place. Change the border and you
   change all of them - otherwise it's exactly the one you forgot that
   stands out, and only once two of them are on screen side by side.

   The FILL is deliberately not part of this, it hangs off .gefuellt:
   ---------------------------------------------------------------------------
   In the DMs, content sits directly in the box - bubbles and rows need a
   surface under them. The poll list, by contrast, contains CARDS, and
   each one already carries that same fill. Both together would mean
   --bg-1 on --bg-1: boxes inside boxes with no difference between them.
   So there, the box stays a bare outline, and the cards inside it keep
   standing out. */
.rahmen {
  border: 1px solid var(--line);
  border-radius: var(--radius);
}
.rahmen.gefuellt { background: var(--bg-1); }

/* .dm-user carries the border and so has to carry the padding that
   .thread-view has for Ansem too - otherwise bubbles and the input would
   stick to the edge. None on top: the list starts there, and it brings
   its own spacing. */
.dm-user {
  flex: 1; min-height: 0; display: flex; flex-direction: column;
  padding: 0 .9rem .9rem;
}
.dm-admin { flex: 1; min-height: 0; display: grid; grid-template-columns: 290px 1fr; gap: 1rem; }

/* The inbox does NOT scroll as a whole - only the rows inside it.
   Otherwise the bar on the right would run the full height of the
   column, including next to the header, the threshold and the hidden
   row: three things that stay fixed, with a slider next to them that
   means nothing to them.
   Clipping does NOT happen here, but in the two pieces below - each with
   the corners that belong to its own side. overflow: hidden on the
   column would be simpler, but it would block exactly what the scroll
   marker on the right needs: the marker sits ON the border and so a pixel
   outside this box. */
.thread-list { display: flex; flex-direction: column; min-height: 0; }
#thread-items {
  flex: 1; min-height: 0;
  border-radius: 0 0 var(--radius-innen) var(--radius-innen);
}

/* The whole block stays put while scrolling, not just the heading. With
   fifty conversations the list runs several screens long; without this,
   the threshold would be gone after two swipes - and with it the sign
   that the inbox is filtered at all. That's exactly the one thing that
   must not disappear: miss it, and a filtered list reads as the whole
   list.

   It no longer sits sticky inside the scrolling box for this reason, it
   sits next to it: same effect, without position: sticky - and that's why
   the bar now only starts below it. */
.thread-top-bar {
  flex: none;
  background: var(--bg-1); border-bottom: 1px solid var(--line);
  /* Its own top corners, now that the column no longer clips. Minus the
     border width, or the rounding sits a pixel too far out and the border
     shows through at the corner. */
  border-radius: var(--radius-innen) var(--radius-innen) 0 0;
}
.thread-head {
  padding: .7rem .8rem; border-bottom: 1px solid var(--line);
  font-size: .74rem; text-transform: uppercase; letter-spacing: .08em; color: var(--dimmer);
  display: flex; justify-content: space-between; gap: .4rem;
}
.thread-head .dim { text-transform: none; letter-spacing: 0; font-size: .7rem; }

/* Ansem's threshold reuses the .filters and .filter-group building
   blocks and so needs almost no styling of its own. Only the column is
   narrow here instead of screen-wide: the input field takes the full
   width. */
#dm-min-box {
  padding: .7rem .8rem;
  flex-direction: column; align-items: stretch; gap: .5rem;
}
#dm-min-box .filter-group { width: 100%; }
#dm-min-box .filter-group input { width: 100%; min-width: 0; }


/* The row for manually hidden ones: count on the left, toggle on the
   right. It sits in the header bar under the slider and stays put while
   scrolling for that reason. The divider on top sets it apart from the
   slider.

   A second row of the same shape used to sit above it - the counter for
   the threshold. It's gone, class .thread-hidden and all: the slider
   sits right above and says the same thing. */
.thread-versteckt {
  display: flex; align-items: center; justify-content: space-between; gap: .5rem;
  margin: 0; padding: .5rem .8rem;
  border-top: 1px solid var(--line);
  font-size: .74rem; color: var(--dimmer);
}
.btn-mini {
  font-size: .72rem; padding: .18rem .45rem; border-radius: var(--radius-klein);
  min-height: 0; flex: none;
}
/* A dimming for hidden rows used to sit here: they were mixed in among
   the others and needed to stand apart. The toggle now shows them
   INSTEAD OF the others - every row in that list is then hidden, and
   dimming all of it just pales the whole screen without saying anything.
   Which list you're looking at is stated in the row above. */

/* Title row and hide button. This row carries the divider so it runs the
   full width - keep it on .thread-title instead and it would end before
   the button. */
.thread-kopf {
  display: flex; align-items: center; gap: .6rem;
  border-bottom: 1px solid var(--line);
}
.thread-kopf > .thread-title { flex: 1 1 auto; min-width: 0; border-bottom: 0; }
.thread-hide { flex: none; }

/* One row per conversation: handle, preview, amount - in that order,
   because you read left to right, and the amount is the sort order you
   only check, not search for.

   No colored bar for the open conversation, no red badge for unread. Both
   claimed something that isn't true: a selection isn't important, it's
   just selected, and an unread message isn't urgent. */
.thread {
  display: flex; align-items: baseline; gap: .45rem;
  padding: .5rem .8rem; border-bottom: 1px solid var(--line);
  cursor: pointer; width: 100%; background: transparent; text-align: left;
  font: inherit; color: inherit; border-inline: 0; border-top: 0;
}

/* Unopened: a faintly blue background, preview text one step brighter.
   Two cues, both quiet.

   The color is the handle tone (--dim pulled toward blue), turned way
   down. Blue, not brighter, because brightness already means something
   else here: it stands for "open". Two brightness levels would have been
   confusable - color for one state, brightness for the other, aren't.

   The order of these three rules matters: .thread.is-unread,
   .thread:hover and .thread.is-active are equally specific (0,2,0), so
   the last one wins. A conversation under the pointer and an open
   conversation should show their own background, not the blue one.
   Reorder these and you flip the behavior. */
.thread.is-unread { background: var(--ungelesen-fuellung); }
/* Unread is shown twice: a tinted row and brighter text. The text part
   used to be --dim against --dimmer; now that the preview row itself
   sits at --dim, that would no longer be a difference. Hence one step
   brighter, at --text. The tinted background already carries the signal
   anyway - the text is the confirmation, not the only cue. */
.thread.is-unread .thread-prev { color: var(--text); }

.thread:hover { background: var(--bg-2); }
.thread.is-active { background: var(--bg-3); }
.thread .h { font-family: var(--mono); font-weight: 700; flex: none; }
/* Someone in the inbox decides which conversation to open based on the
   balance - that's exactly the information this row is supposed to give.
   The values come from the chat row, which answered the same question;
   that row is gone, but the number lives on here. */
.thread .w {
  font-size: .86rem; font-weight: 600; color: var(--worth);
  font-family: var(--mono); flex: none;
  /* Pushes the amount to the right edge, now that the preview before it
     no longer fills all of the remaining space. */
  margin-left: auto;
}
/* The dot for an unread message - to the left of the amount.
   ---------------------------------------------------------------------------
   Why there and not next to the name: in this list, the eye goes right,
   toward the amount. That's where you decide which conversation to open,
   and that's where the dot catches the eye without having to read the
   row.

   As a ::before on the amount, not as its own element in the markup:
   that way renderThreads() doesn't need to know anything about it. The
   state is already in the row's class.

   The color is #4b9aea, and it isn't made up: it's X's DM blue, already
   measured once in the color comment above and free since the bubble
   switched to --fuellung-spitze. --fuellung-spitze itself doesn't work -
   measured 2.57:1 on the list background, a 6px dot in it would be
   barely visible. This blue lands at 6.34:1.

   And not the name blue, .h.t0: that belongs to people. A saturated tone
   reads as a state, a muted one as someone. */
.thread.is-unread .w::before {
  content: ''; display: inline-block; vertical-align: middle;
  width: 6px; height: 6px; border-radius: 50%;
  background: var(--ungelesen);
  margin-right: .4rem; margin-top: -2px;
}
/* The preview ends earlier than it needs to.
   ---------------------------------------------------------------------------
   It used to be 185px, so 23 characters - and with forty conversations,
   22 of the 40 rows still got truncated anyway. More characters don't fix
   that: "I sold half my bag last week and no..." doesn't say more than "I
   sold half my bag...", it just makes the row more crowded.

   16ch instead of "as much as fits": that way the text ends at a fixed
   point, with air to its right. The amounts then form a column you can
   read top to bottom without previews of different lengths pulling the
   eye there. That's exactly the order this list has - sorted by balance.

   ch, not px: in the mono font, one ch is exactly one cell, so 16ch is
   always 16 characters, no matter what size the font is set to. */
/* "You:" before the preview, when Ansem wrote the last message.
   ---------------------------------------------------------------------------
   The SAME color as the preview, just set heavier. --dimmer used to sit
   here, one step darker, on the thinking: it's not a message, it's a
   statement of who wrote it. Checked against a message list that gets
   this right, it's the other way around - there both carry the same tone
   (#727a7a) and differ only in weight.

   The reason clicks once you see it side by side: "You: hi" is ONE
   sentence. Two brightness levels turn it into two things, and the eye
   reads a pause that isn't meant to be there. The weight says the same
   thing more quietly.

   The gap to the preview is tighter than the rest of the row (see
   margin-right below) - for the same reason.

   flex: none, so it's never truncated. "Yo..." would be worse than
   nothing at all. */
.thread-du {
  flex: none; font-size: .86rem; color: var(--dim); font-weight: 650;
  /* Tighter against the preview than the rest of the row. The row gap is
     .45rem and separates handle, text and amount - three different
     things. But "You:" and the message are ONE sentence, and a sentence
     keeps the same spacing internally as between two words, not the
     spacing between two columns. The negative margin removes the column
     gap again and leaves the word space standing. */
  margin-right: var(--du-abstand);
}

/* TWO widths, and that's the point: every preview ends at the SAME spot,
   whether "You:" sits before it or not.
   ---------------------------------------------------------------------------
   It used to be one width for everyone, and "You:" pushed the text four
   characters to the right. That meant every other row ended further out
   than its neighbors - a ragged edge in the middle of a list whose whole
   order consists of reading it top to bottom.

   Calculated, not measured: the text column is --vorschau wide. When
   "You:" sits in front, the preview gets exactly what "You:" takes up
   less - four cells plus the gap after it. Both are variables, so the
   math doesn't drift apart if someone changes the word or the gap. */
.thread-prev {
  flex: 0 1 auto; min-width: 0; max-width: var(--vorschau);
  font-size: .86rem; color: var(--dim);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.thread-du + .thread-prev {
  max-width: calc(var(--vorschau) - var(--du-breit) - .45rem - var(--du-abstand));
}
/* min-width: 0 isn't cosmetic here: as a flex item, this element won't
   shrink below its content width on its own. A long line in the reply bar
   used to push the whole page sideways off screen on narrow phones. */
.thread-view { display: flex; flex-direction: column; min-height: 0; min-width: 0; padding: 0 .9rem .9rem; }
/* Handle and address, nothing else. The address truncates - it's there
   for looking up, not for reading. */
.thread-title {
  display: flex; align-items: center; gap: .5rem;
  padding: .7rem 0; border-bottom: 1px solid var(--line);
  font-family: var(--mono); font-size: .85rem;
}
.thread-title .h { flex: none; }
.thread-title .addr {
  flex: 1 1 auto; min-width: 0;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
/* On desktop, the list and the conversation sit side by side - no back
   button needed. */
.back-inbox { display: none; }
/* Date separator. Centered, small, no lines - it should organize the
   history, not look like a message itself. The light border sets it
   apart from the background so it stays recognizable as a marker while
   scrolling. */
.day-sep {
  align-self: center; flex: none;
  margin: .5rem 0 .15rem; padding: .12rem .6rem;
  border: 1px solid var(--line); border-radius: 999px;
  font-size: .7rem; color: var(--dimmer); white-space: nowrap;
}
/* A bit more air between two days than between two messages. */
.day-sep + .dm-row { margin-top: .15rem; }

/* Message bubbles. They still carry the class .msg - from the chat, where
   it was once the row list. Since that's gone, .msg only exists here now;
   what's left of the old rule is listed above at ".msg".

   A grid inside, not a flex box. That's not convenience, it's the reason
   for the bug it fixes:

   With a flex box, the text decides its own width. A long text took up
   the whole row, and the timestamp slid onto a second one - which was
   the extra space under multi-line messages. With single-line messages
   both fit side by side, so it didn't show up there. In the grid, the
   cells are fixed: text on the left, timestamp on the right, quote above.
   There's no second line for anything to slide into.

   align-items: end keeps the timestamp level with the LAST line of text,
   not the first.

   The transparent border on incoming messages isn't an oversight: your
   own messages have a visible one, and without a counterpart both
   bubbles would end up exactly one pixel different in height. */
.msg.dm {
  display: grid; grid-template-columns: auto auto;
  grid-template-areas: "body meta";
  align-items: end; gap: 0 .5rem;
  /* auto instead of 1fr for the text column: with 1fr, the text took
     whatever was left after the timestamp - and since the bubble is also
     fit-content, there was nothing left to distribute. The result was a
     one-character-wide column: "Ttt" stacked vertically. With auto, the
     text takes its natural width and only wraps at max-width. */
  /* The bubble should hug its content, not fill the allowed width: an
     "ok" in a box 78% wide looks like a bug. max-width stays the upper
     limit at which the text wraps. */
  /* max-content, not fit-content: the bubble sits in a flex column, and
     there fit-content resolves against the minimum width of its columns.
     Because .body also carries overflow-wrap: anywhere - needed for long
     addresses - its minimum width is ONE CHARACTER. Result: "Ttt"
     stacked vertically. max-content takes the natural width, max-width
     caps it at the block, which sits at 78% of the row. */
  width: max-content; max-width: 100%; padding: .5rem .85rem;
  /* 18px instead of 12: on X, the bubbles are so round that a one-line
     one is almost a capsule. Not 999px - that would turn a three-line
     bubble into a sausage. 18 handles both cases. */
  border: 0; border-radius: var(--radius);
  background: var(--dm-fremd);
  color: var(--dm-fremd-text);
}
/* The block of header, quote and bubble. It carries the alignment that
   used to hang off the bubble: own messages flush right, incoming ones
   left. All three parts line up on the same edge - on X the quote sits
   exactly above the bubble, not next to it. */
/* The width lives on the BLOCK, not the bubble. On the first attempt,
   max-width: 78% sat on the bubble and min-width: 0 here - which shrank
   the block to zero, and the bubble resolved its fit-content against that
   zero: "Ttt" wrapped into three lines and the timestamp stuck to the
   text. */
.dm-block {
  display: flex; flex-direction: column; align-items: flex-start; gap: .25rem;
  max-width: 78%;
}
.dm-row.mine .dm-block { align-items: flex-end; }
/* Your own messages: filled versus outlined, not bright versus dark.
   ---------------------------------------------------------------------------
   Your own bubble used to be --accent-fill, so nearly white with dark
   text. Next to a dark bubble that was the loudest contrast on the whole
   page - of all things, for the messages you wrote yourself and need to
   read the least.

   The way out doesn't run through a third gray. Every fill in the palette
   sits within 1.4:1 of the others (--bg against --line); put two next to
   each other and they differ by 1.1 to 1.2:1, and nobody reads that as
   intentional. So the difference has to be CATEGORICAL, not gradual: one
   bubble is filled, the other just outlined. You see that at a glance,
   with no need to compare two grays.

   Both tones already live elsewhere on the page: --bg is the page
   background, --line is the border of every card and every field.

   Which of the two is "yours" is decided by which side it's on anyway -
   the fill is the second piece of information, not the first. */
.msg.dm.mine {
  background: var(--dm-eigen);
  color: var(--dm-eigen-text);
}
/* --dimmer isn't visible on the blue anymore. So the timestamp uses the
   same color as the text next to it, just turned down - the same ratio
   as in the dark bubble, just mixed the other way around. */
.msg.dm.mine .time { color: var(--dm-eigen-zeit); }
/* A link on blue: white and underlined. --worth would be nearly invisible
   there. */
.msg.dm.mine .body a { color: var(--dm-eigen-text); }
.msg.dm.mine .body a:hover { color: var(--dm-eigen-text); }

/* ---- The reply: header and quote bubble, both OUTSIDE the message bubble ----
   That's how X does it. The gain isn't just visual: inside the bubble,
   the quote used to sit sometimes on blue, sometimes on dark gray, and
   needed its own rule for each case. Above it, it always sits on the
   page background. */
.dm-antwort-kopf {
  font-size: .78rem; color: var(--dm-zitat-text);
  padding: 0 .3rem;
}
.dm-block > .quote {
  /* The base rule for .quote is an indented line - that's how the quote
     looked above a chat row. Here it's its own small bubble, so reset
     everything from that. */
  display: block; width: auto; max-width: 78%;
  margin: 0; padding: .35rem .85rem;
  border: 0; border-radius: var(--radius);
  background: var(--dm-fremd); color: var(--dm-zitat-text);
  font-size: .86rem; text-align: left;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.dm-block > .quote:hover { color: var(--dm-fremd-text); background: var(--dm-fremd); }
.dm-block > .quote.is-gone { font-style: italic; }

/* The row around the bubble. It has two jobs:

   It aligns the bubble - own on the right, incoming on the left - a job
   an automatic margin on the bubble itself used to handle.

   And it's the hover area for the reply arrow. The arrow sits next to the
   bubble, not inside it: inside, it would sit in the middle of the
   sentence and shove the text every time it appeared. If the hover area
   only covered the bubble, the arrow would vanish the moment you moved
   toward it. */
.dm-row { display: flex; align-items: center; gap: .35rem; }
.dm-row.mine { justify-content: flex-end; }
/* The arrow sits where there's spare space next to the bubble anyway.
   ---------------------------------------------------------------------------
   For a while it sat on the left for ALL messages, to match the side you
   swipe toward. That cost too much: the incoming bubbles moved off the
   left edge by the width of the arrow, and the edge is the anchor you use
   to recognize an incoming message. The finding on that: "the messages
   should sit normally at the edge, like before."

   So back to how it was - the arrow on the inner side, where the row is
   empty anyway: on the left for your own (flush-right) messages, on the
   right for incoming ones. Both bubbles end up sitting at their own
   edge.

   Swiping now goes RIGHT (see app.js), so away from the center. */
.dm-row.mine .reply-btn { order: -1; }
.dm-row .reply-btn { margin-left: 0; flex: none; }

@media (hover: hover) {
  .dm-row:hover .reply-btn { opacity: 1; pointer-events: auto; }
}
/* .dm-row.is-active .reply-btn { opacity: 1 } used to sit here - the
   arrow that appeared after tapping a message. Nobody sets that class
   anymore: touch uses swipe, desktop shows the arrow via :hover. The
   reasoning is in app.js at the click handler. */
.msg.dm .body { min-width: 0; overflow-wrap: anywhere; }
.msg.dm .time { font-size: .66rem; }

/* The rules for a quote INSIDE the bubble used to sit here. Since it
   sits above it now (see .dm-block > .quote), there's none there anymore
   - and with it, two special-case rules that contradicted each other
   fell away: a black veil for the dark bubble and a white one for the
   light one. */

/* ---------------- Toast ---------------- */
.toast {
  position: fixed; bottom: 1.2rem; left: 50%; transform: translateX(-50%);
  background: var(--bg-3); border: 1px solid var(--line); border-radius: var(--radius);
  /* 60, nicht 50: der Dialog liegt auf 55, und eine Fehlermeldung, die
     hinter ihm haengt, ist keine Meldung. */
  padding: .6rem 1rem; font-size: .86rem; z-index: 60; box-shadow: var(--schatten-toast);
}
.toast.err { border-color: var(--warn); color: var(--warn-text); }

/* ---------------- The card, before it is saved ----------------
   ===========================================================================
   Desktop only - see teilenDialog() in app.js. On a phone the share sheet
   already shows the image.

   z-index 55: over everything the page itself puts up - the top bar is 5,
   the deadline list 20, the tooltip 30, the install prompt 40. The toast was
   at 50 and has been raised to 60, because an error raised WHILE the dialog
   is open has to be readable; leaving it underneath would have hidden
   exactly the messages this dialog can produce. */
.dialog-grund {
  /* inset: 0 bezieht sich auf das Fenster - aber nur, solange dieses Element
     nicht in .app liegt. Dort traegt ein transform, und ein Transform wird
     zum Bezugsrahmen fuer alles fixed darin. Deshalb steht der Dialog in
     index.html ausserhalb von #app; die Begruendung im Langen steht dort. */
  position: fixed; inset: 0; z-index: 55;
  display: grid; place-items: center;
  padding: 1.5rem;
  /* Kein backdrop-filter mehr.
     Die Seite dahinter wird abgedunkelt, nicht unscharf. Der Weichzeichner
     war da, weil die Kopfzeile einen hat - aber der sitzt hinter einem
     schmalen Streifen, der mitscrollt, und hier lag er ueber der ganzen
     Seite. Was darunter liegt, ist die Liste, aus der die Karte kommt; sie
     unkenntlich zu machen, waehrend man ihr Bild prueft, nimmt einem den
     Vergleich weg. */
  background: rgba(var(--schatten-rgb), .38);
}
.dialog-karte {
  width: min(560px, 100%);
  max-height: 100%; overflow-y: auto;
  background: var(--bg-1); border: 1px solid var(--line);
  border-radius: var(--radius); box-shadow: var(--schatten-hoch);
  padding: 1.1rem 1.2rem 1.2rem;
}
.dialog-kopf {
  display: flex; align-items: center; justify-content: space-between;
  margin-bottom: .9rem;
}
.dialog-titel {
  font-family: var(--mono); font-size: .72rem; letter-spacing: .12em;
  text-transform: uppercase; color: var(--dim);
}
.dialog-zu {
  background: none; border: 1px solid var(--line); color: var(--dim);
  font-family: var(--mono); font-size: .9rem; line-height: 1;
  padding: .3rem .5rem; cursor: pointer; border-radius: var(--radius);
}
.dialog-zu:hover { color: var(--text); background: var(--bg-2); }
/* The image carries its own border because the card it shows has a pale
   ground of its own - without it the picture and the dialog run together. */
.dialog-bild {
  display: block; width: 100%;
  border: 1px solid var(--line); border-radius: var(--radius);
}
.dialog-satz {
  margin: .85rem 0 .25rem; font-size: .9rem; line-height: 1.5; color: var(--dim);
}
.dialog-stempel {
  font-family: var(--mono); font-size: .78rem; color: var(--dimmer);
  letter-spacing: .04em; margin-bottom: 1rem;
}
/* Not full width: the primary button elsewhere on the page fills its column
   because it is the only thing in it. Here it is one of three controls. */
.dialog-karte .btn-primary { width: auto; padding: .6rem 1.1rem; }

/* ==========================================================================
   Phone
   ==========================================================================

   The rules here follow four statements that weigh differently on a
   phone than at a desk:

   1. A thumb is not a mouse cursor. Apple and Google both name around
      44px as the minimum size for anything tappable. Below that, you miss.
   2. iOS zooms the whole page in the moment you tap a field whose text is
      smaller than 16px. That looks like a bug, and there's only one way
      to prevent it.
   3. Vertical space is scarce. Anything sitting above the conversation
      takes lines away from it.
   4. On newer phones, the home indicator sits at the bottom edge. Ignore
      it and the input field sits underneath it.
   ========================================================================== */

@media (max-width: 760px) {
  /* Creating polls only works on desktop.
     -------------------------------------------------------------------------
     Measured, the form with five answers and the keyboard open was taller
     than the visible area, and the "Start poll" button sat 95px below the
     edge. A scroll area helped against that - until something else got in
     the way on the device that couldn't be reproduced from here.

     Rather than keep tugging at a form nobody enjoys filling out on a
     phone anyway, the approach here is: the toggle disappears, the
     sentence appears. Everything else stays - viewing polls, voting,
     reading and writing DMs work unchanged on a phone.

     The form is additionally hard-hidden. Normally removing the toggle is
     enough, since without it, it's never opened; but open it on desktop
     and then narrow the window, and you'd still see it otherwise. */
  .poll-neu { display: none; }
  #poll-admin-felder { display: none; }
  .poll-nur-rechner {
    display: block; margin: 0;
    font-size: .82rem; letter-spacing: .04em; color: var(--dimmer);
  }
  /* And the scroll area that only existed for the open form isn't needed
     here anymore: with no form, there's nothing to overflow. Two nested
     scroll areas are a trap - see above. */
  .pane:has(#poll-admin-felder:not([hidden])) { overflow-y: visible; }

  /* --- Header: one row for identity and balance, one for the tabs --- */
  /* Back to flex: on a phone the header is meant to wrap - brand mark and
     balance on the first line, tabs as their own row below. A grid with
     fixed columns can't do that, and the tabs don't need to be centered
     here either: they take the full width anyway. */
  .topbar { display: flex; flex-wrap: wrap; gap: .5rem;
    padding: calc(.55rem + env(safe-area-inset-top, 0px))
             calc(.8rem + env(safe-area-inset-right, 0px)) .55rem
             calc(.8rem + env(safe-area-inset-left, 0px)); }
  .brand.small { font-size: .85rem; }
  :root { --nah: .35rem; }
  .me { margin-left: auto; }
  .unit { font-size: .8rem; }
  .tabs { order: 3; width: 100%; margin: 0; gap: .3rem; }
  .tab { flex: 1; min-height: 44px; padding: .5rem .4rem; }
  .icon-btn { width: 44px; height: 44px; }
  /* A thumb-sized target without making the header taller. */
  /* More room for the thumb. This value exists as --marke-luft instead of
     being written out twice: the negative margin next to it, which
     offsets it again, depends on the same value. */
  :root { --marke-luft: .45rem; }
  .brand.brand-link { min-height: 44px; }

  /* A zero used to sit here on top.
     -------------------------------------------------------------------------
     That saved one line of height and cost two visible bugs: the topmost
     poll butted straight against the line under the header and looked
     clipped there, and the border around the DM window had no top
     spacing at all.

     calc(.8rem + env(safe-area-inset-bottom)) sat here on the bottom for
     a while.
     -------------------------------------------------------------------------
     That was overcorrecting the bug before it. The safe zone used to hang
     off .composer - which sits INSIDE the border and couldn't lift it, so
     its bottom corners vanished under the home indicator bar. So that
     line moved here, where it lifts the whole border. Except it just
     ADDED to the existing spacing:

       12.8px own spacing  +  34px safe zone  =  46px of empty background

     Measured on an iPhone 14 from the home screen. The finding on it:
     "the outline doesn't reach far enough down, there's too much empty
     space there."

     Now the two jobs are split, each at the spot it belongs:

       .pane      carries only its own spacing        -> border ends
                  (.8rem, same as everywhere else)        12.8px above
                                                           the edge
       .composer  dodges the bar                       -> input stays
                  (env(safe-area-inset-bottom))            61px above it

     The border now runs UNDER the bar instead of ending next to it -
     the way it does in an app where the bar sits on top of the content.
     Its bottom corners stay visible, since 12.8px of margin are left
     over.

     The same applies for the polls one line down: there's no input there
     that could dodge the bar, so the list itself carries the spacing at
     the end - which lets the bottom-most poll scroll up over the bar. */
  .pane { padding: .7rem .8rem .8rem; }
  .composer { padding-bottom: env(safe-area-inset-bottom, 0px); }
  .poll-list { padding-bottom: env(safe-area-inset-bottom, 0px); }
  /* Ansem's inbox scrolls on its own and needs the same run-off. */
  #thread-items { padding-bottom: env(safe-area-inset-bottom, 0px); }

  /* --- Number field: one row, thumb-friendly height --- */
  /* The label stays here. In the chat it was hidden because the preset
     buttons next to it explained themselves; those don't exist anymore,
     and without the sentence all that's left is "$ 10 in $ANSEM" - which
     could mean all sorts of things. */
  .filters { gap: .45rem; padding: .55rem 0; flex-wrap: nowrap; }
  .filter-group { flex: none; width: 178px; min-height: 46px; align-items: stretch; padding: 0 .6rem; }
  .filter-group input { width: 100%; font-size: 16px; min-width: 0; }
  .filter-group .unit { align-self: center; }

  /* Smaller than the message text next to it (.9rem), but still a
     thumb-sized target - the quote is tappable and jumps to the quoted
     message. The height comes from min-height, not the font size. */
  .quote { font-size: .8rem; padding: .35rem 0 .35rem .6rem; min-height: 44px; align-items: center; }
  /* .7rem here is 10.5px - below the threshold for legibility. */
  .day-sep { font-size: .78rem; padding: .18rem .7rem; }

  /* On the narrow screen the bubble may take more width - but the arrow
     next to it also needs room, or it slides off under the edge. */
  .dm-block { max-width: 82%; }
  /* No .is-active before it: that class doesn't exist anymore (see
     above), this size now applies to the arrow in general. It's no
     longer tapped on a phone - swiping replaces that - but it's visible
     during the swipe, and a thumb-sized glyph is easier to see there than
     a text-sized one. */
  .dm-row .reply-btn {
    min-width: 44px; min-height: 44px; font-size: 1.1rem;
  }
  /* .66rem on a phone is 10.6px - below the threshold at which a digit
     is still reliably legible. */
  .msg.dm .time { font-size: .78rem; }
  /* The quote is tappable and needs a thumb-sized measurement. */
  /* The bubble keeps its own padding - the rule above strips the right
     padding from every quote, and in a filled bubble the text would then
     look stuck to the edge. */
  /* The quote bubble now sits ABOVE the message instead of inside it -
     the selector is named accordingly. Thumb size stays: it's clickable
     and jumps to the quoted message. */
  .dm-block > .quote { min-height: 44px; display: flex; align-items: center; font-size: .84rem; }
  .quote .h { font-size: .8rem; }
  .reply-bar { font-size: .9rem; }
  /* Your own balance in the header stays the same size as the handle
     next to it and needs no rule of its own here.
     .7rem on a phone is 10.5px - below the threshold for legibility. */
  .msg .time { font-size: .78rem; }
  .closed-tag { font-size: .74rem; padding: .05rem .35rem; }

  /* --- Input row: above the home indicator, without zooming in --- */
  /* padding-bottom lives further up at .pane, along with the reasoning -
     just the two phone-specific measurements here. */
  .composer { padding-top: .6rem; gap: .4rem; }
  .composer input { font-size: 16px; min-height: 46px; }
  .composer .btn { min-height: 46px; padding-inline: 1.1rem; }

  /* --- Poll: answer and numbers stacked instead of squeezed --- */
  .poll { padding: .9rem; }
  .poll h4 { font-size: 1rem; }
  /* A phone-specific layout used to sit here: answer and amount stacked
     instead of side by side, on the reasoning that a long answer and a
     six-digit amount wouldn't fit on the same line.
     -------------------------------------------------------------------------
     Checked directly at 375px, with the edge case the database allows
     (60-character answer, eight-digit amount):

                        row heights        amount squeezed
       stacked          81 / 81 / 81px     no
       same as desktop  85 / 85 / 85px     no

     So the objection doesn't hold: nothing gets squeezed, not since
     .opt-num sits at flex: none. What stacking cost, though, was
     something you see instantly in a screenshot - the edge of the fill
     ran through the amount underneath it an extra time on every answer,
     4 times instead of 2 in the normal case.

     So there's nothing here anymore: phone and desktop use the same
     rules. Bring the difference back and justify it with numbers -
     scripts/vorschau-antwortzeilen.mjs shows both versions side by
     side. */
  .poll-admin input { font-size: 16px; min-height: 44px; }
  /* Both switches need thumb size - they're the only way to change the
     duration. The border gets sized off them, not the other way around:
     that way the hit area really is the button, not a strip next to it.
     The text doesn't need to go to 16px here, since nothing gets tapped
     anymore that iOS could zoom into. */
  /* And the duration fields too. On desktop they're flatter; here the
     thumb decides, not the eye. */
  .lz-wahl { padding: .45rem 1.5rem .45rem .6rem; font-size: 16px; }
  /* .72rem here is 10.8px - below the threshold at which a word is still
     reliably readable on a phone. And "Days" versus "Minutes" is the
     only piece of information saying which number is which. */
  .lz-name { font-size: .78rem; }
  .poll-admin .btn, .poll-meta .btn { min-height: 44px; }
  /* Thumb size. The button doesn't get visually bigger from this - only
     its hit area does. The negative margin on the right and top takes
     back what the bigger area would otherwise eat into the edge. */
  .poll-tools .icon-btn { width: 44px; height: 44px; }
  .poll-tools { margin: -.5rem -.5rem 0 auto; }

  /* --- DMs --- */
  /* Single-line still needs a thumb-sized measurement on a phone. */
  .thread { padding: .7rem .8rem; min-height: 48px; align-items: center; }
  /* One step up from desktop, for the same reason as there: the amount
     must not be smaller than the preview text next to it. The two values
     - text and amount - belong together and move together. */
  .thread .w { font-size: .9rem; }
  .thread-view { padding: 0 .7rem .7rem; }
  .thread-title { font-size: .8rem; }

  /* The inbox and the conversation don't share the screen, they replace
     each other - the way every messaging app on a phone does it. Side by
     side left too little room for both, and you'd see neither the full
     list nor the full conversation. */
  .dm-admin { grid-template-columns: 1fr; grid-template-rows: 1fr; gap: 0; }
  .thread-list { max-height: none; min-height: 0; }
  .dm-admin > .thread-view { display: none; }
  .dm-admin.viewing > .thread-list { display: none; }
  .dm-admin.viewing > .thread-view { display: flex; }

  .back-inbox {
    display: inline-flex; align-self: flex-start;
    min-height: 44px; padding: .4rem .6rem .4rem 0;
    color: var(--accent); font-size: .95rem;
  }
  /* Handle, address and "Hide" on one line.
     -------------------------------------------------------------------------
     Only padding-top: 0 used to sit here - the row doesn't need top
     padding since the back button above it already gives it air. But the
     bottom padding stayed, which left the TEXT sitting at the top of its
     box while the button next to it sat centered. .thread-kopf centers
     the boxes, not what's inside them. Measured: 5.3px offset, identical
     on iPhone 15 and SE.

     So now the ROW carries the padding, and the title row carries none
     at all. That makes its box exactly its text, and what gets centered
     is what you actually see. Measured after: 0.0px.

     This makes the row 5.6px taller, because the button is taller than
     the text and now sets the measurement. That's the honest price:
     before, the height was right and the alignment wasn't; now it's the
     other way around. */
  .dm-admin.viewing .thread-kopf { padding-bottom: .7rem; }
  .dm-admin.viewing .thread-title { padding: 0; }
  .thread-top .w { font-size: .78rem; }
  .thread-prev { font-size: .9rem; }
  .thread-head, .thread-head .dim { font-size: .78rem; }

  /* --- Login --- */
  .login { padding: 1.2rem .8rem; justify-content: flex-start; padding-top: 2.2rem; }
  .login-card { padding: 1.3rem 1.1rem; border-radius: var(--radius); }
  .brand { font-size: 1.25rem; margin-bottom: 1.1rem; }
  .field input { font-size: 16px; min-height: 48px; }
  .login .btn { min-height: 48px; }

  /* The payment screen is the moment money actually moves. Label on top,
     value big below it, copy as full width - the three-column layout
     doesn't fit at 375px otherwise. */
  .pay-row { padding: .85rem; min-height: 64px; gap: .3rem .6rem; }
  .pay-label { font-size: .78rem; }
  .copy-hint { font-size: .78rem; }
  .pay-value.big { font-size: 1.4rem; }
  .mono-sm { font-size: .92rem; }
  .why p, .steps { font-size: .95rem; }
  .foot-note { font-size: .8rem; }

  /* Visible feedback on tap, since there's no :hover. */
  .btn:active, .tab:active, .thread:active, .opt:active { filter: brightness(1.3); }
}

/* Very narrow devices: the balance next to the handle is the first
   candidate to drop there - it already shows up on every one of your own
   messages anyway. */
@media (max-width: 360px) {
  .holdings { display: none; }
  .brand.small { font-size: .8rem; }
}

/* Anyone who wants reduced motion gets none. */
@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after { animation-duration: .01ms !important; transition-duration: .01ms !important; }
}

/* ---------------- Admin preview ----------------
   Sits fixed at the bottom edge and is deliberately noticeable: a surface
   that looks like Ansem's but has none of his permissions has to explain
   itself - or the save errors it produces get mistaken for bugs.

   It can be closed, and that is not a softening of the warning but the same
   trade the demo-mode bar lost (see the note at PREVIEW_ADMIN in app.js): a
   permanent bar across the bottom edge ends up in every screenshot and every
   recording of this preview, which is what the preview is FOR. Closing is a
   deliberate act, so the sentence has been read by then; and it comes back
   on the next page load, so nobody inherits a hidden warning.

   pointer-events stays none on the bar itself and is switched on only for
   the close button. The bar floats over the content, and a strip that
   swallows clicks meant for what is underneath is worse than a bar that is
   merely visible. */
.preview-flag {
  position: fixed; left: 50%; bottom: .8rem; transform: translateX(-50%);
  z-index: 40; max-width: min(560px, calc(100% - 1.6rem));
  padding: .5rem .4rem .5rem .85rem; border-radius: 999px;
  background: rgba(var(--gold-rgb), .12); border: 1px solid rgba(var(--gold-rgb), .45);
  color: var(--gold); font-size: .78rem; text-align: center;
  display: flex; align-items: center; gap: .5rem;
  -webkit-backdrop-filter: blur(8px); backdrop-filter: blur(8px); pointer-events: none;
}
.flag-zu {
  pointer-events: auto; cursor: pointer;
  background: transparent; border: 0; color: inherit;
  font: inherit; font-size: 1rem; line-height: 1;
  /* 28px, not the 44 a finger needs: this sits on a developer's own machine
     on localhost, and a 44px circle next to a one-line hint would be bigger
     than the hint. */
  width: 28px; height: 28px; border-radius: 999px; flex: none;
  opacity: .75;
}
.flag-zu:hover { opacity: 1; background: rgba(var(--gold-rgb), .16); }

/* ---- Touch hit areas, independent of width ---------------------------
   The block above hangs off (max-width: 760px). But a phone in landscape
   is 844px wide (iPhone 14), 896 (11), or 932 (Pro Max) - none of the
   phone rules applied there anymore, and measured, the targets dropped
   from 44px to 30: tabs 38, header buttons 30 x 30, "Hide" 42 x 24, inbox
   row 39.

   The screen layout in landscape should stay the desktop one - there's
   genuinely room for two columns there. What must NOT depend on width is
   the size of a target: the finger doesn't get thinner when you rotate
   the phone.

   So only the measurements live here, and only for devices with no
   pointer. */
@media (hover: none) and (pointer: coarse) {
  .tab { min-height: 44px; }
  .icon-btn { min-width: 44px; min-height: 44px; }
  .thread-hide { min-height: 44px; }
  .thread { min-height: 48px; }
  .dm-row .reply-btn { min-width: 44px; min-height: 44px; }
}
