/* ---- app.css --------------------------------------------------- */
/* hopli — phone shell
   One accent colour, flat surfaces, no glows. Icons are stroked SVG inheriting
   currentColor, so they stay legible in both themes and at any size. */

:root {
  --bg:     #f6f6f4;
  --ink:    #16171a;
  --muted:  #6d6a64;
  --faint:  #9a968e;
  --line:   #e4e1db;
  --track:  #edeae4;
  --accent: #2f6f4f;
  --ok:     #1f7a4d;
  --bad:    #b3261e;
  --wait:   #8a6d15;
  --radius: 12px;
  /* The page colour with something to see through. Used by the chat header,
     which the conversation now scrolls underneath — a solid bar has nothing to
     blur and reads as a lid rather than as part of the same surface. */
  --glass:  rgba(246, 246, 244, .86);
}

@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) {
    --bg:     #121311;
    --ink:    #eceae5;
    --muted:  #97938a;
    --faint:  #6f6c65;
    --line:   #2c2f2b;
    --track:  #272a26;
    --accent: #7fc4a0;
    --ok:     #6ecf9b;
    --bad:    #ff8a80;
    --wait:   #e0c060;
    --glass:  rgba(18, 19, 17, .86);
  }
}
:root[data-theme="dark"] {
  --bg:     #121311;
  --ink:    #eceae5;
  --muted:  #97938a;
  --faint:  #6f6c65;
  --line:   #2c2f2b;
  --track:  #272a26;
  --accent: #7fc4a0;
  --ok:     #6ecf9b;
  --bad:    #ff8a80;
  --wait:   #e0c060;
    --glass:  rgba(18, 19, 17, .86);
}

* { box-sizing: border-box; }

/* The icon sprite. A style attribute is an inline style, which style-src
   forbids, so the page reported a policy violation on every load. */
.sprite { position: absolute; }

body {
  margin: 0;
  padding: 24px 18px 48px;
  background: var(--bg);
  color: var(--ink);
  font: 15px/1.55 ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif;
  -webkit-text-size-adjust: 100%;
  -webkit-font-smoothing: antialiased;
}

main { max-width: 540px; margin: 0 auto; }

.head { margin-bottom: 22px; }
h1 {
  font-size: 21px; font-weight: 650; margin: 0;
  letter-spacing: -0.02em;
}
.sub { color: var(--muted); font-size: 13px; margin: 1px 0 0; }

/* ---- Icons -------------------------------------------------------------- */

.i { width: 20px; height: 20px; flex: none; display: block; }
.i.lg { width: 26px; height: 26px; }

/* ---- Cards -------------------------------------------------------------- */

/* Cards sit on the page rather than floating above it: same ground, outlined
   only. A raised panel implies a stack of separate things, and this is one
   screen doing one job. */
.card {
  background: transparent;
  border: 1px solid var(--line);
  border-radius: var(--radius);
  padding: 15px 16px;
  margin-bottom: 11px;
}
.card h2 {
  font-size: 11px; font-weight: 600;
  text-transform: uppercase; letter-spacing: 0.085em;
  color: var(--faint); margin: 0 0 12px;
}

.dim { color: var(--muted); }
.small { font-size: 13px; }
.lead p { margin: 0 0 8px; }
.lead p:last-child, .card p:last-child { margin-bottom: 0; }

.steps { margin: 0; padding-left: 19px; }
.steps li { margin-bottom: 5px; }
.steps li:last-child { margin-bottom: 0; }
.steps code, code {
  font: 12px ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  background: var(--track); padding: 1.5px 5px; border-radius: 5px;
}

/* ---- Connection --------------------------------------------------------- */

/* Sticky, because "am I connected?" is the question people ask constantly.
   No frame. The link, its status icon and the address are the content; a box
   around them was drawing attention to the container instead.
   It keeps the page background rather than none at all, because it is sticky
   and content scrolling underneath must not show through. */
.conn {
  position: sticky; top: 0; z-index: 5;
  background: var(--bg);
  padding: 12px 0 16px;
  margin-bottom: 6px;
}
/* Visible to assistive technology, not to the eye. */
.sr {
  position: absolute; width: 1px; height: 1px; margin: -1px;
  padding: 0; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap;
  border: 0;
}

.conn-sub {
  margin-top: 11px; font-size: 12.5px; color: var(--muted);
  word-break: break-all; text-align: center;
}
.conn-sub code { font-size: 12px; }

/* With the frame gone, state is carried entirely by the icon, the line and the
   colour of the address beneath them. */
.conn[data-state="off"] .conn-sub { color: var(--bad); }

@keyframes pulse { 0%, 100% { opacity: 1; } 50% { opacity: .35; } }
/* Targets the pip, which is what actually animates now. Pointing this at the
   old status dot left the pulse running for anyone who asked it not to. */
@media (prefers-reduced-motion: reduce) {
  .pip svg { animation: none !important; transition: none !important; }
}

.path { display: flex; align-items: center; gap: 9px; color: var(--muted); margin-top: 14px; }
.path .line {
  flex: 1; height: 2px; background: var(--line); border-radius: 2px;
  transition: background .2s;
}

/* The status icon, centred on the link. Its ring sits on the page background so
   the line appears to pass behind it rather than stopping at it. */
.pip {
  display: flex; align-items: center; justify-content: center;
  width: 28px; height: 28px; flex: none; border-radius: 50%;
  background: var(--bg);
  border: 1.5px solid var(--line);
  color: var(--faint);
  transition: border-color .2s, color .2s;
}

.conn[data-state="on"] .line { background: var(--ok); }
.conn[data-state="on"] .pip  { border-color: var(--ok); color: var(--ok); }

.conn[data-state="connecting"] .pip,
.conn[data-state="pairing"] .pip { border-color: var(--wait); color: var(--wait); }
.conn[data-state="connecting"] .pip svg,
.conn[data-state="pairing"] .pip svg { animation: pulse 1.1s ease-in-out infinite; }

.conn[data-state="off"] .line { background: var(--line); }
.conn[data-state="off"] .pip  { border-color: var(--bad); color: var(--bad); }

/* ---- Pairing fingerprint ------------------------------------------------ */

#fp {
  font: 600 32px/1.2 ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  font-variant-numeric: tabular-nums;
  letter-spacing: 0.14em;
  text-align: center;
  padding: 10px 0 14px;
}
#pairing.ok  { border-color: var(--ok); }
#pairing.bad { border-color: var(--bad); }
#pairing.bad #pairstatus { color: var(--bad); }

/* ---- Drop zone ---------------------------------------------------------- */

#drop {
  display: flex; align-items: center; gap: 12px; width: 100%;
  border: 1.5px dashed var(--line); border-radius: var(--radius);
  padding: 18px 16px; text-align: left;
  background: transparent; color: var(--muted);
  font: inherit; font-size: 14px; cursor: pointer;
  transition: border-color .15s, background .15s;
}
#drop b { color: var(--ink); font-weight: 600; }
#drop:hover { border-color: var(--accent); }
#drop.hot { border-color: var(--accent); background: var(--track); }
#drop.off { opacity: .4; pointer-events: none; }
#pick { display: none; }

/* ---- Transfer rows ------------------------------------------------------ */

