/* css/main/chat-messages.css — part 25/30 of the app styles (was css/main.css; loaded in this order by index.html). */
      /* ═══════════════════════════════════════════════════════════════
         CLUB CHAT — stage T3 (Documentos/plan-chat-ejecucion.md)
         Design: Documentos/mockups/chat.html, which Santi already reviewed.
         The contrasts of that mockup are MEASURED and its decisions are
         commented there; they are kept here as they are.

         ⛔ EVERYTHING GOES WITH THE `tlch-` PREFIX, AND IT IS NOT A WHIM. The
         mockup is a standalone file and can afford to call things `.row`,
         `.txt`, `.meta`, `.bub` or `.day`. Not here: this is a stylesheet
         of 7,000 lines shared by three products, and the day someone
         writes a table and defines `.row`, the chat would change its
         look **without anybody touching the chat** and with no error.
         Today none of those names is taken — checked — but that
         is today's snapshot, not a guarantee.
         ═══════════════════════════════════════════════════════════════ */

      /* ═══════════════════════════════════════════════════════════════
         THE AVATAR (js/avatares.js) — the circle with its drawing or its initial

         ⛔ THE BACKGROUND IS NOT A TOKEN AND NEITHER IS THE INK, and that is on purpose. An
         avatar is CONTENT, like a club's logo: whoever chose red
         has to see red, whichever theme they look from, and has to see it
         THE SAME as the rest of the club sees it. The background is set by `pintar()` inline
         —it is data, not a style decision— and the ink is a fixed dark
         because the ten backgrounds are light: it reads the same on black as on
         white.
         ⚠️ That is why this block is the exception to the rule of «always CSS
         variables»: the rule protects against a color meant for dark being painted in
         light, and here there is no color meant for one theme.
         ═══════════════════════════════════════════════════════════════ */
      .tlav {
        width: 100%; height: 100%; border-radius: 50%;
        display: flex; align-items: center; justify-content: center;
        color: #1a1a1a; overflow: hidden;
      }
      /* ⛔ 70 % and not 62 %: the drawings are from Tabler and are made to fill
         a 24 grid with their own margin inside. At 62 % they ended up at
         19 real px in the chat and the ones with detail —the bee, the owl—
         got clogged. This does not enlarge the circle: it fills the one that was already there. */
      .tlav svg { width: 70%; height: 70%; display: block; }
      .tlav-txt { font-weight: 800; line-height: 1; letter-spacing: .5px; }

      /* The wrapper of the conversation. It only exists to give a frame to which to
         anchor the «go to last message» button: `#chatHost` has no
         position, and hanging the button from it would force guessing the height of the
         write bar —which changes with the text, with the padlock of the read-only
         channel and with the connection notice—. */
      .tlch-convwrap {
        position: relative; flex: 1; min-height: 0;
        display: flex; flex-direction: column;
      }

      /* ⛔ ONLY APPEARS WHEN THERE IS SOMETHING BELOW. It is the other half of opening
         the channel at the «new messages» line (T5): if the conversation
         opens where you left it, there must be a tap to jump to the
         latest. A permanent one would cover messages on the only screen where space
         is the product. */
      .tlch-abajo {
        position: absolute; right: 12px; bottom: 12px;
        width: 40px; height: 40px; border-radius: 999px;
        display: flex; align-items: center; justify-content: center;
        /* ⛔ The same colors and the same hover jump as the rest of the
           buttons of this screen (`.tlch-atras`, `.tlch-canal`): surface →
           surface 2. Inventing a pair of its own here is how three screens of
           the same house stop looking alike without anybody deciding it.
           ⭐ And the shadow through the elevation token, not by hand: `--elev-2` already
           has a value per theme. */
        background: var(--surface); color: var(--text);
        border: 1px solid var(--border);
        box-shadow: var(--elev-2);
        cursor: pointer;
      }
      .tlch-abajo svg { width: 20px; height: 20px; }
      .tlch-abajo:hover { background: var(--surface2); }
      .tlch-abajo:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
      @media (pointer: coarse) {
        .tlch-abajo { width: 44px; height: 44px; }
      }

      /* The conversation. `flex:1` + `min-height:0` so that the one that
         scrolls is THIS one and not the whole screen. */
      .tlch-conv {
        flex: 1; min-height: 0; overflow-y: auto;
        padding: 12px 12px 6px;
        display: flex; flex-direction: column; gap: 9px;
        /* ⛔ NO `-webkit-overflow-scrolling: touch` (deprecated, and it is the
           cause of the iOS «I reach the end and it no longer goes up»), and YES
           `overscroll-behavior`, which stops the gesture from chaining to the
           document behind. It has come back three times in this project.
           Guard: scripts/scroll-overlay-check.py */
        overscroll-behavior: contain;
      }

      /* The message that OPENS the channel (D-1). No bubble and no author: the entity
         does not sign it, the system does. It carries the 60-day
         deadline, which is where the retention promise is kept. */
      .tlch-sys {
        align-self: center; max-width: 88%; text-align: center;
        font-size: 13px; line-height: 1.45; color: var(--muted);
        background: var(--surface-soft); border: 1px solid var(--border-soft);
        border-radius: 10px; padding: 9px 12px;
      }
      .tlch-sys b { color: var(--text); }

      .tlch-day {
        align-self: center; font-size: 12px; font-weight: 700;
        letter-spacing: .4px; color: var(--muted);
        background: var(--surface-soft); border-radius: 999px;
        padding: 3px 12px; margin: 3px 0;
      }

      .tlch-row { display: flex; max-width: 84%; }
      .tlch-row.tlch-me   { align-self: flex-end; justify-content: flex-end; }
      .tlch-row.tlch-them { align-self: flex-start; }

      /* ⛔ THE CIRCLE ONLY SETS THE PLACE; what goes inside is painted by
         `TLAvatares.pintar()` (js/avatares.js) — its drawing or its initial, with the
         color that person chose. There is no background or typography here any more:
         putting them back would cover the avatar with a gray. */
      .tlch-av {
        /* ⛔ 34 px, and the trade-off is CONSCIOUS (Santi, 2026-08-29): the circle
           goes to the left of each message, so every pixel it gains is taken
           from the width of the bubble. At 30 px the drawing was seen at 19 and the detailed ones
           could not be told apart; at 34 it is seen at 24, which is the size it
           was drawn for, at the cost of 4 px of text per line. */
        width: 34px; height: 34px; flex: 0 0 34px; border-radius: 50%;
        /* ⛔ AT THE TOP, AT THE HEIGHT OF THE NAME (Santi, 2026-08-28, looking at
           WhatsApp): *"the avatar circle is placed at the top, not
           below the message… it should end up at the same height as where the name appears"*.
           And he is right at the root: the circle and the name say THE
           SAME THING —whose this is—, so separating them forces looking at two
           places. Below, besides, in a five-line message the circle
           ended up at the height of the last one, far from the name that
           goes with it. */
        margin-right: 8px; align-self: flex-start;
        display: block;
      }
      .tlch-av .tlav-txt { font-size: 12px; }

      .tlch-bub {
        border-radius: 14px; padding: 7px 11px 6px; position: relative;
        min-width: 0; box-shadow: 0 1px 1px rgba(0,0,0,.14);
      }
      .tlch-row.tlch-them .tlch-bub {
        background: var(--chat-bot-bg); color: var(--chat-bot-fg);
        border: 1px solid var(--border-soft); border-bottom-left-radius: 4px;
      }
      .tlch-row.tlch-me .tlch-bub {
        background: var(--chat-user-bg); color: var(--chat-user-fg);
        border-bottom-right-radius: 4px;
      }

      /* The author header. It is a `<button>` when there is a card to open and a
         `<div>` when not, so what the browser puts on a
         button is reset: without this it would come out with its background, border and typography. */
      .tlch-who {
        display: flex; align-items: baseline; gap: 6px; margin-bottom: 2px; flex-wrap: wrap;
        background: none; border: 0; padding: 0; margin-left: 0; margin-right: 0;
        font: inherit; color: inherit; text-align: left; width: auto;
      }
      button.tlch-who { cursor: pointer; position: relative; }
      button.tlch-who:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
      /* ⛔ THE TOUCH TARGET GROWS WITHOUT TAKING UP SPACE. The norm asks for 44 px and
         an author header measures about 20: giving them for real would put 24 px of
         air above EVERY burst, on the only screen where space is the
         product. The sensitive zone is extended with a pseudo-element, which does not
         enter the flow — the finger has its 44 px and the screen does not grow. */
      @media (pointer: coarse) {
        button.tlch-who::after {
          content: ""; position: absolute; left: -8px; right: -8px; top: -12px; bottom: -10px;
        }
      }
      /* ⛔ The color is set by the author class (`.tlch-au1`…`.tlch-au7`), which is
         derived from the identifier and is therefore the same on all devices.
         This `--accent-2` is the fallback for whoever does not carry it. */
      /* ⛔ THE MESSAGE WEIGHS MORE THAN WHOEVER SIGNS IT (Santi, 2026-08-28): *"my
         eyes go to Federación de Tiro Olímpico… it is hard for me to find the
         message"*. The name went at 13 px in strong bold and the text at 14
         normal: almost the same weight, and the name on top is the first thing
         the eye meets. It is a long name —a federation registration is
         forty characters— competing with a sentence of five.
         ⭐ It drops to 12 and to 600: still legible and with its own color (which is
         what identifies the author at a glance), but it stops being the headline.
         The hierarchy is marked by SIZE, not by color — whoever cannot tell
         colors apart also has to read the message first. */
      .tlch-who-n { font-size: 12px; font-weight: 600; color: var(--accent-2); }
      .tlch-who-n.tlch-au1 { color: var(--chat-au1); }
      .tlch-who-n.tlch-au2 { color: var(--chat-au2); }
      .tlch-who-n.tlch-au3 { color: var(--chat-au3); }
      .tlch-who-n.tlch-au4 { color: var(--chat-au4); }
      .tlch-who-n.tlch-au5 { color: var(--chat-au5); }
      .tlch-who-n.tlch-au6 { color: var(--chat-au6); }
      .tlch-who-n.tlch-au7 { color: var(--chat-au7); }
      /* Whoever speaks on behalf of the club does NOT enter the distribution: their badge is
         precisely not being one more, and it goes with the house gold. */
      .tlch-who-n.tlch-au-org { color: var(--accent); }
      /* ⭐ The 6-character code goes ALWAYS next to the name: it is what tells
         two Javis apart. The name is for reading; this is for identifying. */
      .tlch-who-c { font-size: 11px; font-weight: 600; color: var(--muted); opacity: .7; letter-spacing: .4px; }

      /* Whoever speaks ON BEHALF OF THE CLUB. ⛔ Icon + text + own treatment of
         the bubble, never just a color (UX norm §2): «that those who are shooters
         are quickly distinguished from those of the organization»
         has to hold also for whoever cannot tell colors apart. */
      .tlch-who-org {
        display: inline-flex; align-items: center; gap: 3px;
        /* 800 → 700: it accompanies the name, it does not compete with it. With both at
           maximum weight, the author line weighed more than the sentence. */
        font-size: 10px; font-weight: 700; letter-spacing: .3px; text-transform: uppercase;
        /* ⚠️ Tinted background and NOT border: `--accent-dim` is a gold at 12 %
           opacity —meant for filling, not for drawing—, and a border of
           1 px with it is not seen in either of the two themes. */
        color: var(--accent); background: var(--accent-dim);
        border-radius: 999px; padding: 1px 7px 1px 5px;
      }
      .tlch-who-org svg { width: 11px; height: 11px; flex: 0 0 11px; }
      /* ⛔ `tlch-deorg` AND NOT `tlch-org`, WHICH IS ALREADY TAKEN by a club's group
         in the channel tree — and that one carries `flex-direction: column`, so
         the message row would have put the author's circle ABOVE the
         bubble. With no error and with no gate seeing it: it is
         exactly the name clash the note at the top of the
         block warns about, inside the prefix itself. */
      .tlch-row.tlch-deorg .tlch-bub { border-left: 2px solid var(--accent); }
      /* ⛔ THE GAP IS NOT AN AVATAR. In a burst —several consecutive messages
         from the same author— only the first carries a circle; the others carry
         a gap of the same width so the bubbles stay aligned. This rule
         painted them the GOLD BACKGROUND just like a real avatar, so
         in the organization's messages **an empty circle** was seen
         below the one that did have a face. Santi saw it on 2026-08-28: «the avatar
         circle appears empty».
         ⚠️ The `:not()` goes here and not a `background: none` on the gap, because
         this rule is more specific and would win. */
      .tlch-row.tlch-deorg .tlch-av:not(.tlch-av-hueco) {
        color: var(--accent);
        background: var(--accent-dim);
      }

      /* Message following the same author: no header and stuck to the previous one. The
         gap of the circle is kept so the column does not dance. */
      .tlch-row.tlch-seguido { margin-top: -6px; }
      .tlch-av-hueco { background: none; border: 0; }

      /* The LINK inside a message. Only the organization places them (norm 11,
         qualified on 2026-08-29). The DOMAIN highlighted and the rest behind and
         trimmed: it is the only thing that lets you see where it leads before touching it. */
      .tlch-url { color: inherit; text-decoration: none; word-break: break-all; }
      .tlch-url strong {
        text-decoration: underline; text-underline-offset: 2px; font-weight: 700;
      }
      .tlch-url span { opacity: .72; }
      .tlch-url:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; border-radius: 6px; }

      /* The QUOTE is a button since 2026-08-29: it leads to the original message. Everything
         the browser puts on a button is removed from it; what makes it
         look tappable is the cursor and the focus, not a new ornament. */
      button.tlch-cita {
        width: 100%; text-align: left; font: inherit; color: inherit;
        cursor: pointer;
        /* ⛔ The browser puts its own background, border and typography on a button, and
           they have to be removed: the quote must keep looking EXACTLY
           the same as when it was a `div`. The left border is put back
           here because `border: 0` would take it away — this rule beats by
           specificity `.tlch-cita`, which is where it lived. */
        background: none; border: 0; border-left: 3px solid var(--accent-2);
        -webkit-appearance: none; appearance: none;
      }
      button.tlch-cita:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }

      /* ── THE NEWS ITEM, ANNOUNCED IN THE CHAT (2026-08-29) ──────────────
         Santi's idea: a news item is the important thing a club publishes, and
         many people do not see the notice or enter News — they enter the chat. So
         the announcement goes to both places.
         ⛔ The red is `--badge`, the SAME as the unread badge. In this
         house that red means «look at this», not «careful»: the delete one is another
         and lives in `.btn-danger`. With icon AND text, never just the color. */
      .tlch-noti { display: flex; flex-direction: column; gap: 6px; }
      .tlch-noti-h {
        display: flex; align-items: center; justify-content: center; gap: 6px;
        color: var(--badge); font-family: 'Oswald', sans-serif; font-weight: 700;
        letter-spacing: 1px; font-size: 11px; text-transform: uppercase;
        border-bottom: 1px solid var(--border-soft); padding-bottom: 5px;
      }
      .tlch-noti-h svg { width: 14px; height: 14px; flex: 0 0 14px; }
      /* The COMPETITION one goes in the house gold and not in red: red is
         reserved for news, which is what the club decides to highlight. If
         both were red, red would stop signaling anything. */
      .tlch-noti-h-cv { color: var(--accent); }
      /* 16 and not 15: the mockup's scale has no 15 (§5 of the norms, and it is
         guarded by `unit-escala-visual`). A headline is a headline. */
      .tlch-noti-t { font-weight: 700; font-size: 16px; line-height: 1.3; }
      .tlch-noti-x { font-size: 13px; line-height: 1.4; color: var(--muted); }
      /* The button takes up the width of the bubble: it is the action of the card, not
         an ornament beside the text. Plenty of height for the finger. */
      .tlch-noti-b {
        margin-top: 2px; width: 100%; min-height: 36px;
        background: var(--accent-dim); color: var(--accent);
        border: 1px solid var(--accent); border-radius: 10px;
        font-family: 'Oswald', sans-serif; font-weight: 600; letter-spacing: .5px;
        font-size: 13px; cursor: pointer; padding: 8px 10px;
      }
      @media (pointer: coarse) { .tlch-noti-b { min-height: 44px; } }
      .tlch-noti-b:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }

      /* ── THE NOTICE BOARD: a channel where you cannot reply ─────────────
         ⛔ WHY IT IS NOT A CONVERSATION. Santi saw it on 2026-08-28 entering
         the Principal channel: «since here you cannot reply… otherwise, always
         the right or the left side is going to be empty». Exactly — a
         bubble stuck to one side means «there is another side», and in a
         read-only channel that other side does not exist and will not exist. Centered and
         wide, the same bubble reads as what it is: the club's announcement.
         ⛔ BUT ONLY FOR WHOEVER CANNOT REPLY. Here it said «it applies whoever
         is looking, also the organization itself», and it was an extension of mine that
         Santi corrected on 2026-08-29: *"for a member of the organization the
         federation's channel is a normal channel, where some write and I
         write"*. And more so since each message carries its role — in there is
         a real conversation between the president and the treasurer, and
         centering it disguises it as a notice board. The decision is made by `_pantalla()`, with the
         two conditions: channel closed AND I cannot write.
         ⚠️ The price: the organization no longer sees its main channel exactly
         as its members see it.
         ⭐ And the font goes up one point (15 px) because here the message is
         the content of the screen, not a turn to speak. */
      /* ⛔ CENTERED, BUT NOT FROM EDGE TO EDGE (Santi, 2026-08-29). The first
         version of this put `align-self: stretch` and `width: 100%`, and a
         bubble that touches both edges stops reading as a bubble: it is a
         band. His words: *"we talked about that bubble not being on the side,
         but not about it taking the whole width, it looks very ugly"*.
         ⭐ What solves the case is the CENTERING, not the width: centered it no longer
         promises an «other side» that in a read-only channel does not exist. So
         it is centered and the bubble is left to be as wide as its text is, with
         a cap so that a long announcement does not cross a desktop screen from
         side to side — which is the other way for it not to be read. */
      .tlch-conv.tlch-tablon .tlch-row,
      .tlch-conv.tlch-tablon .tlch-row.tlch-me,
      .tlch-conv.tlch-tablon .tlch-row.tlch-them {
        max-width: min(94%, 620px); align-self: center; justify-content: center;
      }
      .tlch-conv.tlch-tablon .tlch-av { display: none; }
      .tlch-conv.tlch-tablon .tlch-bub { max-width: 100%; }
      /* Without sides there is no «mine» and «theirs»: all bubbles the same. Otherwise, the
         club's device would see its own notice board in gold and the others in
         gray, that is, two different boards of the same channel. */
      .tlch-conv.tlch-tablon .tlch-row.tlch-me .tlch-bub {
        background: var(--surface); color: var(--text);
        /* 14, the dialog radius that `.tlch-bub` already uses: here we only undo
           the clipped corner of «my message», we do not introduce a new shape. */
        border: 1px solid var(--border-soft); border-radius: 14px;
      }
      .tlch-conv.tlch-tablon .tlch-row.tlch-me .tlch-meta { color: var(--muted); opacity: .72; }
      /* ⚠️ 16 and not 15: the mockup's scale has no 15 (§5 of the norms, and
         it is guarded by `unit-escala-visual`). It goes up a step, which is what the
         case asked for — a club announcement is not a turn to speak. */
      .tlch-conv.tlch-tablon .tlch-txt { font-size: 16px; }

      /* `white-space: pre-wrap` keeps the line breaks the person writes;
         `word-break` prevents a long word from overflowing the bubble. */
      /* 14 → 16: the message is the only thing one comes to read, and with the name at
         12 the difference is really noticeable. It is also the body size that
         the mockup asks for text on mobile. */
      .tlch-txt { font-size: 16px; line-height: 1.42; white-space: pre-wrap; word-break: break-word; }
      /* ⛔ ON TOUCH THE TEXT IS NOT SELECTED, AND THAT IS WHAT FIXES THE LONG
         PRESS. The gesture is a 500 ms timer that any `touchmove`
         cancels — and when the finger is held on selectable text, the system
         starts ITS selection, fires that `touchmove` and kills the timer.
         So the message menu never came up where one really presses:
         on top of the words. It looked like «it starts selecting», which
         seems a browser thing and is ours.
         ⛔ What is lost —copying by dragging— comes back as a menu option
         («Copy text»): turning this off without giving it back would leave the chat with
         no way at all to copy a message. It is the trade WhatsApp makes.
         ⚠️ ONLY on coarse pointer: with a mouse the selection is made by dragging,
         not by holding, so it hinders nothing — and the desktop menu
         comes with the right button, which does not depend on this either. */
      /* ── LONG MESSAGES: TWENTY LINES AND «Read more» ──────────────────
         ⛔ The number lives HERE and nowhere else. The JavaScript does not count
         lines: it paints with this class on and asks the browser whether something
         has been left out (`scrollHeight > clientHeight`). So, changing the
         cut is changing this 20 — and it keeps working the same with large
         type, on a tablet and on a phone, which is where counting lines by hand
         gets it wrong.
         ⚠️ `-webkit-line-clamp` REQUIRES `display: -webkit-box`, and that is why the class
         is only put on messages that really overflow: putting it on all of them
         would change the layout of every chat bubble for nothing. */
      .tlch-txt-corta {
        display: -webkit-box; -webkit-box-orient: vertical;
        -webkit-line-clamp: 20; line-clamp: 20;
        overflow: hidden;
      }
      /* ⛔ IT IS A REAL BUTTON, not blue text: a finger presses it and it has to
         meet the touch target like any other control (UX norm §1).
         The 44 px height is achieved with the padding; the width is that of the text,
         which is right for an action link inside a paragraph. */
      .tlch-mas {
        display: inline-flex; align-items: center; min-height: 44px;
        margin: -6px 0 -8px; padding: 6px 0;
        background: none; border: 0; cursor: pointer;
        font: inherit; font-size: 14px; font-weight: 700;
        color: var(--accent); text-align: left;
      }
      .tlch-mas:hover { text-decoration: underline; }
      .tlch-mas:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; border-radius: 6px; }
      /* In the own bubble the accent does not contrast against its background: it inherits. */
      .tlch-row.tlch-me .tlch-mas { color: currentColor; opacity: .82; }

      @media (pointer: coarse) {
        .tlch-txt {
          -webkit-user-select: none; user-select: none;
          /* iOS: without this the magnifier and the system bubble keep coming up. */
          -webkit-touch-callout: none;
        }
      }

      .tlch-meta {
        display: flex; align-items: center; justify-content: flex-end; gap: 4px;
        font-size: 10px; color: var(--muted); opacity: .72; margin-top: 2px;
      }
      /* ⛔ `.6` DID NOT REACH THE MINIMUM, and only on MY messages: measured on
         2026-08-28 in the browser, 3.42:1 in dark and 3.49:1 in light against
         the 4.5:1 required by `UX-UI-normas.md` §2 — while the time of
         other people's messages went at 8.04 and 5.05. The difference is the background: the
         own bubble goes over the accent color, which is already dark, and lowering
         the opacity of the text on top sinks it. The same `.6` on the
         gray bubble passes easily, here it does not. It is 10 px text, that is, the one that reads
         worst on the screen. */
      .tlch-row.tlch-me .tlch-meta { color: var(--chat-user-fg); opacity: .82; }

      /* ── «edited» ──────────────────────────────────────────────────────
         ⛔ It goes STUCK to the time and with the same weight, not as a badge: it is
         a footnote, not an alarm. Let it be seen if looked for and not shout
         if not. It inherits color and opacity from `.tlch-meta`, so it meets
         the contrast in both bubbles and in both themes without saying anything of its own
         —which is exactly what was done wrong the first time with the time—.
         The separator is a middle dot, not a hyphen: a hyphen reads as part
         of the time. */
      .tlch-editado::before { content: "·"; margin: 0 4px; }
      .tlch-editado { font-style: italic; }

      /* ── ✓ / ✓✓ in PRIVATE chats (Santi, 2026-09-24) ─────────────────────
         One tick: saved on the server. Two: the other side has reached it.
         ⛔ The SHAPE tells the state and the color only reinforces it (UX norm §2). */
      .tlch-tic { display: inline-flex; width: 18px; height: 14px; flex: none; }
      .tlch-tic svg { width: 100%; height: 100%; }
      .tlch-tic-leido { color: var(--chat-leido); }

      /* ── THE TOMBSTONE of a deleted message ────────────────────────────
         ⛔ ICON **AND** TEXT, never just a more muted tone (UX norm §2): a
         gray message is confused with one that is still being sent, and
         whoever does not distinguish color nuances would see no difference.
         ⭐ In italics and without `pre-wrap`: it is nobody's text, it is the gap it
         left. */
      .tlch-borrado {
        display: flex; align-items: center; gap: 6px;
        font-size: 14px; font-style: italic; opacity: .78;
      }
      .tlch-borrado svg { width: 15px; height: 15px; flex: none; }

      /* Message not yet confirmed by the server. The local echo appears
         instantly —it is what makes the chat «feel» immediate— and this
         says it is still on its way, without being an alarm. */
      .tlch-row.tlch-pend .tlch-bub { opacity: .62; }

      /* ── «Not sent» (#132, WhatsApp model, Santi 2026-09-16) ───────────
         ⛔ THE BUBBLE IS NOT DIMMED: it is no longer «on its way», it waits for the
         person to decide — and with the echo's opacity the text drops below the minimum
         contrast, in precisely the message that most needs reading.
         ⛔ THE RED SIGNAL GOES OUTSIDE THE BUBBLE, on the background of the
         conversation: inside, red on the dark green of «my messages» does not
         reach 3:1 in dark theme. And it is NOT the only signal: the footer says it
         with words (UX norms §2: color is never the only indicator). */
      .tlch-row.tlch-pend-no .tlch-bub { opacity: 1; }
      .tlch-row.tlch-pend-no { cursor: pointer; }
      .tlch-pend-no-ic {
        flex: 0 0 22px; width: 22px; height: 22px; margin-right: 6px;
        align-self: center; display: inline-flex; color: var(--danger);
      }
      .tlch-pend-no-ic svg { width: 22px; height: 22px; }
      /* The footer is a <button> —keyboard and screen reader—: out with what the
         browser puts, and it inherits the shape of `.tlch-meta`. At 12 px and without transparency:
         it is an instruction, not a time, and it has to be read. */
      button.tlch-meta-no {
        background: none; border: 0; padding: 0; margin: 2px 0 0; width: 100%;
        font: inherit; font-size: 12px; font-weight: 600; line-height: 1.3;
        color: inherit; opacity: 1; cursor: pointer; border-radius: 6px;
      }
      .tlch-row.tlch-me button.tlch-meta-no { color: var(--chat-user-fg); opacity: 1; }
      .tlch-conv.tlch-tablon .tlch-row.tlch-me button.tlch-meta-no { color: var(--text); opacity: 1; }
      button.tlch-meta-no:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }

      /* New-messages separator.
         ⛔ It does NOT use the red badge token. Measured: #dc2626 on the dark background gives 4.02:1 and
         norm §2 requires 4.5. The counter's red works because there the
         text is WHITE ON red; here it would be red on black, which is another
         case. It goes in gold, which is also right: this marks a
         position, it does not warn of a problem.
         The color goes on the TWO LINES, which carry no text on top; the label
         goes in --muted, which passes in both themes. */
      .tlch-newline { display: flex; align-items: center; gap: 9px; margin: 5px 0 1px; }
      .tlch-newline::before, .tlch-newline::after {
        content: ""; flex: 1; height: 1px; background: var(--accent); opacity: .45;
      }
      .tlch-newline span { font-size: 11px; font-weight: 800; letter-spacing: .5px; color: var(--muted); }

      /* ═══════════════════════════════════════════════════════════════
         THE LONG-PRESS MENU, THE QUOTE AND THE VOTES
         ═══════════════════════════════════════════════════════════════ */
      /* ⛔ HERE LIVED `.tlch-opc-prev`, the copy of the message that the sheet
         carried inside to say WHICH one was being acted on. With the menu
         anchored it is redundant: the message is seen right next to it, highlighted. It is removed
         entirely and not left dead — a style that no longer paints anything ends up
         copied elsewhere because it looks like part of the system. */
      .tlch-opc {
        display: flex; align-items: center; gap: 12px; width: 100%;
        min-height: 48px; padding: 0 12px; margin: 0 0 6px;
        background: none; border: 0; border-radius: 10px;
        color: var(--text); font: inherit; font-size: 16px; text-align: left; cursor: pointer;
      }
      .tlch-opc svg { width: 20px; height: 20px; flex: 0 0 20px; color: var(--muted); }
      .tlch-opc:hover { background: var(--surface2); }
      .tlch-opc:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; }
      /* What is already voted. ⛔ Not just the color: the icon is tinted AND the row
         is marked with its background, so it is seen without telling colors apart. */
      .tlch-opc.act { background: var(--accent-dim); }
      .tlch-opc.act svg, .tlch-opc.act span { color: var(--accent); font-weight: 700; }

      /* ══ THE MENU OF A MESSAGE — ANCHORED TO IT, not a sheet ═════════════
         ⛔ WHY IT CHANGED (Santi, 2026-08-28): it was a full-screen sheet
         stuck to the bottom, and his complaint is the one that rules — *"I would try to avoid throughout
         the use floating windows when I'm inside the chat"*. A sheet
         covers the conversation to act on ONE message, and on top of that forces
         carrying inside a copy of the message to say which one is acted on.
         Anchored to the message itself that is redundant: it is seen below.

         ⭐ And ABOVE OR BELOW AS IT FITS, which is what makes it work the same
         with the first message on the screen as with the last: the
         JS decides by measuring, not a fixed class. */
      .tlch-menu {
        position: fixed;
        z-index: 890;
        min-width: 196px;
        max-width: min(280px, calc(100vw - 24px));
        padding: 6px;
        background: var(--card);
        border: 1px solid var(--border);
        border-radius: 14px;
        /* Plane 2: this DOES float above the conversation (§ elevation). */
        box-shadow: var(--elev-2);
      }
      /* Inside the menu the rows go closer together than in a sheet: here there is no
         need to travel half a screen with the thumb. They stay at 44 px, which is the
         touch minimum, and not the 48 of the sheet. */
      .tlch-menu .tlch-opc {
        min-height: 44px;
        margin: 0;
        /* 14, not 15: the mockup's scale has no intermediate steps
           (UX-UI-normas §5, and a test guards it). */
        font-size: 14px;
      }
      /* The layer that catches the tap outside. Transparent ON PURPOSE: a dark veil
         would again say «this is a dialog», which is precisely what has been
         removed. It only exists to close. */
      /* The line that separates MODERATION from the everyday options.
         Above are the four that anyone uses a hundred times a day; below,
         withdraw, mute and expel — which do not undo themselves. Stuck to
         «Copy text» they get pressed by accident (UX norms §7). */
      .tlch-menu-sep {
        height: 1px; background: var(--border); margin: 5px 8px;
      }

      .tlch-menu-capa {
        position: fixed;
        inset: 0;
        z-index: 889;
        background: none;
      }
      /* The message on which the menu is open. Without this, with the menu to one
         side, it is lost from sight WHICH one is being acted on — which is the only
         thing the sheet did well. */
      .tlch-msg-sel .tlch-bub {
        outline: 2px solid var(--accent);
        outline-offset: 1px;
      }

      /* The quote, inside the bubble and above the text. */
      .tlch-cita {
        display: flex; flex-direction: column; gap: 1px;
        border-left: 3px solid var(--accent-2); padding: 3px 0 3px 8px;
        margin-bottom: 5px; min-width: 0;
      }
      .tlch-cita-n { font-size: 11px; font-weight: 700; color: var(--accent-2); }
      .tlch-cita-t {
        font-size: 12px; color: var(--muted); line-height: 1.3;
        overflow: hidden; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical;
      }
      /* And the same quote while the reply is written, above the field. */
      .tlch-cita-edit {
        display: flex; align-items: center; gap: 8px;
        padding: 6px 10px; background: var(--surface-soft);
        border-top: 1px solid var(--border-soft);
      }
      .tlch-cita-cuerpo {
        flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 1px;
        border-left: 3px solid var(--accent-2); padding-left: 8px;
      }
      .tlch-cita-x {
        width: 32px; height: 32px; flex: 0 0 32px; border: 0; border-radius: 50%;
        background: none; color: var(--muted); cursor: pointer;
        display: grid; place-items: center;
      }
      .tlch-cita-x svg { width: 16px; height: 16px; }
      .tlch-cita-x:hover { background: var(--surface2); }
      .tlch-cita-x:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
      @media (pointer: coarse) {
        .tlch-cita-x { width: 44px; height: 44px; flex: 0 0 44px; }
      }

      /* The vote count, below the message. ⛔ Only the numbers: who
         voted cannot be known, and not because it is not painted here but because that data
         lives where only its owner reads it (rule `mivot`). */
      .tlch-votos { display: flex; gap: 6px; margin-top: 4px; }
      .tlch-voto {
        display: inline-flex; align-items: center; gap: 3px;
        font-size: 11px; font-weight: 700; color: var(--muted);
        background: var(--surface-soft); border-radius: 999px; padding: 2px 8px 2px 6px;
      }
      .tlch-voto svg { width: 12px; height: 12px; }
      /* One's own vote, marked with color AND with border: never just color. */
      .tlch-voto.mio { color: var(--accent); box-shadow: inset 0 0 0 1px var(--accent); }
      .tlch-row.tlch-me .tlch-voto { background: rgba(0, 0, 0, .18); color: var(--chat-user-fg); }

