  /* One palette, and every shell wears it.
     Colour is spent on legibility. Panes, cards, rows and buttons are
     greyscale, separated by 1px hairlines and small radii, with micro-cap
     labels over each section. Two things are allowed to be anything other
     than grey:

       the ONE action    Send is the only filled button on the screen, in the
                         text colour, so what you press is never in doubt.
       the STATUS        a dot for the mode, the queue count, and the edge on
                         a question nobody has answered.

     That is the whole colour budget, and it is what makes a queue of forty
     rows readable. test_contrast.py measures every pair below, so a colour
     changed here has to clear the same thresholds.

     Three states, and one thing to know: Missive itself sets data-theme on
     this document ("dark" or "light") and updates it when the user changes
     theirs. So "Match Missive" is not a guess -- it is that attribute. Our own
     choice lives on data-desk-theme, a separate attribute, because writing to
     data-theme would be overwritten the next time Missive touched it.
     prefers-color-scheme is the fallback for when Missive says nothing. */
  :root {
    color-scheme: light dark;
    --fg:#18181b; --muted:#616168; --dim:#616168;
    --line:#e2e2e6; --soft:#f4f4f5; --soft2:#eaeaee; --card:#ffffff;
    /* The strip each pane opens with: the status bar, the conversation head,
       the customer-file head. The sidebar has no panes and opens with the
       same strip. */
    --bar:#fafafa;
    /* The panel sits on Missive's own background, so the bubbles carry the
       contrast themselves rather than relying on what is behind them. */
    --in:#dcdce2; --in-line:#cbcbd3; --out:#ffffff; --out-line:#d3d3da;
    --in-fg:var(--fg); --out-fg:var(--fg);
    /* Unanswered starts as a customer bubble with the accent on its edge.
       A salesman can recolour --now; until then it follows --in. */
    --now:var(--in); --now-fg:var(--fg); --now-line:var(--ac);
    --now-icon-fg:var(--ac-fg);
    --field:#f7f7f8;
    --ac:#0b7a35; --ac-fg:#ffffff; --ac-soft:rgba(11,122,53,.10);
    --bad:#c62222; --bad-soft:rgba(198,34,34,.09);
    /* Busy, and anything else that is neither fine nor wrong. Dark
       enough to read as text on the panel, not just be seen as a dot. */
    --warn:#8a5300; --warn-soft:rgba(138,83,0,.11);
    /* The two states nobody switches into. On duty is one person holding the
       chat open past closing time; After hours is the clock having closed it.
       Their own colours because green and yellow are spoken for, and the dot
       is the only thing that tells the five states apart at a glance. */
    --duty:#1d4ed8; --closed:#c2410c;
    /* The one filled button on the screen, which is Send. */
    --solid-bg:#18181b; --solid-fg:#ffffff;
  }
  :root[data-desk-theme="light"] { color-scheme: light }
  :root[data-desk-theme="dark"] { color-scheme: dark }
  /* Missive says dark, and nobody here has overridden it. */
  :root[data-theme="dark"]:not([data-desk-theme]) {
    --fg:#ededef; --muted:#a1a1aa; --dim:#8c8c96;
    --line:#2f2f34; --soft:#212124; --soft2:#28282c; --card:#1a1a1c;
    --bar:#151517;
    --in:#333338; --in-line:#414147; --out:#1e1e21; --out-line:#37373d;
    --field:#202023;
    --ac:#22c55e; --ac-fg:#04240f; --ac-soft:rgba(34,197,94,.14);
    --bad:#f87171; --bad-soft:rgba(248,113,113,.13);
    --warn:#e5a83a; --warn-soft:rgba(229,168,58,.15);
    --duty:#60a5fa; --closed:#fb923c;
    --solid-bg:#fafafa; --solid-fg:#18181b;
  }
  @media (prefers-color-scheme: dark) {
    :root:not([data-theme]):not([data-desk-theme]) {
      --fg:#ededef; --muted:#a1a1aa; --dim:#8c8c96;
      --line:#2f2f34; --soft:#212124; --soft2:#28282c; --card:#1a1a1c;
      --bar:#151517;
      --in:#333338; --in-line:#414147; --out:#1e1e21; --out-line:#37373d;
      --field:#202023;
      --ac:#22c55e; --ac-fg:#04240f; --ac-soft:rgba(34,197,94,.14);
      --bad:#f87171; --bad-soft:rgba(248,113,113,.13);
      --warn:#e5a83a; --warn-soft:rgba(229,168,58,.15);
    --duty:#60a5fa; --closed:#fb923c;
      --solid-bg:#fafafa; --solid-fg:#18181b;
    }
  }
  :root[data-desk-theme="dark"] {
    --fg:#ededef; --muted:#a1a1aa; --dim:#8c8c96;
    --line:#2f2f34; --soft:#212124; --soft2:#28282c; --card:#1a1a1c;
    --bar:#151517;
    --in:#333338; --in-line:#414147; --out:#1e1e21; --out-line:#37373d;
    --field:#202023;
    --ac:#22c55e; --ac-fg:#04240f; --ac-soft:rgba(34,197,94,.14);
    --bad:#f87171; --bad-soft:rgba(248,113,113,.13);
    --warn:#e5a83a; --warn-soft:rgba(229,168,58,.15);
    --duty:#60a5fa; --closed:#fb923c;
    --solid-bg:#fafafa; --solid-fg:#18181b;
  }
  * { box-sizing: border-box }
  html, body { height: 100% }
  body { margin:0; color:var(--fg); overflow:hidden; background:transparent;
         font:13px/1.5 -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif }
  #app { height:100%; display:flex; flex-direction:column; overflow:hidden;
         position:relative }
  /* Following Missive means inheriting its background. Choosing a theme means
     painting our own, or a light panel would sit in a dark app. */
  :root[data-desk-theme] body { background:var(--card) }
  .scroll { flex:1; overflow-y:auto; overflow-x:hidden; padding:12px }
  /* The queue's own list. Its rows carry their side margins themselves, so the
     list only needs a gap above the first one. */
  .scroll.flush { padding:8px 0 0 }
  /* The queue still scrolls, but its chrome does not consume ten pixels of a
     narrow phone or desk column. Keyboard/wheel/touch scrolling is unchanged. */
  .scroll[data-scroll="queue"] { scrollbar-width:none; -ms-overflow-style:none }
  .scroll[data-scroll="queue"]::-webkit-scrollbar { display:none }
  .scroll::-webkit-scrollbar { width:10px }
  .scroll::-webkit-scrollbar-thumb { background:var(--line); border-radius:6px;
                                     border:3px solid transparent;
                                     background-clip:content-box }
  .scroll::-webkit-scrollbar-thumb:hover { background:var(--dim);
                                           border:3px solid transparent;
                                           background-clip:content-box }

  /* status */
  /* Above everything, on every view, because "is the chat on?" is not a
     question anybody should have to leave a conversation to answer. Missive
     draws its own "Novak Web Chat" title above this; this is the line
     underneath it that says what that chat is actually doing. */
  /* Every pane opens with the same strip -- the queue's status bar, the
     conversation head, the customer-file head -- so the columns read as three
     parts of one tool rather than three unrelated screens. In the sidebar
     there is one column and the same strip still opens it. */
  /* The aligned pane headers are 44px: three quarters of the former 58px
     desktop strips. Removing the presence subtitle makes the conversation
     title fit comfortably on one line at this height. */
  .status { display:flex; align-items:center; gap:4px; padding:1px 6px 1px 9px; flex:none;
            min-height:44px;
            border-bottom:1px solid var(--line); position:relative; z-index:4;
            background:var(--bar) }
  .statusbtn { display:flex; align-items:center; gap:7px; background:none; border:0;
               color:var(--fg); font:inherit; font-size:12.5px; font-weight:600;
               cursor:pointer; padding:7px 7px; border-radius:7px; min-width:0;
               white-space:nowrap; letter-spacing:-.005em }
  .statusbtn:hover { background:var(--soft) }
  .statusbtn .chev { color:var(--muted); line-height:0; margin-left:-2px }
  .statusbtn svg { width:13px; height:13px }
  .dot { width:9px; height:9px; border-radius:50%; flex:none; background:var(--muted) }
  .dot.live { background:var(--ac) }
  .dot.busy { background:var(--warn) }
  .dot.testing { background:var(--bad) }
  /* Customers are getting the same chat as at midday, but it is one person
     holding it open rather than the schedule, and it ends by itself. Blue, so
     the dot alone says which of the two is keeping the chat on. */
  .dot.onduty { background:var(--duty) }
  /* Nobody is here, which is a fact about the clock rather than a fault.
     Filled like the rest, because hollow read as "no state at all" when what
     it means is that questions are arriving and waiting for the morning. */
  .dot.closed, .dot.holiday { background:var(--closed) }
  /* What is true but is not the headline -- Busy switched on at 6pm, when
     After hours is what a customer actually gets.
     The spacer in statusBar() does the pushing; this must not auto-margin as
     well, or it would push the bell and Settings a second time. */
  .status .aside { color:var(--dim); font-size:11.5px; margin-left:0; min-width:0;
                   overflow:hidden; text-overflow:ellipsis; white-space:nowrap;
                   padding-right:4px }
  .status .aside.warn { color:var(--warn) }
  /* The panel is 358px at its widest and narrower in a split view, so this
     menu is bounded on BOTH sides -- an unbounded one grew to fit its longest
     line and cut its own text off at the panel edge.
     top:100% rather than a number: the bar may grow for touch controls or
     translated text, and a fixed offset would detach the menu from it. */
  .menu.left { left:6px; right:6px; top:100%; min-width:0 }
  .menu .pick { display:flex; align-items:flex-start; gap:8px; padding:8px 9px;
                text-align:left }
  .menu .pick .dot { margin-top:4px }
  .menu .pick b { font-weight:600 }
  /* min-width:0, or the description refuses to wrap and pushes the menu wide
     again: a flex item's default minimum size is its own content. */
  .menu .pick > span { min-width:0 }
  .menu .pick span span { display:block; color:var(--dim); font-size:11.5px;
                          font-weight:400; white-space:normal; line-height:1.45 }
  .menu .pick.on { background:var(--soft) }

  /* header */
  /* The header sits above the list and must stay above it: the list scrolls
     under this border, and a row arriving at the top used to look as though it
     had pushed through it. */
  .tabs { display:flex; align-items:center; gap:0; border-bottom:1px solid var(--line);
          padding:0 12px; flex:none; position:relative; z-index:2 }
  /* Two equal tabs. At 36px they are exactly 25% shorter than the former 48px
     row while retaining the full-width click targets. */
  .tab { height:36px; padding:0 3px; font:inherit; font-size:13px; color:var(--muted); cursor:pointer;
         background:none; border:0; border-bottom:2px solid transparent; margin-bottom:-1px;
         letter-spacing:-.005em;
         display:flex; align-items:center; justify-content:center; gap:5px; flex:1 1 50%; min-width:0;
         white-space:nowrap }
  .tab:hover { color:var(--fg) }
  /* Underlined in the text colour: which tab you are on is not a status, so
     it does not get the status colour. */
  .tab.on { color:var(--fg); font-weight:600; border-bottom-color:var(--fg) }
  .icobtn { background:none; border:0; color:var(--muted); cursor:pointer; padding:8px 6px;
            line-height:0; border-radius:7px }
  /* The two controls in the status bar are reached without looking, so they
     are bigger than the icons inside the panel -- and the padding is what
     makes the target 40px, not the icon. */
  .status .icobtn { padding:10px 7px; flex:none }
  .status .icobtn svg { width:20px; height:20px }
  .icobtn:hover { color:var(--fg); background:var(--soft2) }
  .icobtn.on { color:var(--ac) }
  .icobtn[disabled] { opacity:.4; cursor:default }
  .hd { display:flex; align-items:center; gap:8px; padding:4px 9px 4px 5px; min-height:44px;
        border-bottom:1px solid var(--line); flex:none; background:var(--bar) }
  .hd .ttl { font-weight:600; font-size:13.5px; letter-spacing:-.01em }
  .count { background:var(--ac); color:var(--ac-fg); font-weight:700; font-size:10px;
           border-radius:6px; padding:1px 6px; line-height:1.5; flex:none;
           font-variant-numeric:tabular-nums }
  /* Nothing is owed on the settled list, so its count is a number and not a
     status. It must still be readable, which a colour of its own is not. */
  .count.q { background:var(--soft2); color:var(--muted) }
  /* Tab totals are typography, not badges. Answer keeps the status green;
     Done uses the ordinary foreground so it stays white in dark mode
     and readable in light mode. The hidden-chat counter below the list keeps
     the compact badge treatment above. */
  .tabs .count { background:transparent; color:var(--ac); border-radius:0; padding:0;
                 font-size:12px; line-height:inherit }
  .tabs .count.q { background:transparent; color:var(--fg) }

  /* bits */
  .row { display:flex; align-items:center; gap:8px }
  .dot { width:7px; height:7px; border-radius:50%; background:var(--ac); flex:none }
  .dot.off { background:transparent; border:1.5px solid var(--dim); width:8px; height:8px }
  .mut { color:var(--muted) } .dim { color:var(--dim) }
  .sm { font-size:11.5px }
  .ell { overflow:hidden; text-overflow:ellipsis; white-space:nowrap; min-width:0 }
  .wrap-any { overflow-wrap:anywhere }
  /* Outlined by default; exactly one filled button on the screen, and it is
     the one that sends a reply to a customer. */
  .btn { font:inherit; font-size:12px; padding:5px 11px; border-radius:7px;
         border:1px solid var(--line); background:transparent; color:var(--fg); cursor:pointer }
  .btn:hover { background:var(--soft2); border-color:var(--dim) }
  .btn.p { background:var(--solid-bg); border-color:var(--solid-bg); color:var(--solid-fg);
           font-weight:600 }
  .btn.p:hover { background:var(--solid-bg); border-color:var(--solid-bg); opacity:.88 }
  .btn.bad { color:var(--bad) }
  .btn.bad:hover { background:var(--bad-soft); border-color:var(--bad) }
  .btn.sm { font-size:11.5px; padding:4px 9px }
  .btn.on { background:var(--soft2); border-color:var(--dim) }
  .btn[disabled] { opacity:.5; cursor:default }
  a.lnk { color:inherit; text-decoration:none; border-bottom:1px solid var(--line) }
  a.lnk:hover { border-bottom-color:var(--muted) }
  hr.sep { border:0; border-top:1px solid var(--line); margin:14px 0 }
  .thumb { width:48px; height:48px; border-radius:8px; border:1px solid var(--line);
           object-fit:cover; display:block; background:var(--soft2) }
  #tape .shots .thumb { width:96px; height:96px }
  .thumb.s { width:38px; height:38px }
  .shots { display:flex; gap:6px; flex-wrap:wrap; margin:5px 0 3px }
  .photo-preview { display:block; border:0; border-radius:8px; padding:0; background:transparent;
                   color:inherit; cursor:zoom-in }
  .photo-preview:focus-visible { outline:2px solid var(--ac); outline-offset:2px }
  /* A PDF in the conversation: a link that opens it, with the file's name. */
  .pdf-link { display:inline-flex; align-items:center; gap:7px; max-width:100%;
              padding:7px 10px; border:1px solid var(--line); border-radius:8px;
              background:var(--soft); color:var(--fg); text-decoration:none;
              font-size:12.5px; overflow-wrap:anywhere }
  .pdf-link:hover { border-color:var(--ac) }
  .pdf-link:focus-visible { outline:2px solid var(--ac); outline-offset:2px }
  .pdf-badge { flex:none; padding:2px 5px; border-radius:4px; background:var(--ac);
               color:var(--ac-fg); font-size:10px; font-weight:600; letter-spacing:.04em }
  /* Micro-caps over each section. The same face on the customer-file head
     and the cart heading, so they read as one kind of label. */
  .cap { font-size:10px; letter-spacing:.09em; color:var(--dim); text-transform:uppercase;
         font-weight:600; margin:0 0 6px }

  /* queue rows */
  /* Rows are cards. Hover outlines one and the open one is filled and
     outlined, so "which am I looking at" is the only thing the row's shape
     says. There is no coloured rail: on the work list every row is owed, so a
     mark on all of them marked nothing. What a row still waiting says instead
     is its clock -- in the text colour, and bold.
     Scoped to the queue's own list: the customer file reuses .qrow for a
     person's earlier sessions, where an indented card would break the column. */
  .qrow { padding:11px 12px 11px 15px; cursor:pointer; position:relative }
  /* A hairline between one and the next. Without it a list of quiet rows is
     one block of text and there is nothing to say where a question ends --
     which is what it looked like once the rows stopped carrying a border of
     their own. This is a separate square-ended divider: putting the line on
     the rounded row border curved both ends upward. The row itself keeps its
     radius for the open and hovered outlines below. */
  .scroll.flush .qrow { margin:0 8px 6px; border:1px solid transparent;
                        border-radius:9px }
  .scroll.flush .qrow::after { content:""; position:absolute; left:-8px; right:-8px; bottom:0;
                               height:1px; background:var(--line); pointer-events:none }
  /* The last one has nothing under it to be separated from. */
  .scroll.flush .qrow:last-child::after { display:none }
  @media (hover:hover) {
    .scroll.flush .qrow:hover { background:var(--soft); border-color:var(--line) }
    .scroll.flush .qrow:hover::after { display:none }
    .scroll.flush .qrow.here:hover { background:var(--soft2) }
  }
  .scroll.flush .qrow.here { background:var(--soft2); border-color:var(--line) }
  .scroll.flush .qrow.here::after { display:none }
  .qrow .nm { font-weight:600; font-size:13px; letter-spacing:-.005em }
  .qrow .snip { color:var(--muted); font-size:12px; margin-top:3px }
  .qrow .meta { color:var(--dim); font-size:11.5px; margin-top:3px }
  .qrow .sm.dim { font-variant-numeric:tabular-nums }
  .qrow.owed .meta.warn { color:var(--fg) }
  .qrow.owed .sm.dim { color:var(--fg); font-weight:600 }
  /* How the answer gets to somebody who is not on the page. Said on the row
     rather than by moving the row somewhere quieter. */
  .tag { font-size:10px; font-weight:600; letter-spacing:.02em; padding:1px 6px;
         border-radius:5px; background:var(--soft2); color:var(--muted); flex:none;
         border:1px solid var(--line) }
  .tag.mail { color:var(--fg) }
  /* Somebody else is already on this row. Quiet while they are only reading
     it, and in the accent colour once they are actually writing a reply --
     that is the one worth stopping for. */
  .tag.who { color:var(--muted); background:var(--soft2);
             /* Unlike the delivery tag this one carries a name, so it is the
                one that can be long. It gives way before the row does. */
             flex:0 1 auto; min-width:0; overflow:hidden; text-overflow:ellipsis;
             white-space:nowrap }
  .tag.who.on { color:var(--ac-fg); background:var(--ac); border-color:var(--ac) }
  /* The fold that holds put-away chats, at the foot of Done for now. Dashed,
     the way a "show more" is dashed, so it is not mistaken for another chat. */
  .srow.fold { margin:2px 8px 10px; padding:11px 12px; border:1px dashed var(--line);
               border-radius:9px; color:var(--muted); font-size:12.5px }
  .srow.fold:hover { border-color:var(--dim); color:var(--fg) }

  /* transcript */
  .msg-row { display:flex; align-items:center; gap:8px; max-width:100%;
             margin:0 0 2px }
  .msg-row.out { justify-content:flex-end }
  .msg-row .stack { display:flex; flex-direction:column; min-width:0;
                    max-width:calc(100% - 36px) }
  .msg-row.in .stack { align-items:flex-start }
  .msg-row.out .stack { align-items:flex-end }
  .avatar { width:28px; height:28px; border-radius:50%; flex:none;
            display:flex; align-items:center; justify-content:center;
            font-size:12px; font-weight:700; line-height:1;
            border:1px solid transparent; user-select:none }
  .avatar.in { background:var(--in); color:var(--in-fg); border-color:var(--in-line) }
  .avatar.out { background:var(--out); color:var(--out-fg); border-color:var(--out-line) }
  .avatar.now { background:var(--now-line); color:var(--now-icon-fg);
                border-color:var(--now-line) }
  .msg { max-width:88%; padding:9px 12px; border-radius:10px; margin-bottom:10px;
         font-size:13px; line-height:1.55;
         white-space:pre-wrap; overflow-wrap:anywhere; border:1px solid transparent }
  .msg-row .msg { max-width:100%; margin-bottom:0 }
  .msg[data-message-key] { cursor:pointer }
  .msg[data-message-key]:focus-visible { outline:2px solid var(--ac); outline-offset:2px }
  /* Sender and exact local time stay out of the transcript until somebody
     asks for them. Hover previews them on a desktop; click/touch or keyboard
     activation keeps them open. Expanding inside the bubble, rather than as
     an absolutely positioned tooltip, cannot run off a 360px phone or be
     clipped by the transcript scroller. */
  .msg-detail { display:block; max-height:0; margin-top:0; padding-top:0;
                border-top:1px solid transparent; opacity:0; overflow:hidden;
                color:color-mix(in srgb, currentColor 72%, transparent);
                font-size:11px; line-height:1.4;
                white-space:normal; transition:max-height .14s ease, opacity .14s ease,
                margin-top .14s ease, padding-top .14s ease }
  .msg:focus-visible .msg-detail,
  .msg.detail-open .msg-detail { max-height:16em; margin-top:7px; padding-top:6px;
                                 border-top-color:color-mix(in srgb, currentColor 22%, transparent);
                                 opacity:1; overflow:auto }
  @media (hover:hover) {
    .msg:hover .msg-detail { max-height:16em; margin-top:7px; padding-top:6px;
                             border-top-color:color-mix(in srgb, currentColor 22%, transparent);
                             opacity:1; overflow:auto }
  }
  .msg-detail .btn { display:block; margin-top:8px; color:inherit;
                     border-color:color-mix(in srgb, currentColor 28%, transparent);
                     background:transparent }
  .msg-tr { display:block; margin-top:6px; font-size:11px; font-style:italic;
            color:color-mix(in srgb, currentColor 72%, transparent); line-height:1.35 }
  /* What the customer said reads as a filled bubble; what we said reads as an
     outlined one. Both have to stand off the panel background on their own. */
  .msg.in { background:var(--in); border-color:var(--in-line); color:var(--in-fg) }
  .msg.out { margin-left:auto; background:var(--out); border-color:var(--out-line);
             color:var(--out-fg) }
  .msg-row .msg.out { margin-left:0 }
  /* The question nobody has answered yet -- in a long thread the one thing a
     salesman is looking for. Tinted and edged rather than filled: on a grey
     field that is already the loudest thing in the thread, and it leaves Send
     as the only solid block on the screen. It goes back to an ordinary bubble
     the moment a reply lands. A colour picked in Settings paints --now. */
  .msg.now { background:var(--now); border-color:var(--now-line); color:var(--now-fg) }
  /* Sent, and on its way to Missive. Faded rather than hidden: the salesman
     should see their own words the instant they press Send, and also see that
     the customer has not had them yet. */
  .msg.sending { opacity:.65 }
  .when { font-size:10.5px; color:var(--dim); margin:0 0 12px }
  .when.r { text-align:right }
  .day { text-align:center; font-size:11px; color:var(--dim); margin:2px 0 12px }
  .crm { text-align:center; margin:0 0 12px;
         display:flex; justify-content:center; flex-wrap:wrap; gap:8px }
  .crm .btn { display:inline-flex; align-items:center; gap:6px }
  .cart { border:1px solid var(--line); border-radius:10px; padding:11px 13px;
          background:var(--soft); margin:0 0 12px }
  .cart h4 { font-size:10px; color:var(--dim); font-weight:600; margin:0 0 6px;
             text-transform:uppercase; letter-spacing:.09em }
  .cart .line { display:flex; gap:6px; font-size:12px; margin:0 0 3px }
  .cart .line:last-child { margin-bottom:0 }
  .cart .pn { font-weight:600; white-space:nowrap }
  .cart button.pn {
    background:none; border:0; padding:0; margin:0; font:inherit; font-weight:600;
    color:var(--ac); cursor:pointer; text-decoration:underline;
    text-underline-offset:2px;
  }
  .cart button.pn:hover, .cart button.pn:focus-visible { color:var(--fg) }
  .cart .qty { color:var(--dim); white-space:nowrap }
  .notice { border:1px solid var(--line); border-radius:10px; padding:11px 13px;
            background:var(--soft); font-size:12.5px; color:var(--muted);
            margin-bottom:12px }
  .notice b { color:var(--fg) }

  /* Somebody else is part way through a reply to this same customer. Muted
     on purpose: it is a reason to pause, not an alarm. */
  .typing { font-size:11.5px; color:var(--dim); margin:0 0 12px;
            display:flex; align-items:center; gap:5px }
  .typing .dots { display:inline-flex; gap:2px }
  .typing .dots i { width:3px; height:3px; border-radius:50%;
                    background:currentColor; opacity:.35;
                    animation:typedot 1.2s infinite }
  .typing .dots i:nth-child(2) { animation-delay:.18s }
  .typing .dots i:nth-child(3) { animation-delay:.36s }
  @keyframes typedot { 0%,60%,100% { opacity:.25 } 30% { opacity:1 } }
  /* A salesman who has asked for less movement gets the words and no dots. */
  @media (prefers-reduced-motion:reduce) {
    .typing .dots i { animation:none; opacity:.5 }
    .msg-detail { transition:none }
  }

  /* composer */
  /* One field at the foot of the view. The attach and voice buttons sit INSIDE
     the field's bottom edge and Send aligns to its bottom right, so the box is
     one object rather than a box with a row of controls under it. The editor
     keeps its own autogrow height; the field chrome lives on .composer. */
  .comp { flex:none; padding:10px 12px 12px }
  .composer { border:1px solid var(--line); border-radius:11px; background:var(--field);
              display:flex; flex-direction:column; overflow:hidden }
  .composer:focus-within { border-color:var(--dim); background:var(--card) }
  /* Grows with what is typed rather than scrolling inside itself: a salesman
     rewriting a draft should be able to see all of it. It stops growing at
     about ten lines, where the transcript above would otherwise disappear. */
  .editor { width:100%; border:1px solid var(--line); border-radius:8px; padding:9px 10px;
            min-height:68px; max-height:230px; background:var(--field); color:inherit;
            font:inherit; resize:none; outline:none; overflow-y:hidden;
            display:block }
  .composer .editor { border:0; background:transparent; border-radius:0; flex:none;
                      min-height:52px; padding:10px 12px; font-size:13px;
                      line-height:1.55 }
  .editor.tall { overflow-y:auto }
  /* Said out loud rather than left to the browser. Chrome paints its own grey
     here, which is not --muted and does not clear 4.5:1 on the backgrounds
     this box actually gets. opacity is for Firefox, which dims a placeholder
     colour it was given. */
  .editor::placeholder, .field::placeholder { color:var(--muted); opacity:1 }
  .field { width:100%; border:1px solid var(--line); border-radius:8px; padding:7px 9px;
           background:var(--field); color:inherit; font:inherit; font-size:13px; outline:none }
  .field:focus { border-color:var(--dim); background:var(--card) }
  .editor:focus { border-color:var(--dim); background:var(--card) }
  .composer .editor:focus { border:0; background:transparent }
  .ctools { display:flex; align-items:center; gap:4px }
  .composer .ctools { padding:5px 8px 7px }
  .composer .reply-lang { margin:0 12px 4px }
  .att { display:flex; align-items:center; gap:6px; margin-top:8px; font-size:11.5px;
         color:var(--muted) }

  /* stats */
  .seg { display:flex; border:1px solid var(--line); border-radius:8px; overflow:hidden }
  .seg button { flex:1; padding:6px 0; font:inherit; font-size:12px; color:var(--muted);
                background:none; border:0; cursor:pointer }
  .seg button:hover { color:var(--fg) }
  .seg button.on { background:var(--soft2); color:var(--fg); font-weight:600 }
  .tiles { display:grid; grid-template-columns:1fr 1fr; gap:1px; background:var(--line);
           border:1px solid var(--line); border-radius:10px; overflow:hidden }
  .tile { background:var(--card); padding:11px 12px }
  .tile b { display:block; font-size:22px; font-weight:600; line-height:1.25;
            letter-spacing:-.02em; font-variant-numeric:tabular-nums }
  .tile span { font-size:11.5px; color:var(--muted) }
  .bar { height:6px; border-radius:3px; background:var(--soft2); overflow:hidden; flex:1 }
  .bar i { display:block; height:100%; background:var(--ac) }
  .bar i.q { background:var(--dim) }

  /* settings */
  .srow { display:flex; align-items:center; gap:10px; padding:12px 0; width:100%;
          border:0; border-bottom:1px solid var(--line); background:none; color:inherit;
          font:inherit; font-size:13px; text-align:left; cursor:pointer }
  .srow.static { cursor:default }
  .sw { width:34px; height:19px; border-radius:10px; background:var(--ac); position:relative;
        flex:none; cursor:pointer; border:0; padding:0 }
  .sw::after { content:""; position:absolute; top:2.5px; right:2.5px; width:14px; height:14px;
               border-radius:50%; background:var(--card) }
  .sw.off { background:var(--soft2) }
  .sw.off::after { left:2.5px; right:auto; background:var(--dim) }

  /* overlays */
  .over { position:fixed; inset:0; background:rgba(0,0,0,.45); display:flex; align-items:center;
          padding:16px; z-index:20 }
  .dialog { background:var(--card); border:1px solid var(--line); border-radius:13px;
            padding:16px; width:100% }
  .dialog.rename-dialog { max-width:400px; margin-left:auto; margin-right:auto }
  .photo-over { padding:10px; background:rgba(0,0,0,.88); z-index:30 }
  .photo-dialog { position:relative; width:100%; height:100%; min-width:0; min-height:0;
                  display:flex; align-items:center; justify-content:center }
  .photo-dialog > img { display:block; max-width:100%; max-height:100%; object-fit:contain;
                        border-radius:8px }
  .photo-close { position:absolute; top:0; right:0; color:#fff;
                 background:rgba(0,0,0,.66); border-color:rgba(255,255,255,.45) }
  .photo-close:hover { background:rgba(0,0,0,.88); border-color:#fff }

  /* An earlier customer conversation is a real selectable row. Keep native
     button styling out while retaining focus and keyboard behaviour. */
  .history-session { display:block; width:100%; border:0; border-top:1px solid var(--line);
                     background:transparent; color:inherit; text-align:left; font:inherit }
  .history-session:hover, .history-session.on { background:var(--soft) }
  .history-transcript { padding:10px 0 2px }
  /* The conversation menu belongs to the three-dot control, not to the app's
     top-right corner. Anchoring their right edges puts the menu immediately
     below the trigger and makes its fixed minimum width grow inward on a
     narrow phone instead of beyond the screen. */
  .menu-anchor { position:relative; display:flex; align-items:center; flex:none }
  .menu { position:absolute; right:8px; top:42px; background:var(--card);
          border:1px solid var(--line); border-radius:11px; padding:5px; z-index:15;
          min-width:172px; box-shadow:0 12px 34px rgba(0,0,0,.34) }
  .menu-anchor > .conversation-menu { right:0; top:calc(100% + 4px) }
  .menu button { display:block; width:100%; text-align:left; background:none; border:0;
                 color:inherit; font:inherit; font-size:12.5px; padding:8px 10px;
                 border-radius:7px; cursor:pointer }
  .menu button:hover { background:var(--soft2) }
  .menu button.bad { color:var(--bad) }
  .empty { color:var(--dim); text-align:center; padding:30px 14px; font-size:13px;
           line-height:1.6 }
  .err { color:var(--bad); font-size:12.5px; margin:8px 0 0 }
  svg { display:block }

  /* ===== appearance customiser (shared: both shells have Settings) ====
     Theme, the panel background, and the bubble colours. Fields are
     deliberately NOT .seg -- the theme picker's choices are the last three
     .srow .seg button in the view. */
  .acfields { margin-top: 12px }
  .acfield { display:flex; align-items:center; gap:10px; padding:9px 0;
            border-top:1px dashed var(--line) }
  .acfield .aclabel { min-width:98px; flex:1; color:var(--fg); font-size:12.5px }
  .acfield .acctl { flex:none; display:flex; align-items:center; gap:8px;
                    justify-content:flex-end }
  .acfield .avatar { width:22px; height:22px; font-size:10px }
  .acfield input[type="color"] { width:38px; height:26px; padding:2px;
                                border:1px solid var(--line); border-radius:6px;
                                background:var(--soft2); cursor:pointer }