.file { margin-top: 14px; font-size: 13px; }
.file .row1 { display: flex; justify-content: space-between; gap: 12px; }
.file .nm { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.file .pc {
  color: var(--muted); flex: none; font-variant-numeric: tabular-nums;
  font-size: 12px;
}
.file .rmeta {
  color: var(--faint); font-size: 11.5px; margin-top: 4px;
  font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
}
/* The progress bar for the card-based file rows this page used to have, before
   the conversation replaced them. Nothing has carried class="bar" for a while
   except the chat header — and since both sheets load over the same document,
   this was giving that header a 7px top margin, which showed as a strip of the
   conversation running above it and through the glass. The class guard had this
   pair listed as deliberate on the grounds that no element ever carried both.
   That stopped being true; the listing is gone and so is this.

   .file .pc and .file .rmeta stay: the offer rows above still use them. */
.file.err  .pc, .file.err .rmeta { color: var(--bad); }

/* ---- Incoming offers ---------------------------------------------------- */

.offer {
  border: 1px solid var(--line); border-radius: var(--radius);
  padding: 14px 15px; margin-top: 11px;
}
.offer .on { font-weight: 600; font-size: 14px; word-break: break-all; }
.offer .os {
  color: var(--muted); font-size: 12px; margin-top: 4px;
  font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
}
.offer.done { border-color: var(--ok); }
.offer.done .os { color: var(--ok); }
.offer.err  { border-color: var(--bad); }
.offer.err  .os { color: var(--bad); }

.btns { display: flex; gap: 8px; margin-top: 12px; flex-wrap: wrap; }
.btns button {
  font: 600 13px ui-sans-serif, system-ui, sans-serif;
  border: 1px solid var(--line); background: transparent; color: var(--ink);
  border-radius: 9px; padding: 9px 15px; cursor: pointer;
  transition: border-color .15s;
}
.btns button:hover:not(:disabled) { border-color: var(--accent); }
.btns button.go {
  background: var(--accent); border-color: transparent; color: var(--bg);
}
.btns button:disabled { opacity: .4; cursor: default; }


[hidden] { display: none !important; }

/* ---- Footer ------------------------------------------------------------- */

.foot {
  display: flex; gap: 18px; flex-wrap: wrap; justify-content: center;
  margin-top: 30px; font-size: 12.5px;
}
.foot a { color: var(--muted); text-decoration: none; }
.foot a:hover { color: var(--ink); text-decoration: underline; }
.head a { color: var(--muted); text-decoration: none; }
.head a:hover { text-decoration: underline; }

/* Verified receipt — a drawn tick, never a character glyph. */
.vi { width: 13px; height: 13px; flex: none; color: var(--ok); }
.file .rmeta, .offer .os {
  display: flex; align-items: center; gap: 5px;
}
.file.done .rmeta, .offer.done .os { color: var(--ok); }

/* ---- One list, both directions ------------------------------------------ */

.i.sm { width: 14px; height: 14px; }
.file .row1 { align-items: center; gap: 8px; }
.file .nm { flex: 1; min-width: 0; }
.file .i.sm { color: var(--faint); }
.file.done .i.sm { color: var(--ok); }
.file.err  .i.sm { color: var(--bad); }
.offer .on { display: flex; align-items: center; gap: 8px; }
.offer .on .i.sm { color: var(--faint); }
#transfers:empty + #idle { display: block; }

/* Across the top, above everything, only while it is true. */
.offline {
  position: fixed; top: 0; left: 0; right: 0; z-index: 60; margin: 0;
  padding: 10px 16px;
  padding-top: max(10px, env(safe-area-inset-top));
  text-align: center; font-size: 13px; font-weight: 600;
  background: #7a2e2e; color: #fff;
}
.offline[hidden] { display: none; }

/* The mark, rather than the word set in whatever the system font happens to be.
   Width given in pixels and height left to follow, with both on the tag as
   well, so the space is reserved before the file has loaded and the screen
   does not jump under a thumb already reaching for the button. */
.wordmark { display: block; width: 116px; height: auto; margin: 0 0 3px; }

/* ---- The one thing to do on the landing page ----------------------------- */

/* An anchor rather than a button, because it goes somewhere: a phone can
   long-press a link and offer to open it in a new tab, which it cannot do with
   a button. Named .start rather than reusing .wide, which belongs to p.css and
   is not loaded here. */
.start {
  display: flex; align-items: center; justify-content: center;
  box-sizing: border-box; width: 100%; min-height: 52px;
  margin: 0 0 12px; border-radius: 14px;
  background: var(--accent); color: var(--bg); text-decoration: none;
  font: 650 16px ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif;
}
.start:hover { text-decoration: none; }

/* The same shape, outlined. Starting is the ordinary thing to do on this page;
   joining is what you do when somebody has sent you something, so it is the
   quieter of the two rather than a second thing competing for the same press. */
.start.ghosted {
  background: transparent; color: var(--ink);
  border: 1px solid var(--line);
}

/* A link inside the writing, rather than one the browser has styled for us.
   Default blue on a green-and-stone palette reads as something that arrived
   from somewhere else, which is precisely what it is. */
.card a { color: var(--accent); text-underline-offset: 2px; }
.card a:hover { text-decoration: none; }

/* ---- chat.css -------------------------------------------------- */
/* The conversation, shared by both pairing paths.
   
   These rules used to live in p.css, where only the phone-to-phone page could
   see them, while the LAN page kept a screen of cards. One product, one
   surface: both pages load this, and what stays in their own stylesheet is the
   screen that comes before a connection exists.
   
   A transfer is something that happened between two people, in order, with a
   direction — which is what a message list is for. Cards made it look like
   configuration. */

/* ---- The conversation --------------------------------------------------- */

/* Fills the viewport so the composer sits on the bottom edge rather than after
   the content, which is what makes it read as a chat and not a page.
   
   Height is dvh rather than inset:0 or vh. Both of those use the layout
   viewport, which on a phone is taller than what you can actually see — the
   composer ends up under the browser's own bar. dvh follows the visible area as
   that bar grows and shrinks; vh is kept above it for anything too old. */
.chat {
  position: fixed; left: 0; right: 0; top: 0;
  height: 100vh;
  height: 100dvh;
  display: flex; flex-direction: column;
  background: var(--bg);
  overflow: hidden;

  /* How much room the header takes, including whatever the phone reserves at
     the top of the screen for itself. Named because three other rules have to
     agree with it and none of them can see it. */
  --top: calc(60px + env(safe-area-inset-top, 0px));
}

/* A fixed height so it cannot grow with its contents, plus whatever the notch
   needs above it. The previous rule let long status text push the bar taller
   and squeeze the conversation. */
/* Over the conversation rather than above it.
   
   A header sitting in the flow has nothing scrolling underneath it, so there is
   nothing for a blur to blur and the translucency is wasted — it reads as a lid
   on a separate box. Lifted out of the flow, the messages pass under it, which
   is what makes it read as the same surface seen through frosted glass.
   
   The page colour rather than a colour of its own, so the top of the screen is
   the same material as the rest of it. */
.bar {
  position: absolute; left: 0; right: 0; top: 0; z-index: 5;
  display: flex; align-items: center; gap: 12px;
  height: 60px; box-sizing: content-box;
  padding: 0 16px;
  padding-top: env(safe-area-inset-top, 0px);
  /* No rule underneath. A line was how a header in the flow said where it
     ended; a header the conversation passes under says that by being frosted,
     and the rule on top of that reads as a seam in one surface. */
  background: var(--glass);
  -webkit-backdrop-filter: blur(16px) saturate(1.6);
  backdrop-filter: blur(16px) saturate(1.6);
}

/* A browser with no backdrop-filter would show the conversation straight
   through the header, which is worse than no glass at all. */
@supports not ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) {
  .bar { background: var(--bg); }
}
/* Who, with how in the corner. The two used to share one circle, and the
   connection won it — a grey disc with a coloured light in the middle, saying
   nothing about the person on the other end. */
.avatar {
  position: relative; flex: none;
  width: 40px; height: 40px; border-radius: 50%;
  display: flex; align-items: center; justify-content: center;
  background: var(--track); color: var(--ink);
}
.initials {
  font: 650 14px/1 ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif;
  letter-spacing: .02em; user-select: none;
}

/* A colour that follows from the name, so the same person looks the same on
   both phones without either of them being told. Tinted rather than saturated:
   a header is not the place for six competing blocks of colour. */
.avatar[data-tone="0"] { background: #d8e6dd; color: #23553f; }
.avatar[data-tone="1"] { background: #e2e2ef; color: #3a3a63; }
.avatar[data-tone="2"] { background: #f0e2d8; color: #6b452c; }
.avatar[data-tone="3"] { background: #e6e3d3; color: #55521f; }
.avatar[data-tone="4"] { background: #efdde2; color: #6b2f42; }
.avatar[data-tone="5"] { background: #d6e5ea; color: #274c58; }

@media (prefers-color-scheme: dark) {
  .avatar[data-tone="0"] { background: #21382c; color: #a9d6bd; }
  .avatar[data-tone="1"] { background: #2a2a3d; color: #bcbcdf; }
  .avatar[data-tone="2"] { background: #3a2b21; color: #e0bda4; }
  .avatar[data-tone="3"] { background: #35331d; color: #d6d19a; }
  .avatar[data-tone="4"] { background: #3a2229; color: #e3aebd; }
  .avatar[data-tone="5"] { background: #1f3640; color: #a6cfdd; }
}

/* The connection, as a badge rather than as the whole of it. */
.sdot {
  position: absolute; right: -1px; bottom: -1px;
  width: 12px; height: 12px; border-radius: 50%;
  background: var(--faint);
  box-shadow: 0 0 0 2.5px var(--bg);
}
.sdot.work { background: var(--wait); animation: pulse 1.1s ease-in-out infinite; }
.sdot.on   { background: var(--ok); }
.sdot.bad  { background: var(--bad); }
@media (prefers-reduced-motion: reduce) { .sdot { animation: none !important; } }

.whobox { min-width: 0; }
.whobox b { display: block; font-size: 15px; font-weight: 650; letter-spacing: -0.01em; }
.sub2 {
  display: block; font-size: 12px; color: var(--muted);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}

/* The room the header is taking is padding here rather than a gap above,
   because the point of lifting the header out of the flow is that this scrolls
   under it. */
.stream {
  flex: 1; overflow-y: auto;
  padding: calc(var(--top) + 14px) 14px 18px;
  display: flex; flex-direction: column; gap: 10px;
  -webkit-overflow-scrolling: touch;
}
.empty {
  margin: auto; color: var(--faint); font-size: 13px; text-align: center;
}

/* Not from either person: something that happened to the connection. */
.sys {
  align-self: center; max-width: 82%; margin: 6px 0;
  font-size: 12px; line-height: 1.5; text-align: center; color: var(--muted);
}

/* ---- Bubbles ------------------------------------------------------------ */

.msg { display: flex; max-width: 100%; }
.msg.out { justify-content: flex-end; }
.msg.in  { justify-content: flex-start; }

.bubble {
  max-width: 78%; min-width: 0;
  border-radius: 18px; padding: 8px 14px 8px 8px;
  background: var(--track);
  border: 1px solid transparent;
}
.msg.out .bubble {
  background: var(--accent); color: var(--bg);
  border-bottom-right-radius: 6px;
}
.msg.in .bubble { border-bottom-left-radius: 6px; }

/* A file row is a glyph and two lines, and the glyph is the button.
   
   Named .ficon and not .lead: app.css already owns .lead for the intro card on
   the first screen, and the two sheets load together. Taking the name turned
   that card into a 38px circle with its text spilling out one word per line.
   
   This is what every messaging app converged on, and the reason is that a file
   has exactly one thing you can do to it. Given a row and a separate Save
   button underneath, the row becomes a form; given the action on the file's own
   icon, it stays a message. */
.fline { display: flex; align-items: center; gap: 10px; }

.ficon {
  width: 38px; height: 38px; flex: none; padding: 0; margin: 0;
  display: flex; align-items: center; justify-content: center;
  border: 0; border-radius: 50%; cursor: default;
  background: rgba(128, 128, 128, .22); color: inherit;
}
.ficon .i { width: 17px; height: 17px; opacity: .8; }
/* Only when there is something to be done to it. */
.msg.in .bubble .ficon.can {
  background: var(--accent); color: var(--bg); cursor: pointer;
}
.msg.in .bubble .ficon.can .i { opacity: 1; }
.msg.in .bubble .ficon.took { background: transparent; color: var(--ok); }
.msg.in .bubble .ficon.took .i { opacity: 1; }

.ftext { min-width: 0; display: flex; flex-direction: column; gap: 2px; }
.fname {
  font-size: 14px; font-weight: 600; min-width: 0;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.fmeta { font-size: 11.5px; opacity: .75; }
.msg.in .fmeta { color: var(--muted); opacity: 1; }

/* Progress reads as part of the bubble rather than a widget dropped into it. */
.bar2 {
  height: 3px; border-radius: 3px; margin-top: 9px; overflow: hidden;
  background: rgba(128, 128, 128, .28);
}
.bar2 i { display: block; height: 100%; width: 0; background: currentColor; transition: width .12s linear; }

.receipt {
  display: flex; align-items: center; gap: 5px;
  font-size: 11.5px; margin-top: 8px; opacity: .85;
}
.vi { width: 13px; height: 13px; flex: none; }
/* No outline on a delivered file. A green box around a message reads as a form
   that validated, not as something someone sent you; the word on the row and
   the tick on the icon already say it arrived intact. */
.msg.in .bubble.done .receipt { color: var(--ok); opacity: 1; }
.bubble.err { border-color: var(--bad); }
.msg.in .bubble.err .receipt, .msg.in .bubble.err .fmeta { color: var(--bad); }

.acts { display: flex; gap: 8px; margin-top: 11px; flex-wrap: wrap; }
.acts button {
  font: 600 13px ui-sans-serif, system-ui, sans-serif;
  padding: 8px 14px; border-radius: 9px; margin: 0;
  border-color: var(--line);
}
.msg.in .acts button.yes { background: var(--accent); border-color: transparent; color: var(--bg); }

/* ---- Composer ----------------------------------------------------------- */

/* Attach on the left, the field in the middle, send on the right — the shape
   every messaging app has settled on, and the one thumbs already know. */
.composer {
  flex: none; display: flex; align-items: flex-end; gap: 4px;
  padding: 7px 8px;
  padding-bottom: max(7px, env(safe-area-inset-bottom));
  border-top: 1px solid var(--line);
  background: var(--bg);
}
.composer.off { opacity: .45; pointer-events: none; }

/* A ring only for something happening to the field, not for being in it.
   Tapping the box already puts a cursor in it and raises the marks above it —
   an outline as well is a third way of saying the same thing, and the loudest
   of the three. A file dragged over is a different matter: nothing else on
   screen says the box would take it. */
.composer.hot .field { border-color: var(--accent); }

/* What this connection carries, applied to the surface. */
.composer.nofiles .attach { display: none; }
.composer.notext .field, .composer.notext .sendbtn { display: none; }

/* With no message field there is nothing for a bare glyph to sit beside, and a
   lone plus on an empty bar reads as something half-loaded. It becomes the
   control it actually is. */
.composer.notext .attach {
  flex: 1; width: auto; height: 46px; gap: 9px;
  border-radius: 23px; background: var(--track); color: var(--ink);
  font: 600 15px ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif;
}
.composer.notext .attach::after { content: 'Send a file'; }
.composer.notext .attach .i { width: 21px; height: 21px; }

.attach, .sendbtn {
  flex: none; padding: 0; margin: 0;
  display: flex; align-items: center; justify-content: center;
  border-radius: 50%; border: 0; background: transparent;
}

/* A bare glyph. The field beside it is the only filled shape down here, and
   that is what keeps the row reading as one control rather than three. */
.attach { width: 44px; height: 44px; color: var(--muted); }
.attach .i { width: 26px; height: 26px; }

/* Send arrives with the first character and leaves with the last. A permanently
   greyed circle is a control that spends most of its life saying no. */
.sendbtn {
  width: 36px; height: 36px; margin: 0 2px 4px;
  background: var(--accent); color: var(--bg);
}
.sendbtn .i { width: 19px; height: 19px; }
.composer:not(.ready) .sendbtn { display: none; }

.field {
  flex: 1; min-width: 0; display: flex; align-items: center;
  min-height: 44px; box-sizing: border-box;
  background: var(--track); border: 1px solid transparent;
  /* A pill while the message is one line, a rounded rectangle once it is more.
     
     One number does both, because a border radius is clamped to half the box:
     at one line 24 is more than half of 44 and renders as a full half-circle at
     each end, and at four lines it is 24 of a hundred-odd and renders as an
     ordinary rounded corner.
     
     999px was not that. It stayed a stadium at every height, so a four-line
     message sat inside a lozenge with its text pressed into the curve. */
  border-radius: 24px;
  padding: 10px 16px;
  transition: border-color .15s;
}
.field textarea {
  display: block; width: 100%; border: 0; outline: 0; resize: none;
  background: transparent; color: var(--ink);
  /* Sixteen, and not a pixel less.
     Safari zooms into any field it is asked to focus whose text is smaller than
     that, and the zoom is what takes the sides of the page off screen — the
     layout never changed, the browser simply magnified it and the edges went
     past the glass. Every field somebody can type into is 16px for this reason
     and no other. */
  font: 16px/1.4 ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif;
  /* Grows with what is typed, then scrolls rather than eating the screen. */
  max-height: 96px;
}
.field textarea::placeholder { color: var(--faint); }

#pick { display: none; }

/* ---- Pictures and video -------------------------------------------------

   Shown at the top of the bubble, above the name, because the picture is the
   message and the filename is the footnote. Width is the bubble's; height is
   capped so one tall photo cannot take the whole conversation with it. */

.shot {
  margin: 1px 0 9px; border-radius: 12px; overflow: hidden;
  background: rgba(128, 128, 128, .16);
}
/* Letterboxed rather than cropped, both of them. Cropping is what a gallery
   does, where the point is the grid; here the point is whether this is the
   right file, and a screenshot cropped to its middle answers nothing. */
.thumb {
  display: block; width: 100%; height: auto; max-height: 380px;
  object-fit: contain;
}
video.thumb { background: #000; }

/* Once there is a picture, the bubble is the picture.

   Nothing is drawn on top of it. Chips over an image are how a gallery marks up
   a thumbnail, and they turn a message back into an item in a list — the thing
   the file row was already doing wrong. What still has to be said goes into a
   caption underneath, where a timestamp would go, and the picture is left to be
   a picture. */
.msg .bubble.media {
  position: relative;
  padding: 0; background: transparent; border: 0;
  min-width: 0; max-width: 268px;
}
.bubble.media .fline,
.bubble.media .fmeta,
.bubble.media .acts { display: none; }

.bubble.media .shot { margin: 0; border-radius: 16px; }
.msg.out .bubble.media .shot { border-bottom-right-radius: 6px; }
.msg.in .bubble.media .shot { border-bottom-left-radius: 6px; }
.shot.tap { cursor: pointer; }

/* The one exception, and only while it is true: during a transfer there is
   nothing else to look at yet. */
.bubble.media .bar2 {
  position: absolute; left: 0; right: 0; bottom: 0; margin: 0;
  height: 3px; border-radius: 0; background: rgba(0, 0, 0, .38);
}
.bubble.media .bar2 i { background: #fff; }

/* The caption. Quiet enough to ignore, present enough to be checked. */
.msg.in .bubble.media .receipt,
.msg.out .bubble.media .receipt,
.msg.in .bubble.media.done .receipt {
  position: static; margin: 6px 3px 0; padding: 0;
  background: transparent; color: var(--muted); opacity: 1;
  font-size: 11.5px; font-weight: 500;
}
.msg.out .bubble.media .receipt { justify-content: flex-end; }
.msg.in .bubble.media.err .receipt { color: var(--bad); }

/* Saving is a word in the caption, not a button over the picture. */
.receipt .link {
  font: inherit; background: none; border: 0; padding: 0; margin: 0;
  color: var(--accent); font-weight: 650; cursor: pointer;
}

/* If it will not decode it is a file again, and everything it was hiding
   comes back. */
.bubble.media.plain .fline { display: flex; }
.bubble.media.plain .fmeta, .bubble.media.plain .acts { display: block; }
.bubble.media.plain { padding: 11px 13px 10px; background: var(--track); }

/* ---- Text messages ------------------------------------------------------ */

.bubble.text {
  min-width: 0; padding: 9px 13px 8px;
  font-size: 15px; line-height: 1.4; word-break: break-word;
  white-space: pre-wrap;
}

/* ---- Looking at something full screen ------------------------------------
   
   Over everything, out of the way of nothing. The picture is the content and
   the two things you can do to it sit at the top edge where a thumb reaches:
   back on the left, keep on the right. */

.stage {
  position: fixed; inset: 0; z-index: 50;
  display: flex; flex-direction: column;
  background: #0b0c0a;
  animation: rise .18s ease-out;
}
.stage[hidden] { display: none; }
@keyframes rise { from { opacity: 0; } to { opacity: 1; } }
@media (prefers-reduced-motion: reduce) { .stage { animation: none; } }

/* The conversation stops scrolling behind it. */
body.behind { overflow: hidden; }

.stage-bar {
  flex: none; display: flex; align-items: center; gap: 12px;
  padding: 10px 8px;
  padding-top: max(10px, env(safe-area-inset-top));
}
.stage-name {
  flex: 1; min-width: 0;
  font-size: 14px; font-weight: 600; color: #f2f1ec;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.stage-btn {
  width: 44px; height: 44px; flex: none; padding: 0; border: 0;
  display: flex; align-items: center; justify-content: center;
  border-radius: 50%; cursor: pointer;
  background: rgba(255, 255, 255, .1); color: #f2f1ec;
}
.stage-btn:hover { background: rgba(255, 255, 255, .18); }
.stage-btn:focus-visible { outline: 2px solid #f2f1ec; outline-offset: 2px; }
.stage-btn .i { width: 21px; height: 21px; }
.stage-btn[hidden] { display: none; }

.stage-body {
  flex: 1; min-height: 0;
  display: flex; align-items: center; justify-content: center;
  overflow: auto;
}
.stage-media {
  max-width: 100%; max-height: 100%;
  display: block; object-fit: contain;
}
img.stage-media { cursor: zoom-in; transition: transform .18s ease-out; }
/* Zoomed, the picture is bigger than the frame and the frame scrolls. */
img.stage-media.close-up {
  cursor: zoom-out; transform: scale(2.2); transform-origin: center;
}
@media (prefers-reduced-motion: reduce) { img.stage-media { transition: none; } }

.stage-hint {
  flex: none; margin: 0; padding: 10px 16px;
  padding-bottom: max(10px, env(safe-area-inset-bottom));
  text-align: center; font-size: 12px; color: rgba(242, 241, 236, .5);
}

/* A picture you can open should look like one. */
.shot.tap { cursor: zoom-in; }

/* ---- Voice messages ------------------------------------------------------ */

/* A recording has no filename anybody wants to read and no size anybody wants
   to know. What it has is a length and a shape, and those are the whole bubble.

   Everything here draws itself in currentColor, so the same rules land white on
   green going out and dark on grey coming in without a second set of them. */
.bubble.voice { padding: 7px 12px 7px 7px; }
.bubble.voice .fline { display: none; }

.vc { display: flex; align-items: center; gap: 10px; }

.vplay {
  flex: none; width: 34px; height: 34px; padding: 0; border: 0;
  display: flex; align-items: center; justify-content: center;
  border-radius: 50%; cursor: pointer;
  background: var(--accent); color: var(--bg);
}
.msg.out .bubble.voice .vplay { background: var(--bg); color: var(--accent); }
.vplay .i { width: 15px; height: 15px; }
.vplay:disabled { opacity: .4; cursor: default; }

/* The waveform is the scrubber. A slider underneath it would be a second
   drawing of the same line, and there is only room down here for one. */
/* Fixed bars rather than a share of the row: the number of them already says
   how long the message is, so letting them stretch would throw that away and
   size every bubble the same. */
.wave {
  flex: none; height: 26px;
  display: flex; align-items: center; gap: 2px;
}
.wave.seek { cursor: pointer; }
.wave i {
  flex: none; width: 3px;
  border-radius: 2px; background: currentColor;
  opacity: .32; transition: opacity .1s linear;
}
/* Played, rather than a bar sliding over the top of it: the part behind you is
   solid and the part ahead is not, which is the same picture without a second
   element to keep in step. */
.wave i.on { opacity: 1; }

.vtime {
  flex: none; font-size: 12px; font-variant-numeric: tabular-nums;
  opacity: .75; min-width: 30px; text-align: right;
}

/* ---- The microphone ------------------------------------------------------ */

/* An empty composer offers a microphone where the send arrow will be. Only one
   of the two is ever there, because only one of them can be meant at a time. */
.mic {
  flex: none; padding: 0; margin: 0 2px 4px;
  width: 40px; height: 40px;
  display: flex; align-items: center; justify-content: center;
  border: 0; border-radius: 50%; background: transparent;
  color: var(--muted); cursor: pointer;
}
.mic .i { width: 23px; height: 23px; }
.composer.ready .mic { display: none; }
/* Speaking is a recording, and a recording is a file. A connection that was
   asked to carry only text does not carry this either. */
.composer.nofiles .mic { display: none; }

/* ---- While the microphone is open ---------------------------------------- */

/* The row is given over entirely. There are exactly two things that can happen
   next — throw it away or send it — and they sit at the two ends with what is
   being recorded between them. */
.rec { display: none; }
.composer.taping .attach,
.composer.taping .field,
.composer.taping .mic,
.composer.taping .sendbtn { display: none; }
.composer.taping .rec {
  flex: 1; min-width: 0; display: flex; align-items: center; gap: 9px;
  height: 44px; padding: 0 5px 0 3px;
  background: var(--track); border-radius: 22px;
}

.recbin {
  flex: none; width: 36px; height: 36px; padding: 0;
  display: flex; align-items: center; justify-content: center;
  border: 0; border-radius: 50%; background: transparent;
  color: var(--muted); cursor: pointer;
}
.recbin .i { width: 21px; height: 21px; }

.recdot {
  flex: none; width: 8px; height: 8px; border-radius: 50%;
  background: var(--bad); animation: recpulse 1.4s ease-in-out infinite;
}
@keyframes recpulse { 0%, 100% { opacity: 1; } 50% { opacity: .25; } }
@media (prefers-reduced-motion: reduce) { .recdot { animation: none; } }

.rectime {
  flex: none; font-size: 13px; font-variant-numeric: tabular-nums;
  color: var(--ink); min-width: 32px;
}

/* A meter, not a plot: it fills from the right, so the newest sound is always
   at the end and the strip never grows under the fingers on either side. */
.reclive {
  flex: 1; min-width: 0; height: 22px; overflow: hidden;
  display: flex; align-items: center; justify-content: flex-end; gap: 2px;
}
.reclive i {
  flex: none; width: 3px; border-radius: 2px;
  background: var(--muted); opacity: .8;
}

.recgo {
  flex: none; width: 36px; height: 36px; padding: 0;
  display: flex; align-items: center; justify-content: center;
  border: 0; border-radius: 50%; cursor: pointer;
  background: var(--accent); color: var(--bg);
}
.recgo .i { width: 19px; height: 19px; }

/* ---- Marked-up messages -------------------------------------------------- */

/* The message keeps its own line breaks, so pre-wrap rather than a stack of
   paragraph rules that would fight it. */
.bubble.text { white-space: pre-wrap; }
.bubble.text b { font-weight: 650; }
.bubble.text i { font-style: italic; }
.bubble.text u { text-underline-offset: 2px; }
.bubble.text code,
.bubble.text pre.block {
  font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  font-size: .93em;
  background: rgba(127, 127, 127, .17);
  border-radius: 5px;
}
.bubble.text code { padding: 1px 4px; }
.bubble.text pre.block {
  display: block; margin: 4px 0; padding: 8px 10px;
  white-space: pre; overflow-x: auto;
}
.bubble.text blockquote {
  display: block; margin: 3px 0; padding: 1px 0 1px 10px;
  border-left: 3px solid currentColor; opacity: .82;
}

/* A link says where it goes and goes there. There is no syntax for giving one
   different words, so what is underlined is always the address. */
.bubble.text a { color: inherit; text-underline-offset: 2px; }
.msg.in .bubble.text a { color: var(--accent); }

/* Covered until asked. The block sits over the words rather than replacing
   them, so the shape of what is hidden is still the shape of what is there.

   The block is a flat tint rather than currentColor. currentColor resolves
   against this element's own colour, which the line below sets to transparent
   — so the cover painted itself invisible and the spoiler was not hidden, it
   was gone, leaving a hole in the sentence the width of the missing words. */
.spoiler {
  background: rgba(125, 125, 125, .62); color: transparent;
  border-radius: 4px; cursor: pointer;
  transition: background .18s, color .18s;
  -webkit-user-select: none; user-select: none;
}
.msg.out .spoiler { background: rgba(255, 255, 255, .5); }
.spoiler.shown, .msg.out .spoiler.shown {
  background: transparent; color: inherit; cursor: auto;
  -webkit-user-select: auto; user-select: auto;
}
/* Marks nested inside keep their own colour, so they have to be hidden in
   their own right — and a link under the cover must not be reachable. */
.spoiler * { visibility: hidden; }
.spoiler:not(.shown) * { pointer-events: none; }
.spoiler.shown * { visibility: visible; }

/* ---- Receipts ------------------------------------------------------------ */

/* Sent, delivered, read. Trailing the last word rather than pinned to a corner:
   it wraps with the text instead of reserving a notch the message has to avoid,
   and a one-word message stays one word wide. */
.ticks {
  display: inline-block; vertical-align: -2px;
  margin-left: 7px; width: 15px; height: 13px;
  opacity: .55; transition: opacity .2s;
}
.ticks svg { width: 15px; height: 13px; display: block; }
/* Read is the one worth telling apart at a glance. Said with weight rather
   than with a colour: the bubble's own text colour is already the highest
   contrast available against it, in both themes, and a colour picked to stand
   out on a dark green bubble is a colour that disappears on a light green one.
   One tick sent, two delivered, two at full strength read. */
.ticks[data-state="seen"] { opacity: 1; }

/* ---- The formatting bar -------------------------------------------------- */

/* The bar sits above the composer and takes over its top rule, so the two of
   them read as one thing rather than as two stacked bars with a line between. */
.fmt {
  flex: none; display: flex; gap: 2px;
  padding: 5px 8px 0;
  border-top: 1px solid var(--line);
  background: var(--bg);
  overflow-x: auto; scrollbar-width: none;
}
.fmt:not([hidden]) + .composer { border-top: 0; }
.fmt::-webkit-scrollbar { display: none; }
.fmt button {
  flex: 1; min-width: 44px; height: 34px; padding: 0;
  border: 0; border-radius: 8px; background: transparent;
  color: var(--faint); cursor: pointer;
  transition: color .15s;
  font: 15px ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif;
  display: flex; align-items: center; justify-content: center;
}
.fmt.armed button { color: var(--ink); }
.fmt button:active { background: var(--track); color: var(--ink); }
.fmt button.mono {
  font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  font-size: 13px;
}
.fmt button .i { width: 19px; height: 19px; }
/* The bar belongs to the message field, and goes when the field does. Done in
   the script rather than a sibling selector: the bar sits above the composer,
   so there is no combinator pointing the right way that every browser has. */
.fmt[hidden] { display: none; }

/* ---- What can be done to one message ------------------------------------- */

/* A long press is how every chat app reaches these, and a press cannot happen
   while the browser is busy starting a text selection. Only where there is no
   mouse: on a desktop, selecting the words of a message is still worth having,
   and a right-click gets to the same place. */
@media (pointer: coarse) {
  /* On the row and on everything in it. Inheritance covers this in the
     browsers checked here, and stating it costs nothing next to a phone that
     highlights a bubble blue and puts its own Copy bar over ours. */
  .msg, .msg * {
    -webkit-user-select: none; user-select: none;
    -webkit-touch-callout: none;
    -webkit-user-drag: none;
  }
  /* Except a spoiler that has been uncovered: the words are readable now, so
     they are worth being able to take. */
  .msg .spoiler.shown, .msg .spoiler.shown * {
    -webkit-user-select: auto; user-select: auto;
  }
}

.msg { flex-wrap: wrap; }

/* Where a quoted message was, once you have followed the quote back to it. */
.msg.found .bubble { outline: 2px solid var(--accent); outline-offset: 2px; }

/* ---- The message being answered ------------------------------------------ */

/* Drawn inside the answer, at the top, because it is what the answer is about
   and so it is read first. The excerpt travels with the reply rather than being
   looked up, so the quote still reads on a phone that never had the original. */
.quote {
  display: block; width: 100%; text-align: left; cursor: pointer;
  margin: 0 0 6px; padding: 2px 0 3px 9px;
  border: 0; border-left: 3px solid currentColor;
  /* A button carries a corner radius of its own, which bends the rule down the
     side of the quote into a parenthesis. It is a margin line; it is straight. */
  border-radius: 0;
  background: transparent; color: inherit; font: inherit;
  opacity: .78;
}
.quote b { display: block; font-size: 11px; font-weight: 650; }
.quote span {
  display: block; font-size: 12.5px;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}

.edited { font-size: 11px; font-style: italic; opacity: .7; margin-left: 7px; }

/* Gone, with something left saying so. A message that simply vanished would
   leave the conversation reading as though it had never been sent, and the
   other person answering something that is no longer there. */
.bubble.del {
  background: transparent; color: var(--faint);
  border: 1px dashed var(--line);
}
.msg.out .bubble.del { background: transparent; color: var(--faint); }
.bubble.del i { font-size: 13px; }

/* ---- Reactions ----------------------------------------------------------- */

/* Two people, so at most two, and usually the same one. Tucked onto the corner
   of the bubble rather than set out in a row underneath, because it belongs to
   the message rather than coming after it. */
.chips {
  width: 100%; display: flex; gap: 4px;
  margin-top: -7px; padding: 0 4px;
}
.msg.out .chips { justify-content: flex-end; }
.msg.in .chips { justify-content: flex-start; }
.chip {
  font-size: 13px; line-height: 1;
  padding: 4px 8px; border-radius: 11px;
  background: var(--bg); border: 1px solid var(--line);
}
.chip.own { border-color: var(--accent); }

/* ---- The sheet ----------------------------------------------------------- */

/* No z-index of its own, so the three layers below sort themselves out in one
   stacking context: the scrim under everything, the held message above it, and
   the card above both. Putting a number on this element would trap the message
   underneath the scrim however high it was lifted. */
.sheet { position: fixed; inset: 0; }
.sheet[hidden] { display: none; }
.sheet-back {
  position: absolute; inset: 0; z-index: 39;
  background: rgba(0, 0, 0, .34);
  opacity: 0; transition: opacity .18s;
}

/* The one being held stays lit. Dimming the whole conversation leaves nothing
   saying which message the sheet is about, and holding the wrong bubble is
   easy — they are close together and a thumb is wide. */
.msg.lifted { position: relative; z-index: 40; pointer-events: none; }
.sheet.up .sheet-back { opacity: 1; }

/* Up from the bottom, where a thumb already is. */
.sheet-card {
  position: absolute; left: 0; right: 0; bottom: 0;
  background: var(--bg);
  border-top: 1px solid var(--line);
  border-radius: 18px 18px 0 0;
  padding: 10px 10px max(12px, env(safe-area-inset-bottom));
  z-index: 41;
  transform: translateY(100%);
  transition: transform .2s ease;
}
.sheet.up .sheet-card { transform: translateY(0); }
@media (prefers-reduced-motion: reduce) {
  .sheet-card, .sheet-back { transition: none; }
}

.quick { display: flex; gap: 4px; padding: 2px 0 8px; }
.quick[hidden] { display: none; }
.quick button {
  flex: 1; height: 46px; padding: 0;
  font-size: 24px; line-height: 1;
  border: 0; border-radius: 12px; background: transparent; cursor: pointer;
}
/* The one already on this message. Pressing it again takes it off, which is
   the only way to change your mind about one. */
.quick button.on { background: var(--track); }

.act {
  display: block; width: 100%; text-align: left;
  padding: 13px 14px; border: 0; border-radius: 10px;
  background: transparent; color: var(--ink); cursor: pointer;
  font: 15px ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif;
}
.act[hidden] { display: none; }
.act:active { background: var(--track); }
.act.gone { color: var(--bad); }

/* ---- What the next message is about -------------------------------------- */

.pending {
  flex: none; display: flex; align-items: center; gap: 9px;
  padding: 7px 10px;
  border-top: 1px solid var(--line);
  background: var(--bg);
}
.pending[hidden] { display: none; }
.pending-bar {
  flex: none; width: 3px; align-self: stretch;
  border-radius: 2px; background: var(--accent);
}
.pending-txt { flex: 1; min-width: 0; }
.pending-txt b { display: block; font-size: 11.5px; color: var(--accent); }
.pending-txt span {
  display: block; font-size: 13px; color: var(--muted);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.pending button {
  flex: none; width: 32px; height: 32px; padding: 0;
  display: flex; align-items: center; justify-content: center;
  border: 0; border-radius: 50%; background: transparent;
  color: var(--muted); cursor: pointer;
}
.pending button .i { width: 18px; height: 18px; }

/* The strip belongs to the message field, and goes when the field does. */
.pending:has(~ .composer.notext) { display: none; }

/* ---- Marks that carry a value -------------------------------------------- */

/* Mid-tone colours, chosen once rather than per bubble. A shade picked to stand
   out on a dark green bubble is a shade that disappears on a light grey one, so
   these sit in the middle and read on both, in both themes. */
.fg-red    { color: #d94f43; }
.fg-orange { color: #d1852c; }
.fg-green  { color: #2f9e63; }
.fg-blue   { color: #3d8dd6; }
.fg-purple { color: #9068cf; }
.fg-pink   { color: #d2589a; }

/* A highlighter puts ink behind the words and the words go dark, which is what
   makes it readable on any bubble underneath. */
.hl { color: #16171a; padding: 0 3px; border-radius: 3px; }
.hl-yellow { background: #f5d762; }
.hl-green  { background: #97e0b0; }
.hl-blue   { background: #a8d4f2; }
.hl-pink   { background: #f7b6d2; }
.hl-orange { background: #f8c79a; }
.hl-purple { background: #d3bdf0; }

.sz-lg { font-size: 1.32em; }
.sz-sm { font-size: .82em; }

/* Where a link goes, when its words do not already say. A link whose text and
   destination disagree is the oldest trick there is, and this is a
   conversation with somebody who can write both. */
.host {
  font-size: .78em; opacity: .72; margin-left: 4px;
  text-decoration: none; white-space: nowrap;
}
.host::before { content: '('; }
.host::after { content: ')'; }

/* ---- The panel that asks which one --------------------------------------- */

.pop {
  flex: none; display: flex; align-items: center; gap: 6px;
  padding: 8px 10px;
  border-top: 1px solid var(--line);
  background: var(--bg);
  overflow-x: auto; scrollbar-width: none;
}
.pop[hidden] { display: none; }
.pop::-webkit-scrollbar { display: none; }

.swatch {
  flex: none; width: 30px; height: 30px; padding: 0;
  border: 1px solid var(--line); border-radius: 50%;
  background: currentColor; cursor: pointer;
}
/* The font-colour swatches show their colour as ink, so the circle is filled
   with it; the highlight ones already carry it as a background. */
.swatch.hl-yellow, .swatch.hl-green, .swatch.hl-blue,
.swatch.hl-pink, .swatch.hl-orange, .swatch.hl-purple { color: inherit; }
/* Taking one off again. A slash, because an empty circle reads as white. */
.swatch.off {
  background: var(--bg); color: var(--muted); position: relative;
}
.swatch.off::after {
  content: ''; position: absolute; left: 4px; right: 4px; top: 13px;
  height: 2px; background: var(--muted); transform: rotate(-45deg);
}

.popsize {
  flex: none; padding: 6px 12px; min-height: 30px;
  border: 1px solid var(--line); border-radius: 15px;
  background: transparent; color: var(--ink); cursor: pointer;
  font: 14px ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif;
}
.popsize:active { background: var(--track); }

.popurl {
  flex: 1; min-width: 0; height: 36px;
  padding: 0 13px; border: 1px solid var(--line); border-radius: 18px;
  background: var(--track); color: var(--ink);
  /* 16px for the same reason as the message box: anything smaller and the
     phone zooms the whole page when this is focused. */
  font: 16px ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif;
}
.popurl:focus { outline: 0; border-color: var(--accent); }
/* An address that is not one. Said on the box rather than in a message, because
   the box is where it can be corrected. */
.popurl.bad { border-color: var(--bad); }
.popgo {
  flex: none; height: 36px; padding: 0 16px;
  border: 0; border-radius: 16px;
  background: var(--accent); color: var(--bg); cursor: pointer;
  font: 600 14px ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif;
}

/* Ten buttons do not fit across a phone, so they keep their size and the bar
   scrolls. Sharing the width between them would leave ten targets too narrow
   to hit and a row of illegible glyphs. */
.fmt button { flex: 0 0 auto; }
.fmt button .ink {
  border-bottom: 2.5px solid #d94f43; padding-bottom: 1px; font-weight: 650;
}
.fmt button.aa { font-size: 14px; letter-spacing: .5px; }

/* ---- Emoji --------------------------------------------------------------- */

/* Inside the field on the right, grey, where every chat app puts it. */
.emoji {
  flex: none; width: 28px; height: 28px; padding: 0; margin-left: 6px;
  display: flex; align-items: center; justify-content: center;
  border: 0; background: transparent; color: var(--faint); cursor: pointer;
}
.emoji .i { width: 22px; height: 22px; }
.field:focus-within .emoji { color: var(--muted); }
/* With no message field there is nothing to put one in. */
.composer.notext .emoji { display: none; }

/* A grid rather than a row, and the only panel that scrolls downward. */
.pop.grid {
  flex-wrap: wrap; gap: 2px;
  overflow-x: hidden; overflow-y: auto;
  max-height: 188px;
}
.face {
  flex: none; width: 40px; height: 38px; padding: 0;
  font-size: 22px; line-height: 1;
  border: 0; border-radius: 8px; background: transparent; cursor: pointer;
}
.face:active { background: var(--track); }

/* ---- Being told, while you are looking somewhere else -------------------- */

/* One line above the composer, offered once. It is asking for a permission
   that cannot be asked for twice, so it says what it is for rather than just
   naming the permission. */
.ask {
  flex: none; display: flex; align-items: center; gap: 8px;
  padding: 8px 10px 8px 14px;
  /* Under the header rather than over the composer: it is a notice about the
     conversation, not about the message being typed. */
  border-bottom: 1px solid var(--line);
  background: var(--track);
}
.ask[hidden] { display: none; }
.ask-what {
  flex: 1; min-width: 0; font-size: 12.5px; color: var(--muted);
  line-height: 1.35;
}
.ask-yes {
  flex: none; height: 30px; padding: 0 13px;
  border: 0; border-radius: 15px;
  background: var(--accent); color: var(--bg); cursor: pointer;
  font: 600 13px ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif;
}
.ask-no {
  flex: none; width: 28px; height: 28px; padding: 0;
  display: flex; align-items: center; justify-content: center;
  border: 0; border-radius: 50%; background: transparent;
  color: var(--muted); cursor: pointer;
}
.ask-no .i { width: 16px; height: 16px; }

/* Mid-sentence on the other end. Said in the same place the connection is
   said, because it is the same kind of fact about the same person — and set
   apart only enough to read as happening now rather than as a state. */
.sub2.atit { color: var(--accent); font-style: italic; }

/* ---- what the floating header displaced ---------------------------------- */

/* With the header out of the flow, the banner is the first thing in it, so the
   room the header occupies is now the banner's to reserve. */
.ask { margin-top: var(--top); }

/* And then the conversation follows the banner rather than the header, so it
   must not reserve that room a second time. */
.ask:not([hidden]) ~ .stream { padding-top: 14px; }

/* ---- the way out, as a glyph -------------------------------------------- */

/* A round icon in the corner rather than two words in a box. Most of the time
   it is not there at all, and when it is, the header has a name and a state to
   fit alongside it. */
.over {
  flex: none; margin-left: auto; padding: 0;
  width: 38px; height: 38px;
  display: flex; align-items: center; justify-content: center;
  border-radius: 50%; border: 1px solid var(--line);
  background: transparent; color: var(--muted); cursor: pointer;
}
.over .i { width: 19px; height: 19px; }
.over:active { background: var(--track); color: var(--ink); }
.over[hidden] { display: none; }

/* ---- a line that is true for a moment ----------------------------------- */

/* Typing, or something on its way. Three dots that actually move, because the
   whole point of saying it is that it is happening right now — a still "…"
   reads as a state the conversation is stuck in. */
.dots { display: inline-flex; gap: 3px; margin-left: 3px; vertical-align: 1px; }
.dots i {
  width: 3px; height: 3px; border-radius: 50%;
  background: currentColor;
  animation: adot 1.1s ease-in-out infinite;
}
.dots i:nth-child(2) { animation-delay: .18s; }
.dots i:nth-child(3) { animation-delay: .36s; }

@keyframes adot {
  0%, 60%, 100% { opacity: .25; transform: translateY(0); }
  30%           { opacity: 1;   transform: translateY(-2px); }
}

/* Reduced motion still gets the dots, just not the movement: the words already
   say what is happening. */
@media (prefers-reduced-motion: reduce) {
  .dots i { animation: none; opacity: .55; }
}

/* ---- the way back down --------------------------------------------------- */

/* Over the conversation, just above the composer. Only there when the newest
   message is out of sight, because a button pointing at something already on
   the screen is a button that has to be read before it can be ignored. */
.tobottom {
  position: absolute; right: 14px; bottom: 78px; z-index: 4;
  width: 38px; height: 38px; padding: 0;
  display: flex; align-items: center; justify-content: center;
  border: 1px solid var(--line); border-radius: 50%;
  background: var(--glass);
  -webkit-backdrop-filter: blur(10px); backdrop-filter: blur(10px);
  color: var(--ink); cursor: pointer;
}
.tobottom[hidden] { display: none; }
.tobottom .i { width: 20px; height: 20px; }
.tobottom:active { background: var(--track); }

/* ---- what a recording does not need -------------------------------------- */

/* A recording arrives whole or not at all, and it is one bubble either way.
   A picture is worth watching arrive, because you can see how much of it there
   is; a waveform is drawn complete from the first frame, so a bar creeping
   along underneath it is measuring something already on the screen. */
.bubble.voice .bar2 { display: none; }

/* ---- keeping the scroll cheap ------------------------------------------- */

/* A blur that samples whatever is moving underneath it is the most expensive
   thing on this screen, and it sits over the one part that moves. Its own
   layer means the conversation scrolling does not force it to be recomposited
   against the rest of the page.

   And the list is told that its own painting stays inside it, so a message
   arriving at the bottom cannot invalidate anything above it. */
.bar { will-change: backdrop-filter; transform: translateZ(0); }
.stream { contain: paint; overscroll-behavior: contain; }

/* ---- pulling a message aside to answer it -------------------------------- */

/* The list owns up-and-down; a message owns side-to-side. Declaring that here
   is what lets the pull be followed without a handler ever having to refuse a
   scroll, which is the thing that makes a list feel stuck. */
.msg { touch-action: pan-y; }

.msg.pulling { transition: none; }

/* The arrow that says what letting go will do, behind the bubble on the side
   the bubble is being pulled away from. It fades in with the pull, so a short
   accidental drag shows almost nothing. */
.pull {
  position: absolute; top: 50%; transform: translateY(-50%);
  display: flex; align-items: center; justify-content: center;
  width: 26px; height: 26px; opacity: 0;
  color: var(--accent); pointer-events: none;
}
.msg.in .pull { left: 4px; }
.msg.out .pull { right: 4px; transform: translateY(-50%) rotate(180deg); }
.pull .i { width: 18px; height: 18px; }

/* ---- the quote reads as a quote ------------------------------------------ */

/* It was one flat 78% together, which left the label — the part that says what
   the block is — as the faintest thing in the bubble. The label and the rule
   carry the colour; the excerpt stays quiet, because it is a reminder of
   something already said and not the message you are here to read. */
.quote { opacity: 1; }
.quote b { color: var(--accent); letter-spacing: .1px; }
.quote span { opacity: .72; }
.msg.in .quote { border-left-color: var(--accent); }

/* On your own bubble the accent is the bubble, so it would disappear into it.
   There the label goes to full strength against the dimmed excerpt instead. */
.msg.out .quote b { color: inherit; opacity: .95; }
.msg.out .quote { border-left-color: currentColor; }

/* The pull arrow is placed against the row, so the row has to be what it is
   placed against. Without this it escapes to the conversation box and lands at
   the top of the screen. */
.msg { position: relative; }

/* ---- p.css ----------------------------------------------------- */
/* Phone to phone, laid out as a conversation.
   A transfer is a thing that happened between two people, in order, with a
   direction — which is exactly what a message list is for. A stack of cards
   made it look like configuration. */

body { padding: 0; }

.pad { max-width: 540px; margin: 0 auto; padding: 24px 18px 40px; }

button {
  font: 600 15px ui-sans-serif, system-ui, sans-serif;
  border: 1px solid var(--line); background: transparent; color: var(--ink);
  border-radius: 12px; padding: 14px 18px; cursor: pointer;
}
button.wide {
  display: flex; align-items: center; justify-content: center; gap: 9px;
  width: 100%; margin-bottom: 10px;
  background: var(--accent); border-color: transparent; color: var(--bg);
}
button.ghost { background: transparent; color: var(--ink); border-color: var(--line); }
button:disabled { opacity: .45; cursor: default; }
.i { width: 20px; height: 20px; flex: none; }

.u {
  display: block; margin-top: 14px;
  font: 11px/1.6 ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  background: var(--track); color: var(--muted);
  padding: 11px 13px; border-radius: 10px; word-break: break-all;
}

.card.bad { border-color: var(--bad); }
#err {
  font: 12px/1.7 ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  white-space: pre-wrap; word-break: break-word; color: var(--bad); margin: 0;
}

/* ---- The pairing code ----------------------------------------------------- */

/* Its own white card, always, in both themes. A code is read by contrast, and
   this is one of the few places where following the theme is the worse answer. */
.qrbox {
  background: #fff; border-radius: 18px;
  padding: 14px; margin: 4px auto 14px;
  max-width: 300px;
  font-size: 13px; color: #555; text-align: center;
}
.qr { display: block; width: 100%; height: auto; }

.hint.mid { text-align: center; margin-bottom: 22px; }

/* ---- The invitation screen ----------------------------------------------- */

.field-row { margin-bottom: 22px; }
.field-row[hidden] { display: none; }

.lbl {
  display: block; margin-bottom: 9px;
  font-size: 12px; font-weight: 600; letter-spacing: .01em;
  color: var(--muted);
}

/* Full width. It used to sit in a flex row beside a shuffle button, where
   flex:1 filled the space; with the button gone that rule does nothing and the
   box shrank to whatever the browser felt like. */
.txt {
  width: 100%; box-sizing: border-box;
  font: 600 16px ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif;
  color: var(--ink); background: var(--track);
  border: 1px solid transparent; border-radius: 12px;
  padding: 13px 15px;
}
.txt:focus { outline: 0; border-color: var(--accent); }

/* One row of choices where only one can be true. Three short words beat a
   dropdown that hides two of them behind a tap. */
.seg { display: flex; gap: 6px; }
.seg button {
  flex: 1; padding: 11px 6px; font-size: 13px; font-weight: 600;
  background: var(--track); border-color: transparent; color: var(--muted);
  border-radius: 11px;
}
.seg button.on { background: var(--accent); color: var(--bg); }

.hint { margin: 9px 2px 0; font-size: 12px; line-height: 1.5; color: var(--muted); }

/* The record of what happened. Folded away, because it is for the times it is
   needed and those should be rare. */
.trail { margin-top: 16px; }
.trail summary {
  cursor: pointer; padding: 8px 0;
  font-size: 13px; font-weight: 600; color: var(--muted);
}
.trail pre {
  margin: 6px 0 10px; max-height: 40vh; overflow: auto;
  font: 11px/1.6 ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  white-space: pre-wrap; word-break: break-word; color: var(--muted);
  background: var(--track); padding: 10px 12px; border-radius: 10px;
}
.trail button { width: 100%; }

/* Present for Copy and for anyone who wants to select it, out of the way of
   everyone else. A raw address on a screen is something to squint at. */
.u.away {
  position: absolute; width: 1px; height: 1px;
  padding: 0; margin: -1px; overflow: hidden;
  clip-path: inset(50%); white-space: nowrap;
}

/* A moment that is usually a blink and sometimes is not. Quiet rather than
   busy: a spinner for something that takes 200ms is its own kind of noise. */
.mid-screen {
  min-height: 60vh;
  display: flex; align-items: center; justify-content: center;
}
.waiting-line {
  margin: 0; font-size: 15px; color: var(--muted);
  animation: breathe 1.6s ease-in-out infinite;
}
@keyframes breathe { 0%, 100% { opacity: .45; } 50% { opacity: 1; } }
@media (prefers-reduced-motion: reduce) { .waiting-line { animation: none; } }

/* ---- The first screen ---------------------------------------------------- */

/* The footer sits at the bottom of the screen rather than wherever the content
   above it happens to stop. On a screen with this little on it, a row of links
   floating in the upper third reads as a page that failed to finish loading. */
#start {
  display: flex; flex-direction: column;
  min-height: calc(100dvh - 72px);
}
#start .foot { margin-top: auto; padding-top: 34px; }

/* The hint under a field, when what is in the field is the problem. */
.hint.wrong { color: var(--bad); }
