* { box-sizing: border-box; }

[hidden] { display: none !important; }

:root {
  --bg: #0e0e10;
  --bg-alt: #18181b;
  --text: #efeff1;
  --muted: #848494;
  --border: #2a2a2e;
  --accent: #9147ff;
  --up: #3fb950;
  --down: #f85149;
  --headerh: calc(40px + env(safe-area-inset-top)); /* until measureHeader (app.js) sets it */
}

/* Chromium on Windows draws an opaque slab of a scrollbar; app.js stamps
   .win and every scroller gets the Chatterino look instead — a thin
   translucent thumb, no track, firming up under the pointer. Fenced to
   Windows: once styled, a bar can't fade on idle or float over content,
   so mac/iOS keep their native overlay bars. */
.win ::-webkit-scrollbar { width: 8px; height: 8px; }
.win ::-webkit-scrollbar-thumb {
  background: rgba(132, 132, 148, 0.25);
  border-radius: 4px;
}
.win :hover::-webkit-scrollbar-thumb { background: rgba(132, 132, 148, 0.45); }
.win ::-webkit-scrollbar-thumb:hover,
.win ::-webkit-scrollbar-thumb:active { background: rgba(132, 132, 148, 0.7); }
.win ::-webkit-scrollbar-track,
.win ::-webkit-scrollbar-corner { background: transparent; }

/* stroke icons replace UI emoji (idea #28); sized by context, inline by default */
.icon {
  width: 16px;
  height: 16px;
  flex: none;
  display: inline-block;
  vertical-align: -3px;
}

/* THE button family — one look, styled by state, never by purpose:
   .abtn          the quiet workhorse (row actions, dormant toggles)
   .abtn.primary  the one action on a surface that changes something
   .abtn.on       an engaged chip or toggle
   the accent is earned, not default — a toolbar is mostly quiet */
.abtn {
  display: inline-block; /* the <a> members of the family */
  background: var(--bg);
  border: 1px solid var(--border);
  color: var(--muted);
  padding: 4px 10px;
  font: inherit;
  font-size: calc(13 * var(--px));
  text-align: center;
  text-decoration: none;
  cursor: pointer;
}
.abtn:hover { color: var(--text); border-color: var(--muted); }
.abtn.primary { border-color: var(--accent); color: var(--accent); }
.abtn.primary:hover { background: color-mix(in srgb, var(--accent) 18%, transparent); }
.abtn.on {
  border-color: var(--accent);
  color: var(--accent);
  background: color-mix(in srgb, var(--accent) 12%, transparent);
}
.abtn.on:hover { background: color-mix(in srgb, var(--accent) 22%, transparent); }
.abtn:disabled { opacity: 0.6; cursor: default; }
/* a chip set shares one border; members divide inside it, nobody boxes */
/* the field look: every dropdown (card topic + status, upload target, the
   font pick) and every boxed write-in (the homes stamp .field); the sizes
   are per home */
select, .field {
  background: var(--bg);
  border: 1px solid var(--border);
  color: var(--text);
  font: inherit;
}
.field { font-size: calc(13 * var(--px)); }
.field:focus { outline: none; border-color: var(--accent); }
select { cursor: pointer; }
.seg { display: inline-flex; border: 1px solid var(--border); }
.seg .abtn { border: none; background: none; }
.seg .abtn + .abtn { border-left: 1px solid var(--border); }
.seg .abtn.on { background: color-mix(in srgb, var(--accent) 14%, transparent); }

/* The bare button: everything the app draws as plain text or an icon
   rather than as a chip — markup and the builders stamp the class. Spelled
   HERE, above every member, so each rule below still owns whatever it
   says next (the attach row's own top border, the current account's
   default cursor). */
.bare {
  background: none;
  border: none;
  font: inherit;
  cursor: pointer;
}

.sep { border-top: 1px solid var(--border); }

/* The one-line label that clips what doesn't fit (stamped like .bare);
   .grow also takes the slack in its strip — what the filter is filtering,
   the file about to go up, the message being replied to. */
.clip {
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.clip.grow { flex: 1; }

/* the font pick (chat card): app.js sets --font on body, absent = the
   system stack. The two shipped faces are OFL (licenses in fonts/), regular +
   bold only — the one italic in the app is a muted note, synthesized is
   fine; a browser fetches a face only once the pick names it */
@font-face {
  font-family: OpenDyslexic;
  src: url(fonts/OpenDyslexic-Regular.woff2) format("woff2");
  font-display: swap;
}
@font-face {
  font-family: OpenDyslexic;
  font-weight: bold;
  src: url(fonts/OpenDyslexic-Bold.woff2) format("woff2");
  font-display: swap;
}
@font-face {
  font-family: "Atkinson Hyperlegible";
  src: url(fonts/AtkinsonHyperlegible-Regular.woff2) format("woff2");
  font-display: swap;
}
@font-face {
  font-family: "Atkinson Hyperlegible";
  font-weight: bold;
  src: url(fonts/AtkinsonHyperlegible-Bold.woff2) format("woff2");
  font-display: swap;
}

html, body { margin: 0; }
/* the page never scrolls sideways: a bar that overran the window once
   scrolled the whole document by the overrun and cut every row at the
   left (#185) — an item past the edge is off-screen, never a shift */
html { overflow-x: clip; }

/* the page is a grid (#82): the app's own stack in the first column —
   the bars, the pinned banner, the stream, the chat, the composer, each
   placed by row so a folded one leaves its row empty — and the split panes
   (#splits, app.js) in a second column running the full height beside
   it, header to footer. The floating surfaces are fixed and take no cell */
body {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  grid-template-rows: auto auto auto auto minmax(0, 1fr) auto;
  height: 100dvh;
  background: var(--bg);
  color: var(--text);
  --lh: 1.45;
  font: 14px/var(--lh) var(--font, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif);
  /* --px: the scaled pixel. The font-size pick (--chatfs, set on body by
     app.js) is the whole page's, like the face pick — marce, on an iPad:
     the chat grew, the box she typed into and the menus didn't. Every
     text size below is written calc(N * var(--px)): N is the px it draws
     at the default 14, and a 21px pick draws it at N × 1.5; the cards'
     widths ride it too, so their lines wrap where they did. Icons,
     avatars, switches and tap targets stay px */
  --px: calc(var(--chatfs, 14px) / 14);
  font-size: calc(14 * var(--px));
  overscroll-behavior: none;
}
body > * { grid-column: 1; }
header:not(#row2) { grid-row: 1; }
#modpin { grid-row: 3; }
#stream { grid-row: 4; }
#chat, #peek { grid-row: 5; }
#composer { grid-row: 6; }
/* the stream (app.js watchStream, chatsen's shape): twitch's player in
   the row above the chat, the page's width at 16:9 but never past 45% of
   the window — the chat keeps the larger half — and a capped player
   centers on the page's ground; the strip naming the channel runs the
   full width above it, its label a caption rather than a member to press */
#stream { display: flex; flex-direction: column; align-items: center; }
#stream .strip { align-self: stretch; background: var(--bg-alt); border-bottom: 1px solid var(--border); }
#stream .strip .clip:hover { background: none; }
#stream iframe {
  border: none;
  width: min(100%, calc(45dvh * 16 / 9));
  aspect-ratio: 16 / 9;
}
/* each pane is the page again in an iframe, a divider leading it: the
   drag handle (col-resize), and while one is dragged the iframes let the
   pointer through, or the frame under it would eat the move */
#splits {
  grid-column: 2;
  grid-row: 1 / -1;
  display: flex;
}
.split { display: flex; flex: none; }
.split iframe { border: none; }
.splitbar {
  width: 4px;
  flex: none;
  cursor: col-resize;
  background: var(--border);
}
.splitbar:hover, body.splitting .splitbar { background: var(--muted); }
body.splitting iframe { pointer-events: none; }
/* the column under the page instead (#167, body.stack — a portrait phone,
   or a drop on the chat's bottom zone): the page's rows above, the panes'
   row below them, each pane the page's width with its divider above it */
body.stack { grid-template-columns: minmax(0, 1fr); grid-template-rows: auto auto auto auto minmax(0, 1fr) auto auto; }
body.stack #splits { grid-column: 1; grid-row: 7; flex-direction: column; }
body.stack .split { flex-direction: column; }
body.stack .splitbar { width: auto; height: 4px; cursor: row-resize; }
/* the drop zone under a dragged tab or pane title (app.js dropZoneAt): a
   faint wash of the accent over the chat's third it would land on */
#splitzone { position: fixed; background: var(--accent); opacity: .2; pointer-events: none; z-index: 30; }
/* a pane's own close, after the dot */
header #panex { padding: 0 0 0 4px; }

header {
  position: relative; /* anchors the #filterbar overlay */
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 8px 12px;
  padding-top: calc(8px + env(safe-area-inset-top));
  background: var(--bg-alt);
  border-bottom: 1px solid var(--border);
  font-weight: 600;
  flex: none;
}

/* natural width, not the flex spacer — left-side navbar buttons sit right
   beside it, and #navspacer holds the middle open (#65) */
header #title {
  color: var(--text);
  font-weight: 600;
  text-align: left;
  padding: 0;
  max-width: 45vw;
}
header #navspacer { flex: 1; }

/* the second row (settings → layout → arrange): a bar like the navbar,
   same button language, no safe-area pad (the navbar above took it);
   folded while nothing of its own shows (the logic-hidden attribute and
   the user's eye both count) */
#row2 { grid-row: 2; padding-top: 8px; }
#row2:not(:has(> :not([hidden]):not(.userhid))) { display: none; }

/* the tab strip (settings → layout → channels: tabs): the picker's list
   laid flat, seated in a bar like any other button — a flex item that
   shrinks to a scroller when the row is tight. Real tabs, not chips: the
   bar's padding is not the tabs' — the strip stretches through it to the
   bar's top edge (the safe-area inset stays above) and one past its
   bottom rule, so the open tab, which takes the chat's own background,
   opens into the pane; leading the bar (.edge, applyHeaderBtns) it runs
   to the window edge too, else the first tab grows a left border;
   trailing it (.tail) it runs to the right one, so a strip on a row of
   its own has the whole bar to wrap in (wholesome: the + wrapped with
   space still beside it — the bar's padding, which no tab could use).
   No rule past the last tab: the bar's own continues. A tab holding unseen
   messages speaks up in a quieter accent, bold when one of them is a
   mention */
#chantabs {
  min-width: 0;
  /* the strip takes the squeeze before any button does: flex shares
     shrink by basis × factor, and the strip's basis (every tab in a
     line) is already the biggest — the factor makes its share all of it */
  flex: 0 1000 auto;
  align-self: stretch;
  margin: -8px 0 -9px;
  display: flex;
  overflow-x: auto;
  font-weight: 400;
}
/* the strips that scroll (wheel, swipe) but draw no bar */
#chantabs, #suggestions, .pn-tabs, #input { scrollbar-width: none; }
#chantabs::-webkit-scrollbar, #suggestions::-webkit-scrollbar, .pn-tabs::-webkit-scrollbar, #input::-webkit-scrollbar { display: none; }
#chantabs.edge { margin-left: -12px; }
#chantabs.tail { margin-right: -12px; }
/* the Mac shell's bar (html.lights, app.js): the native traffic lights
   sit in the bar's left end, so it starts past them — and a strip
   leading it can't run to the edge anymore, so it keeps its left rule */
.lights header:not(#row2) { padding-left: 80px; }
.lights header:not(#row2) > #chantabs.edge { margin-left: 0; }
.lights header:not(#row2) > #chantabs.edge .abtn:first-child { border-left: 1px solid var(--border); }
/* a tab's own color (--tabc, paintTab) is worn while it holds unread and
   while it's the lit one; read and unlit it's muted like the rest (Sarah,
   09-01: 'tab color while unread or selected'). Without one the family's
   colors stand: muted, the accent mix unread, the accent lit */
#chantabs .abtn {
  border: none;
  border-right: 1px solid var(--border);
  border-bottom: 1px solid var(--border);
  background: none;
  font-size: var(--tabfs, calc(13 * var(--px))); /* the tabs' own pick (#191), or the family's */
  padding: 6px var(--tabpad, 14px);
  white-space: nowrap;
  color: var(--muted);
}
#chantabs:not(.edge) .abtn:first-child { border-left: 1px solid var(--border); }
#chantabs .abtn:hover { border-color: var(--border); }
#chantabs .abtn.on {
  background: var(--bg);
  border-bottom-color: var(--bg);
  color: var(--tabc, var(--accent));
}
#chantabs .abtn.unread { color: var(--tabc, color-mix(in srgb, var(--accent) 60%, var(--muted))); }
#chantabs .abtn.unread.mention { font-weight: 700; }
#chantabs .tabadd { padding: 6px 10px; }
#chantabs .tabadd.open { color: var(--accent); }
/* wrap (settings → layout → tab overflow): rows instead of a
   sideways scroller; the bar grows with them, and its buttons keep to
   the first row (chatterino's title bar) instead of floating mid-bar */
#chantabs.wrap { flex-wrap: wrap; }
header:has(> #chantabs.wrap:not([hidden])) > :not(#chantabs) { align-self: flex-start; }
/* past fitting beside the buttons, or a bar too tight for the buttons
   alone (fitHeader): the strip takes the row under them, edge to edge —
   the bar wraps, the strip goes last (its rows in wrap flow, a scroller
   in scroll); its top pull cancels the bar's row gap, so it sits right
   under */
header:has(> #chantabs.full) { flex-wrap: wrap; }
#chantabs.full {
  order: 99;
  flex-basis: calc(100% + 24px); /* 100% is the bar's content; the margins reach its edges */
  margin-left: -12px;
  margin-right: -12px;
}
#chantabs.full .abtn:first-child,
.lights header:not(#row2) > #chantabs.full .abtn:first-child { border-left: none; }
.lights header:not(#row2) > #chantabs.full {
  margin-left: -80px;
  flex-basis: calc(100% + 92px);
}
/* a tab mid-drag (dragTab), riding the pointer */
#chantabs .abtn.dragging { opacity: 0.85; background: var(--bg); }
/* the tab's own ✕ — the picker's, moved onto the thing it removes */
#chantabs .tabx { color: var(--muted); margin-left: 8px; }
#chantabs .tabx:hover { color: var(--text); }
/* live on twitch: a red dot leads the name (tabs and the picker rows) */
#chantabs .abtn.live::before,
.pickrow.live .pickgo::before {
  content: '';
  display: inline-block;
  width: 6px;
  height: 6px;
  border-radius: 50%;
  background: #eb0400; /* twitch's live red, both themes */
  margin-right: 6px;
  vertical-align: 1px;
}
/* the picker's grip (signed in): the row's left partition, same chip as
   the arrange list's */
.chanlist .grab { align-self: center; margin-left: 6px; }

/* two partitions, one hover language: the name and the ✕ each light up
   their own whole block; the ✕'s stretches full-height and gets some
   width so it's a real target, not a sliver */
.pickrow { display: flex; }
.pickrow .pickgo {
  flex: 1;
  text-align: left;
  color: var(--text);
  padding: 8px 10px;
}
.pickrow .pickgo:hover { background: var(--bg); }
.pickrow.current .pickgo { color: var(--accent); font-weight: 700; }
a.pickgo { text-decoration: none; }
/* the quiet ✕ that removes a row — same chip wherever it appears */
.pickx {
  color: var(--muted);
  padding: 8px 12px;
}
.pickrow .pickx:hover { background: var(--bg); color: var(--text); }
.pickrow .pickx.on { color: var(--accent); } /* a bell that rings (#146) */

/* a text input with a go button (addRow): the add-channel row, the
   highlight-phrase adder, the custom timeout. Flush like .pickrow — the
   input and its button partition the row edge to edge, no inset boxes */
.addrow {
  display: flex;
  border-top: 1px solid var(--border);
}
.addrow input {
  flex: 1;
  min-width: 0;
  background: none;
  border: none;
  color: var(--text);
  padding: 8px 10px;
  font: inherit;
  font-size: calc(13 * var(--px));
}
.addrow input:focus { outline: none; }
.addrow .abtn {
  border: none;
  border-left: 1px solid var(--border);
  background: none;
}

.channote {
  color: var(--muted);
  font-size: calc(12 * var(--px));
  padding: 8px 10px;
  border-top: 1px solid var(--border);
}

/* the accent block button (stamped): the composer's sign-in strip */
.cta {
  display: block;
  padding: 12px 16px;
  border: none;
  background: var(--accent);
  color: #fff;
  font: inherit;
  font-weight: 600;
  text-align: center;
  text-decoration: none;
  cursor: pointer;
}

/* the navbar's icon buttons, named by what they are: the header's own
   children that speak through an icon (the title and the account button
   carry text, and #filterclear lives down in the filter strip — the child
   combinator is what keeps it out; the Windows trio sits in its own box
   and is named alongside) */
header > button[aria-label],
#winbtns > button {
  padding: 0 2px;
  color: var(--muted);
  line-height: 1;
}
header > button[aria-label]:hover,
#winbtns > button:hover { color: var(--text); }
/* lit while their card is open, no matter what opened it — the bell while
   you're on the mentions page it navigates to, search while its strip is up */
header > button[aria-label].open,
header #acctbtn.open,
header #title.open { color: var(--accent); }
/* the bell with a match you haven't looked at (#91): the tabs' unread mix,
   not the full accent — that says "open", this says "something's there" */
header > button[aria-label].unread { color: color-mix(in srgb, var(--accent) 60%, var(--muted)); }

/* the frameless Windows shell's three (app.js), one box: the bar's own
   icon language, a little apart from the app's buttons, close reddening
   the way the OS's does. The auto margin is nothing beside #navspacer
   (growth takes the room first) and the line's end once the bar wraps */
header #winbtns { display: flex; gap: 8px; padding-left: 8px; margin-left: auto; }
header #winclose:hover { color: #c42b1c; }
/* its top edge (app.js, 0.2.5+): a 5px strip over everything that
   resizes, the corners too; gone while maximized. And Windows 10's
   missing top line: DWM frames a frameless window on three sides only */
#resizetop {
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  height: 5px;
  z-index: 1000;
  cursor: ns-resize;
}
#resizetop::before,
#resizetop::after {
  content: '';
  position: absolute;
  top: 0;
  bottom: 0;
  width: 12px;
}
#resizetop::before { left: 0; cursor: nwse-resize; }
#resizetop::after { right: 0; cursor: nesw-resize; }
.maxed #resizetop { display: none; }
.win10:not(.maxed) header:not(#row2) { border-top: 1px solid var(--border); }
/* a bar too tight for its buttons (.tight, fitHeader — measured, since
   how many show is the user's pick): the icons can't shrink, so past the
   width they fit one line the bar wraps instead of running off the edge
   with the close button (#winbtns takes the end of its line). The title
   and account name give way first (.clip, no floor) */
header.tight { flex-wrap: wrap; }

header #acctbtn {
  padding: 0;
  color: var(--muted);
  font-size: calc(12 * var(--px));
  font-weight: 400;
  max-width: 40vw;
}
header #acctbtn:hover { color: var(--text); }

/* .pickrow, but the current account isn't clickable */
.acctrow.current .pickgo { cursor: default; }
.acctrow.current .pickgo:hover { background: none; }
.acctlink {
  display: block;
  width: 100%;
  padding: 8px 10px;
  color: var(--muted);
  font-size: calc(12 * var(--px));
  text-align: left;
  text-decoration: none;
}
.acctlink:hover { color: var(--text); }

/* THE card — the one chrome every menu card wears (index.html stamps the
   class; ids below keep only identity and per-card geometry):
   .card              fixed under the header, seam border, shadow, scroll cap
   .card.flush        its rows run edge to edge (no shared padding)
   .card.fromcomposer stands on the composer instead: seam and shadow flip,
                      and the card's own geometry rule (or placePanel)
                      supplies the vertical */
.card {
  position: fixed;
  top: var(--hang, var(--headerh)); /* its button's row (placePanel), else the whole header */
  right: 8px;
  z-index: 30;
  min-width: calc(220 * var(--px));
  max-width: 85vw;
  /* cap at the space actually under the header, not a guessed fraction —
     a fixed 60vh left cards scrolling with a third of the screen to spare
     whenever their content crept just past it */
  max-height: calc(100dvh - var(--hang, var(--headerh)) - env(safe-area-inset-bottom, 0px) - 16px);
  overflow-y: auto;
  background: var(--bg-alt);
  border: 1px solid var(--border);
  /* the header's bottom border is the card's top edge — its own top border
     doubled the line, and a plain centered shadow smudged the header above.
     This one starts exactly at the card's top: offset + spread swallow the
     upward blur, so the seam stays one clean shared line */
  border-top: none;
  padding: 4px;
  box-shadow: 0 10px 16px -6px rgba(0, 0, 0, 0.6);
}
.card.flush { padding: 0; }
.card.fromcomposer {
  border-top: 1px solid var(--border);
  border-bottom: none;
  box-shadow: 0 -10px 16px -6px rgba(0, 0, 0, 0.6);
}
#chanmenu { min-width: calc(230 * var(--px)); }
#devmenu { min-width: calc(160 * var(--px)); }
#acctmenu { min-width: calc(180 * var(--px)); }
/* the picker window (#155, app.js inPicker): the page again with the
   drawer alone on screen, filling the window — the toast still lands.
   Its bar starts past the Mac shell's traffic lights like the navbar */
.pickerwin body > :not(#composer):not(#toast), .pickerwin #composer > :not(#picker) { display: none; }
.pickerwin #composer { grid-row: 1 / -1; border-top: none; }
.pickerwin #picker { max-height: none; height: 100dvh; border-bottom: none; }
.pickerwin.lights .picker-nav { padding-left: 80px; }
.pn-chan { flex: none; max-width: 40%; font-weight: 700; padding: 0 4px; }
.pn-pop { flex: none; padding: 2px; color: var(--muted); }
.pn-pop:hover { color: var(--text); }

/* the "filter #channel" button closing the highlights section */
.hlfoot {
  padding: 4px 6px 8px;
}
.hlfoot .abtn {
  display: block;
  width: 100%;
  padding: 8px 12px;
}

/* the my-uploads card (.fromcomposer: opened from the attach menu, it
   stands on the composer; placePanel sets the vertical): two budget
   meters over a thumbnail grid */
#upmenu { width: calc(300 * var(--px)); }
.upmeter { padding: 4px 6px 6px; }
.upmeter.uphead { cursor: pointer; }
.upmeter.uphead:hover { background: var(--bg); }
.upbar {
  height: 4px;
  margin-top: 4px;
  background: var(--bg);
  border: 1px solid var(--border);
}
.upfill { height: 100%; background: var(--accent); }
.upfill.hot { background: #e91916; }
.upgrid {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 6px;
  padding: 8px 6px 6px;
}
.upitem { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
/* the thumb: a bordered box the picture fills — the card's grid tiles
   and the attach menu's recents (upThumb's callers stamp it) */
.thumb {
  position: relative;
  background: var(--bg);
  border: 1px solid var(--border);
  overflow: hidden;
}
.thumb img { width: 100%; height: 100%; object-fit: cover; display: block; }
.upthumb { display: block; aspect-ratio: 1; }
.upaud { display: grid; place-items: center; height: 100%; color: var(--muted); }
.upaud .icon { width: 40%; height: 40%; }
.upplay {
  position: absolute;
  inset: 0;
  display: grid;
  place-items: center;
  color: #fff;
  font-size: calc(16 * var(--px));
  text-shadow: 0 0 6px rgba(0, 0, 0, 0.8);
  pointer-events: none;
}
.upmeta {
  display: flex;
  align-items: center;
  justify-content: space-between;
  font-size: calc(11 * var(--px));
  color: var(--muted);
}
/* on an upload's meta line it shrinks to fit, and stays flat on hover */
.upmeta .pickx { font-size: calc(11 * var(--px)); padding: 0 2px; }
.upmeta .pickx:hover { color: var(--text); }
/* the thumb now re-sends (#68) — this small ↗ keeps open-in-tab; the auto
   margin walks it over to sit with the ✕ */
.upmeta .upopen {
  margin-left: auto;
  padding: 0 2px;
  color: var(--muted);
  text-decoration: none;
}
.upmeta .upopen:hover { color: var(--text); }
.upnote { padding: 8px 6px; color: var(--muted); font-size: calc(13 * var(--px)); }

/* the settings card — no padding of its own: .card's shared 4px is the
   chrome, same as every other header card */
.setcard {
  display: flex;
  flex-direction: column;
  /* fixed width: shrink-to-fit made the card stretch whenever click-to-
     edit swapped a phrase label for the input (its intrinsic size is
     wider) — steady no matter what a row or a tab swaps in */
  width: calc(330 * var(--px));
  max-width: 100%;
}
/* the card's tabs (chat · highlights · layout · account): a strip across
   the top, the picked one underlined in the text color — the accent stays
   with the buttons below that earn it */
.cardtabs {
  display: flex;
  margin: 0 -4px 4px;
  border-bottom: 1px solid var(--border);
}
.cardtabs button {
  flex: 1;
  border-bottom: 2px solid transparent;
  margin-bottom: -1px;
  padding: 8px 4px;
}
.cardtabs button:hover { color: var(--text); }
.cardtabs button.on { color: var(--text); border-bottom-color: var(--text); }
.setrow {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 8px;
  padding: 8px 6px;
}
/* whole-row labels (the plain switch rows) toggle on click */
label.setrow { cursor: pointer; }
/* the opt-ins' fold (optInRows): one quiet line under the row for all of them */
.setfold { padding: 0 12px 8px; }
.setrow:hover { background: var(--bg); }
/* the label block soaks up the slack, so the controls flanking it (eye on
   the left, swatch/switch on the right) hold the edges */
.settext { flex: 1; }
/* notice labels double as a filter for their type */
.setlink { cursor: pointer; }
.setlink:hover .setlabel { color: var(--accent); }
.setlabel { font-size: calc(13 * var(--px)); }
.setsub { font-size: calc(11 * var(--px)); color: var(--muted); margin-top: 2px; }
.setseg { flex-shrink: 0; }
/* the − value + steppers (font/emote size): the number sits between its
   two buttons wearing the chip dividers itself (the .abtn + .abtn sibling
   rule can't see across the span); fixed width so px and × don't wiggle */
.stepval {
  padding: 3px 0;
  font-size: calc(12 * var(--px));
  width: 48px;
  text-align: center;
  border-left: 1px solid var(--border);
  border-right: 1px solid var(--border);
  font-variant-numeric: tabular-nums;
}
/* the founder-badge picker: art chips at chat's badge height, block so the
   button's line box doesn't add descender slack under the ghost; the
   any-color swatch closes the seg, divided like a chip and as tall */
.founderseg { flex-shrink: 0; }
.founderseg svg { height: 1.15em; display: block; }
.founderseg .hlcolor { width: 24px; height: auto; border: none; border-left: 1px solid var(--border); }
/* the verb rows (download / delete…): target chips sized like the seg
   chips so the whole your-data corner reads as one scale; a long channel
   name wraps them under the label, kept to the right edge, instead of
   overflowing the card. The font dropdown and the founder chips take the
   same size */
.setacts { flex-wrap: wrap; justify-content: flex-end; }
.setseg .abtn, .setacts .abtn, .founderseg .abtn, .setrow select { padding: 3px 8px; font-size: calc(12 * var(--px)); }
.setrow .abtn { flex-shrink: 0; }
/* "enabled ✓" style status text in a settings row */
.seton { color: var(--accent); font-size: calc(12 * var(--px)); flex-shrink: 0; }
/* header-buttons rows (#60): grip to drag, eye to show/hide; user-hidden is
   a class so the hidden attribute stays the logic channel */
.userhid { display: none !important; }
/* The small uppercase section header, wherever one appears (stamped like
   .bare): card blocks, picker sections, board phases, the navbar's side
   markers. Each member adds its own spacing below. */
.head {
  color: var(--muted);
  font-size: calc(11 * var(--px));
  font-weight: 700;
  text-transform: uppercase;
}
/* the arrange list's zone markers: drag a row across one to change zones */
.btnside { font-size: calc(10 * var(--px)); padding: 6px 6px 2px; }
.grab {
  cursor: grab;
  touch-action: none;
  /* no text-selection loupe or callout eating the long-press on iOS */
  -webkit-touch-callout: none;
  -webkit-user-select: none;
  user-select: none;
}
.btnrow.dragging { background: var(--bg); opacity: 0.85; }
.btnrow.dragging .grab { cursor: grabbing; }
.btnrow .settext .icon { margin-right: 6px; }
/* build-info footer: host · server rev · shell version — diagnostics, quiet */
.setfoot {
  padding: 8px 6px 4px;
  font-size: calc(11 * var(--px));
  color: var(--muted);
  text-align: center;
}
/* the first-visit tour's pointers under the navbar buttons (and inside
   the chat card): blocky accent bubbles with a square caret, shown for a
   beat then gone — the #65 navhint recipe, back from c0114fa */
.navhint {
  position: fixed;
  z-index: 40;
  max-width: calc(220 * var(--px));
  padding: 6px 10px;
  background: var(--accent);
  color: #fff;
  font-size: calc(12 * var(--px));
  font-weight: 600;
  opacity: 0;
  transform: translateY(-4px);
  transition: opacity 0.2s, transform 0.2s;
  pointer-events: none;
}
.navhint.show { opacity: 1; transform: none; }
.navhint::before {
  content: '';
  position: absolute;
  top: -4px;
  left: calc(var(--caret, 50%) - 4px);
  width: 8px;
  height: 8px;
  background: var(--accent);
  transform: rotate(45deg);
}

/* toggle switch: the checkbox is hidden, the track+knob draw the state */
.swinput { display: none; }
.swtrack {
  position: relative;
  width: 34px;
  height: 20px;
  border-radius: 999px;
  background: var(--border);
  transition: background 0.15s;
  flex-shrink: 0;
}
.swtrack::after {
  content: '';
  position: absolute;
  top: 2px;
  left: 2px;
  width: 16px;
  height: 16px;
  border-radius: 50%;
  background: var(--text);
  transition: left 0.15s;
}
.swinput:checked + .swtrack { background: var(--accent); }
.swinput:checked + .swtrack::after { left: 16px; }

/* app icon picker (settings row, native shell only): squircle thumbs, the
   worn one outlined — marks follow the shell's word, not the tap */
.iconseg { display: flex; gap: 6px; flex-shrink: 0; }
.iconseg .ipick {
  border: 2px solid transparent;
  padding: 2px;
  border-radius: 10px;
  line-height: 0;
}
.iconseg .ipick.on { border-color: var(--accent); }
.iconseg .ipick svg {
  width: 30px;
  height: 30px;
  border-radius: 22.37%;
}

/* the suggestions & bugs card */
.fbcard {
  display: flex;
  flex-direction: column;
  gap: 8px;
  padding: 6px;
}
/* the form and the tail under it share one width: the card is shrink-to-
   fit, and a tail row's nowrap title is its max-content, so a long one
   (#108) pulled the whole card out to the 85vw cap with the form left at
   its own width; sized like the form, the row's .clip ellipsizes instead */
.fbcard, .fbtail { width: calc(280 * var(--px)); max-width: 100%; }
.fbkind { display: flex; }
.fbkind .abtn { flex: 1; }
/* the form controls a card carries: the write-in fields, and the topic +
   status dropdowns wherever one has them */
.fbcard .field, .fbcard select { padding: 6px 8px; }
.fbcard select { font-size: calc(13 * var(--px)); }
.fbcard textarea { resize: vertical; }
.fbcard .fbnote { color: var(--text); font-size: calc(13 * var(--px)); text-align: center; margin: 0; }
.fbanon {
  display: flex;
  gap: 6px;
  align-items: center;
  color: var(--muted);
  font-size: calc(13 * var(--px));
  cursor: pointer;
}
.fbanon input { accent-color: var(--accent); }
/* the quiet one-liners: the signed-out note, the boards-off signpost */
.fbhint {
  display: block;
  color: var(--muted);
  font-size: calc(12 * var(--px));
  text-align: center;
  text-decoration: none;
  padding: 0 6px 8px;
}
a.fbhint:hover { color: var(--accent); }
/* the read-only tail under the form: card-block rows, quiet */
.fbtail { color: var(--muted); font-size: calc(12 * var(--px)); }

.blockhead { padding: 8px 6px 4px; } /* same left edge as the rows below it */
.blockrow {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 4px 4px 4px 6px;
}
.blockrow .abtn, .reportacts .abtn { padding: 4px 8px; font-size: calc(12 * var(--px)); }
.blocknote {
  color: var(--muted);
  font-size: calc(12 * var(--px));
  padding: 8px 10px;
}

/* the demo door's terms tick (the twitch door gets /terms itself) */
.termsrow {
  display: flex;
  align-items: center;
  gap: 6px;
  padding: 4px 10px 6px;
  font-size: calc(12 * var(--px));
  color: var(--muted);
}
.termsrow a { color: var(--accent); }

/* the admin's reports card: one report per row, its actions under it */
#reports { max-width: min(92vw, 420px); }
.reportrow {
  padding: 6px 6px 8px;
  border-top: 1px solid var(--border);
  font-size: calc(12 * var(--px));
}
.reportwho { color: var(--muted); }
.reportwho b { color: var(--text); font-weight: 600; }
.reporttext { margin-top: 2px; }
.reportacts { display: flex; gap: 6px; margin-top: 6px; }

/* mentions & highlights panel (pref toggles are .setrow switches, shared
   with the settings card). Same left edge and eye→label gap as .setrow,
   so the two row styles read as one list */
.hlrow {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 4px 6px;
}
/* toggled-off phrases stay listed but read as dormant (the remove chip
   and the eye stay bright — they're the ways back out) */
.hlrow.off .hlpat,
.hlrow.off .hlcolor,
.hlrow.off .hlflag:not(.rm):not(.eye) { opacity: 0.4; }
.hlrow .hlpat {
  flex: 1;
  min-width: 0;
  font-size: calc(13 * var(--px));
  cursor: pointer;
}
/* a command of the reader's own: the name at its width, the message the rest */
.hlrow .cmdname { flex: none; font-size: calc(13 * var(--px)); }
.hlrow .cmdname + .hlpat { color: var(--muted); }
.hlrow .hlpat:hover { text-decoration: underline dotted; }
/* the hidden list's entries aren't editable — no edit affordance */
.hlrow .hlpat.plain, .hlrow .hlpat.plain:hover { cursor: default; text-decoration: none; }
.hlrow .hlpat-edit {
  flex: 1;
  min-width: 0;
  background: var(--bg);
  border: 1px solid var(--accent);
  color: var(--text);
  font: inherit;
  font-size: calc(13 * var(--px));
  padding: 1px 4px;
}
.hlrow .hlpat.rx { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: calc(12 * var(--px)); }
/* the compact members of the .abtn family: per-phrase flag chips */
.hlflag {
  flex: none;
  padding: 3px 6px;
  font-size: calc(11 * var(--px));
  line-height: 1.2;
}
.hlflag .icon { width: 12px; height: 12px; vertical-align: -2px; }
.hlcolor {
  flex: none;
  width: 22px;
  height: 22px;
  padding: 0;
  border: 1px solid var(--border);
  background: none;
  cursor: pointer;
}
.hlcolor::-webkit-color-swatch-wrapper { padding: 2px; }
.hlcolor::-webkit-color-swatch { border: none; }

/* THE slab — everything that floats over the chat wears the same raised
   surface, hairline and shadow (index.html and the tip builders stamp the
   class; ids keep identity and per-surface geometry). #msgmenu is the
   rounded exception; the tips take no pointer: the image preview sits
   lower (20), the tip card over everything a hover can reach — a clock or
   an emote in a popover's rows raises it too (38). */
.slab {
  position: fixed;
  z-index: 35;
  background: var(--bg-alt);
  border: 1px solid var(--border);
  box-shadow: 0 4px 16px rgba(0, 0, 0, 0.6);
}
/* the popover opened or pressed last sits over the other; the menus over both */
.slab.front { z-index: 36; }
.slab.strip { z-index: 37; }

/* the strip: a hairline-divided row of buttons — the user card's action
   and mod rows, and the message menu's whole pill. .even is the card-row
   flavor: members partition the row equally under a seam; the menu keeps
   natural widths and its own border. */
.strip { display: flex; }
.strip.even { border-top: 1px solid var(--border); }
.strip.even > * { flex: 1; text-align: center; }
/* members by position, not tag — the card's twitch link sits among buttons */
.strip > * {
  color: var(--text);
  font-size: calc(13 * var(--px));
  padding: 8px 10px;
  text-decoration: none;
}
.strip > *:hover { background: var(--bg); }
.strip > * + * { border-left: 1px solid var(--border); }

/* user card: actions for the name you clicked */
#usercard {
  min-width: calc(220 * var(--px));
  max-width: 94vw;
  cursor: grab; /* any quiet spot drags it aside (mouse; see app.js) */
}
.uc-head {
  display: flex;
  align-items: flex-start;
  gap: 10px;
  padding: 10px 28px 8px 12px; /* the name clears the × */
}
.uc-ava {
  /* channel-less cards: name line + the first meta line */
  width: 36px;
  height: 36px;
  flex: none;
  /* the square shows as a quiet placeholder until the image lands */
  background: var(--bg);
}
/* channel cards: the avatar spans the name line + all four meta lines,
   top flush with the name, bottom flush with the last line (chatterino) */
.uc-head:has(.uc-meta-ch) .uc-ava {
  width: 90px;
  height: 90px;
}
/* the card keeps its reserved height even when a user has fewer lines
   (never subbed, follow unknown) — stability beats compactness here */
.uc-meta-ch { min-height: 72px; }
/* channel cards carry four meta lines — room enough that dates don't wrap */
#usercard:has(.uc-meta-ch) { min-width: calc(320 * var(--px)); }
/* placeholder text lines, swapped for the real ones when the fetch lands;
   each block is exactly one 18px meta line (12px bar centered by padding) */
.uc-skel {
  height: 18px;
  padding: 3px 0;
  background: var(--border);
  background-clip: content-box;
  animation: uc-pulse 1.4s ease-in-out infinite;
}
.uc-skel:nth-child(1) { width: 120px; }
.uc-skel:nth-child(2) { width: 150px; }
.uc-skel:nth-child(3) { width: 135px; }
.uc-skel:nth-child(4) { width: 110px; }
@keyframes uc-pulse {
  50% { opacity: 0.4; }
}
.uc-col { min-width: 0; flex: 1; } /* fill the head, so the id can right-align */
.uc-meta {
  font-size: calc(12 * var(--px));
  color: var(--muted);
  line-height: 18px;
}
/* the name row: the name (and nickname) clip, the pen and the arrow hold
   their spot — a novelty-length login must not push them off the card */
.uc-namerow { display: flex; align-items: center; }
.uc-namerow .uc-pen,
.uc-namerow .uc-ext { flex: none; }
.uc-name {
  font-weight: 700;
  line-height: 18px; /* the avatar's top edge meets the name's */
  min-width: 0; /* let the clip actually give way inside the flex row */
}
/* the mod row under the action strip: same shape, quieter text */
.uc-modrow button { font-size: calc(12 * var(--px)); color: var(--muted); }
.uc-modrow button:hover { color: var(--text); }
/* the top meta line: followers on the left, the id on the right
   (chatterino's corner), quiet and click-to-copy */
.uc-idrow { display: flex; gap: 12px; }
.uc-idrow .uc-id { margin-left: auto; }
/* their pet's line (#188): the sprite at a one-pixel cell beside its name and level */
.uc-pet { display: flex; align-items: center; gap: 6px; }
.uc-petart svg { display: block; }
.uc-id { cursor: pointer; }
.uc-id:hover { color: var(--text); }
/* the square-with-arrow beside the name: their twitch usercard */
/* "report ↗": the arrow after the word marks the link that leaves the site */
.extlink .icon { width: 11px; height: 11px; margin-left: 4px; vertical-align: -1px; }
.uc-ext { padding: 0 2px 0 6px; color: var(--muted); }
.uc-ext .icon { width: 12px; height: 12px; vertical-align: -1px; }
.uc-ext:hover { color: var(--text); }
/* the avatar is a link to their profile; font-size 0 kills the inline gap */
.uc-avalink { flex: none; font-size: 0; }
/* your saved note about them, between the head and the actions */
.uc-note {
  padding: 0 12px 8px;
  font-size: calc(12 * var(--px));
  font-style: italic;
  color: var(--muted);
  max-width: calc(320 * var(--px));
  overflow-wrap: break-word;
}
/* .msgs — a chat-styled scroller of message rows inside a slab: the user
   card's history in this channel, the subset popover's search hits and
   thread. Newest at the bottom, older pages arriving as you scroll up.
   The corner grip resizes it — and through it the slab (the card caps its
   starting height inline, so the grip has no max-height to fight) */
.msgs {
  overflow-y: auto;
  overscroll-behavior: contain;
  cursor: auto;
  resize: both;
}
#usercard .msgs {
  width: min(400px, 92vw);
  /* static floors and ceilings only: a %-of-card bound would feed the
     card's width (which this box itself sets) back into the drag, and the
     grip ran away with it */
  min-width: min(320px, 92vw);
  max-width: 94vw;
}
/* the native grip is near-invisible on a dark theme, and the .win
   scrollbar restyle blanks its corner entirely — paint it ourselves */
.msgs::-webkit-resizer {
  background: linear-gradient(315deg, rgba(132, 132, 148, 0.5) 25%, transparent 25%);
}
.msgs:not(:empty) { border-top: 1px solid var(--border); }
/* the card's × — always there (#135, wholesome: 'the x to close it
   doesn't show until you move the window'; it used to appear only once a
   drag held the card), the subset popover's twin */
.uc-x {
  position: absolute;
  top: 2px;
  right: 2px;
  padding: 2px 8px;
  font-size: calc(14 * var(--px));
  color: var(--muted);
}
.uc-x:hover { background: none; color: var(--text); }

/* the subset popover (#122, #123): a search or a reply thread floats over
   the chat like the user card does, instead of replacing the chat's rows —
   the rows behind keep their place (#121). The head line names the view
   (or holds the search box) and ends in the ×; the .msgs box below takes
   the popover's width and the grip resizes it */
#subview {
  min-width: min(320px, 94vw);
  max-width: 94vw;
  cursor: grab; /* any quiet spot drags it aside (mouse; see app.js) */
}
.sv-head {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 6px 4px 6px 12px;
  color: var(--muted);
  font-size: calc(12 * var(--px));
}
.sv-head button { color: var(--muted); padding: 2px 8px; }
.sv-head button:hover { color: var(--text); }
/* the box fits its rows: a search is the bar alone until hits land, then
   grows down to them (#128, Sarah: 'just the search bar and then expanded
   more downwards for results'); a thread is what it is. With the keyboard
   up the cap is what the keys leave under the header (app.js sets
   --kbinset) — a box that can't fit there had clampPopup sliding the whole
   popover under the status bar, its × out of reach */
/* the box's size is the popover's, as on the card: the grip sets both,
   and the viewport is the only ceiling — it opens fitting its rows, capped
   inline (subCap) */
#subview .msgs {
  width: min(560px, 94vw);
  min-width: min(320px, 94vw);
  max-width: 94vw;
  height: auto;
  max-height: calc(100dvh - var(--headerh) - 48px);
}
body.kbopen #subview .msgs {
  max-height: calc(100dvh - var(--kbinset, 0px) - var(--headerh) - 64px);
}
/* the people popover (#144): the tab strip under the head, a name per row */
#subview .cardtabs { margin: 0; }
.pp-row {
  display: flex;
  align-items: center;
  gap: 8px;
  width: 100%;
  padding: 4px 12px;
  text-align: left;
}
.pp-row:hover { background: var(--bg-alt); }
.pp-name { font-weight: 700; }
.pp-when {
  margin-left: auto;
  color: var(--muted);
  font-size: calc(12 * var(--px));
  white-space: nowrap;
}
.pp-n { color: var(--muted); font-weight: 400; }
#searchinput,
#peoplefilter {
  flex: 1;
  min-width: 0;
  background: none;
  border: none;
  color: var(--text);
  font: inherit;
  font-size: calc(14 * var(--px));
  padding: 2px 0;
}
#searchinput:focus { outline: none; }
#searchinput::placeholder { color: var(--muted); }

/* message menu: right-click / long-press a message — a single row of
   options with hairline dividers, iA-Writer-style (idea #33). The deliberate
   exception to the sharp-corner theme: this one is a rounded pill. */
#msgmenu {
  flex-wrap: nowrap; /* one row: a narrow screen pages behind … (fitMenu) */
  /* its own width wherever it lands: a fixed box with only `left` set
     shrinks to the room left of the viewport edge, so a hold near the
     right edge folded the pill before placePopup ever clamped it (her
     screenshot); max-content measures the whole row first, and the
     92vw cap is the only thing that wraps it now */
  width: max-content;
  max-width: 92vw;
  border-radius: 18px;
  overflow: hidden;
}
#msgmenu > * {
  padding: 8px 12px;
  white-space: nowrap;
}
/* the channel menu (hold a tab or a picker row, #82): the same strip,
   square like the rest */
#tabmenu { width: max-content; }
/* the color face's any-color swatch fills its strip cell (the pet's too) */
#tabmenu .hlcolor, #petmenu .hlcolor { width: 24px; height: auto; padding: 0; border: none; }
#petmenu { width: max-content; }
/* a hold or right-click on a tab or a picker row opens its menu (holdChan),
   never a text selection or the OS callout */
#chantabs, .pickrow {
  -webkit-touch-callout: none;
  -webkit-user-select: none;
  user-select: none;
}

/* the fold-out editor rows (custom timeout, nickname) share one quiet
   shape — addRow callers tag both with .uc-edit */
.uc-edit input {
  font-size: calc(12 * var(--px));
  padding: 6px 10px;
}
.uc-edit .abtn {
  font-size: calc(12 * var(--px));
  color: var(--muted);
  border-left: none; /* quieter than the add rows: no divider before 'go' */
}
.uc-edit .abtn:hover { background: var(--bg); color: var(--text); }

/* the little pen next to the card-head name opens the nickname editor —
   deliberately quiet, it's a per-viewer note, not an account action */
.uc-pen {
  padding: 0 2px 0 8px;
  color: var(--muted);
}
.uc-pen .icon { width: 11px; height: 11px; vertical-align: -1px; }
.uc-pen:hover { background: none; color: var(--text); }
/* your name for them, tucked after the real one in the card head */
.uc-nick {
  color: var(--muted);
  font-weight: 400;
  font-size: calc(12 * var(--px));
}

/* The two strips that overlay the chat rather than sit in its flow — the
   filter bar pinned under the header, the autocomplete over the composer —
   so opening or closing either never shifts the messages. Same slab, same
   inset; each names its own edge below. */
#filterbar,
#suggestions {
  position: absolute;
  left: 0;
  right: 0;
  z-index: 15;
  display: flex;
  padding: 6px 12px;
  background: var(--bg-alt);
}

/* pinned while viewing a single user's messages, a notice bucket, the
   channel's highlights, or a feed page's standing line */
#filterbar {
  /* under the whole header stack: with the tab strip on the second row,
     the first row's edge would seat the bar over the strip and leave the
     strip's height as a gap above the page */
  top: var(--headerh);
  font-weight: 400;
  align-items: center;
  gap: 8px;
  border-bottom: 1px solid var(--border);
  color: var(--muted);
  font-size: calc(12 * var(--px));
}
/* the strips wear the same quiet button */
#filterbar button,
#replybar button,
#uploadcancel {
  color: var(--muted);
  padding: 2px 6px;
}
#filterbar button:hover { color: var(--text); }

#dropzone {
  position: fixed;
  inset: 12px;
  z-index: 40;
  pointer-events: none;
  display: flex;
  align-items: center;
  justify-content: center;
  border: 2px dashed var(--accent);
  background: rgba(14, 14, 16, 0.75);
  color: var(--text);
  font-size: calc(18 * var(--px));
  font-weight: 600;
}

.dot {
  width: 9px;
  height: 9px;
  border-radius: 50%;
  flex: none;
}
.dot.online { background: #00e676; }
.dot.connecting { background: #ffb300; }
.dot.offline { background: #f44336; }
/* reading twitch directly (#134): live, but not through chatter.boo — the
   green as a ring */
.dot.direct { background: transparent; box-shadow: inset 0 0 0 2px #00e676; }
/* the dev site's "connected" wears the accent instead of green — one glance
   says which instance you're on */
.dot.online.dev { background: var(--accent); }
.dot.direct.dev { box-shadow: inset 0 0 0 2px var(--accent); }

#chat {
  flex: 1;
  overflow-y: auto;
  overflow-anchor: none;
  touch-action: pan-y; /* a sideways finger is the pager's (#145), never also a scroll */
  /* --filterpad: height of the #filterbar overlay while it's open (set from
     app.js) so the very top of the history clears it instead of hiding under it */
  padding: calc(6px + var(--filterpad, 0px)) 0 6px;
}

/* the pager (#145, chatsen's feel): a sideways finger drags the chat with
   it and #peek — the next channel's name, in the chat's cell — in from the
   edge; the release draws the hop where the panel was and slides it the
   rest of the way in, or slides both back. Transforms only: nothing
   around the chat moves or re-measures */
#peek {
  display: flex;
  align-items: center;
  padding: 0 24px;
  color: var(--muted);
  font-size: calc(16 * var(--px));
  pointer-events: none;
}
#chat.slide, #peek.slide { transition: transform .18s ease-out; }
@media (prefers-reduced-motion: reduce) { #chat.slide, #peek.slide { transition: none; } }

/* every row is laid out as it lands and painted as the compositor asks:
   no deferred rendering (app.js hold) — the engine's own cost a layout
   per row entering the view and every other frame of a fling through a
   chat of gifs, and an off-screen gif costs nothing on its own.
   Painted as its own cached piece (contain): an append re-records the new
   row, not every row in view — the engine otherwise re-records the whole
   box each time (a long session's 1500 rows: 6.8 → 3.1ms an append on a
   Mac; 1.47 → 1.15 at the 250-row cap). A row's own piece snaps to its
   own pixel, so rows must stand a whole number of pixels tall or hairlines
   open between tinted neighbours: the line-height is rounded (14px × 1.45
   = 20.3 otherwise; the reply quote and the time set their own font, so
   their own — a smaller font under the row's px line reaches lower), the
   art's height and pull are rounded,
   and the art sits on the line — a centered image's edge is a font metric
   no rounding reaches. Nothing a row draws reaches past its box (the
   cards, tips and menus are the body's), so the clip changes no pixel.
   Chrome's win is WebKit's loss (the Mac app, the iPhone): WebKit gives a
   contained row a layer of its own, and an append's layout doubles, 1.2 →
   2.3ms a message (tools/abcheck/wkprofile.js, 2026-09-04) — so the
   containment is every engine's but WebKit's, told apart by a font
   keyword only WebKit knows (the rounding stays everywhere: free on
   WebKit, and the rows one shape) */
@supports not (font: -apple-system-body) { .msg { contain: paint; } }
.msg {
  padding: 3px 12px;
  /* the row's size is the body's (the font-size pick); --emotesc scales
     the art on top of it; timestamps and replies ride along in em */
  overflow-wrap: anywhere;
}
.msg, .msg .reply, .msg .time { line-height: round(calc(var(--lh) * 1em), 1px); }
.msg:hover { background: var(--bg-alt); }
/* braille art (brailleArt): its own block under the name, a run per line */
.msg .text.art { display: block; white-space: pre-wrap; }

/* a long-press opens the message menu or an emote card — keep the OS
   text-selection callout from fighting it (copy lives in the menu) */
@media (pointer: coarse) {
  .msg, .picker-grid {
    -webkit-touch-callout: none;
    -webkit-user-select: none;
    user-select: none;
  }
}

/* mentioned or matched a highlight word; --hl is set per-row from the
   matching phrase's color */
.msg.highlight {
  background: color-mix(in srgb, var(--hl, #ff5c5c) 13%, transparent);
}
.msg.highlight:hover { background: color-mix(in srgb, var(--hl, #ff5c5c) 20%, transparent); }

/* moderator-deleted, live or loaded: struck through but readable (chatterino-style, #143) */
.msg.deleted .text,
.msg.deleted .emote {
  opacity: 0.4;
  text-decoration: line-through;
}

.msg.system .text { color: var(--muted); }

/* the mod-pinned message (#101): a mod's banner between the header and the
   chat, the row it names tinted while on screen */
#modpin {
  display: flex;
  align-items: baseline;
  gap: 6px;
  padding: 4px 12px;
  background: var(--bg-alt);
  border-bottom: 1px solid var(--border);
  cursor: pointer;
}
#modpin .icon { width: 14px; height: 14px; flex: none; align-self: center; color: var(--accent); }
#modpin .who, #modpin .left { color: var(--muted); font-size: 0.85em; white-space: nowrap; }
#modpin .who { max-width: 40%; }
#modpin .line { flex: 1; min-width: 0; overflow-wrap: anywhere; }
.msg.modpinned { background: color-mix(in srgb, var(--accent) 10%, transparent); }
.msg.modpinned:hover { background: color-mix(in srgb, var(--accent) 16%, transparent); }

/* a row twitch captioned — a first message, its own highlight, a
   redemption (tagCaption): tinted like a notice, the caption its .ntext
   line above the message; a plain 'shared' room tag reads like a feed
   row's channel, muted */
.roomtag { margin-right: 6px; white-space: nowrap; }
span.roomtag:not(.feedchan) { color: var(--muted); }
body.hide-labels .caption, body.hide-labels .roomtag { display: none; }
/* a cheer (cheerNode): the cheermote's art, the amount beside it in the
   tier's color */
.cheer .bits { font-weight: 700; margin-right: 2px; }
/* the gigantified-emote power-up: the message's last emote, three tall */
.msg .emote.giant { height: round(calc(5.25em * var(--emotesc, 1) * var(--eh, 1)), 1px); }
/* the animated-message power-up: the words wear the effect twitch names
   in animation-id (dressText); the art and the links inside keep their own
   paint */
.text[data-anim="rainbow-eclipse"], .text[data-anim="cosmic-abyss"] {
  background-size: 200% 100%;
  -webkit-background-clip: text;
  background-clip: text;
  color: transparent;
  animation: anim-slide 4s linear infinite;
}
.text[data-anim="rainbow-eclipse"] {
  background-image: linear-gradient(90deg, #ff6b6b, #ffd93d, #6bcb77, #4d96ff, #c77dff, #ff6b6b);
}
.text[data-anim="cosmic-abyss"] {
  background-image: linear-gradient(90deg, #7b2ff7, #2fd6f7, #f72f8e, #7b2ff7);
}
.text[data-anim="simmer"] {
  color: #ff8a3d;
  animation: anim-simmer 1.6s ease-in-out infinite alternate;
}
@keyframes anim-slide { to { background-position: 200% 0; } }
@keyframes anim-simmer {
  from { text-shadow: 0 0 2px rgba(255, 138, 61, 0.4); }
  to { text-shadow: 0 0 8px rgba(255, 138, 61, 0.9); }
}
@media (prefers-reduced-motion: reduce) { .text[data-anim] { animation: none; } }

/* sub/resub/gift/raid notices, and the rows twitch captioned (.lit):
   tinted like a highlight, twitch's sentence (the caption) as its own
   .ntext line above whatever message the user attached; --ntc is set
   per-row from the type's color in the bell card, a captioned row wears
   the accent */
.msg.notice, .msg.lit {
  background: color-mix(in srgb, var(--ntc, var(--accent)) 9%, transparent);
}
.msg.notice:hover, .msg.lit:hover { background: color-mix(in srgb, var(--ntc, var(--accent)) 14%, transparent); }
.msg .ntext .text {
  color: color-mix(in srgb, var(--ntc, var(--accent)) 55%, var(--text));
  font-weight: 600;
}
/* the sentence and the attached message read as two rows — give them the
   same breathing room two .msg rows get from their padding (3px + 3px) */
.msg .ntext:not(:last-child) { margin-bottom: 6px; }
.reply {
  color: var(--muted);
  font-size: 0.86em; /* 12px at the default 14px */
  margin-bottom: 1px;
}
.reply .icon { width: 12px; height: 12px; vertical-align: -2px; }
/* quotes that can open the thread popover */
.reply[data-thread] { cursor: pointer; }
.reply[data-thread]:hover { color: var(--text); }

.time {
  color: var(--muted);
  font-size: 0.79em; /* 11px at the default 14px */
  margin-right: 6px;
  font-variant-numeric: tabular-nums;
}
/* the settings toggle */
body.hide-time .time { display: none; }
body.hide-badges .badge { display: none; }

/* channel tag on mentions-page rows — reads like a username with a # on it,
   in the channel's color */
.feedchan {
  font-weight: 700;
  margin-right: 6px;
  cursor: pointer;
}

.name { font-weight: 700; cursor: pointer; }
/* the login after a localized display name — "かず (kaz)" — quiet beside it */
.login { color: var(--muted); font-weight: 400; }

/* twitch badges (mod/sub/vip…) before the name; the founders' ghost is an
   inline svg wearing the same class, one thing to the pointer — its parts
   never take the hover themselves */
.badge {
  height: 1.15em;
  aspect-ratio: auto 1 / 1; /* a square box until the art says otherwise */
  vertical-align: -0.18em;
  margin-right: 3px;
}
svg.badge * { pointer-events: none; }

/* "@name" inside a message, colored per-user from chatUsers */
.mention { font-weight: 700; }
.msg .mention { cursor: pointer; }
.msg .mention:hover { text-decoration: underline; }

/* the -0.2em pull keeps rows compact while the art (margin box + 0.2em
   each way) stays inside the row's 3px padding under any font's baseline
   — Segoe UI sat the bottom edge lower than SF and reached it.
   --eh is the art's own shortfall against its set's baseline (twitch
   smilies, fitTwitchArt): the img sets it, every height rule multiplies it */
.emote {
  height: calc(1.75em * var(--eh, 1));
  /* auto, not the img's width attribute: the attribute pair is the ratio
     the box is held open at until the art comes (emoteImg) */
  width: auto;
  vertical-align: middle;
  margin: -0.2em 1px;
}

/* zero-width emotes lay over the emote before them: the stack wrapper takes
   over the .emote line-fitting job. Every layer piles into the same grid
   cell, centered, and the cell sizes to the widest layer — so wide art
   like curtains shows at full aspect (chatterino-style) and the line
   makes room for it instead of it spilling over the name column */
.emote-stack {
  display: inline-grid;
  justify-items: center;
  align-items: center;
  vertical-align: middle;
  margin: -0.2em 1px;
}
.emote-stack .emote {
  grid-area: 1 / 1;
  margin: 0;
}
/* an emoji base (bug #72) blows up to emote height so overlays land on it */
.emote-stack .uemoji {
  grid-area: 1 / 1;
  font-size: 1.6em;
  line-height: 1;
}
/* overlays match the base's height at their natural width */
.emote-stack .zw { height: 1.75em; width: auto; }

/* the emote-size pick scales art in message rows only — the picker and the
   autocomplete sit outside .msg, and the reaction chips' later rule wins.
   Whole pixels and on the line, per the .msg rule */
.msg .emote { height: round(calc(1.75em * var(--emotesc, 1) * var(--eh, 1)), 1px); }
.msg .emote, .msg .emote-stack { vertical-align: bottom; margin: round(-0.2em, 1px) 1px; }
.msg .emote-stack .emote { margin: 0; }
/* a twitch gif (#112): four emotes tall, as wide as the picture and never
   past the row; the chat tab's 'gifs: hide' shows the title twitch sends
   in its place */
.gif, .msg .gif { height: round(calc(7em * var(--emotesc, 1) * var(--eh, 1)), 1px); max-width: 100%; }
/* the picture on a line of its own under the name, as twitch draws it;
   hidden, the title takes the picture's place in the line */
.gifwrap { display: block; margin-top: 4px; }
.giftext, body.hide-gifs .gif { display: none; }
body.hide-gifs .gifwrap, body.hide-gifs .giftext { display: inline; }
/* twemoji art standing in for a glyph the font lacks (#130): the size of
   the character it replaces, wherever that character draws */
.emoji-art {
  height: 1.2em;
  width: 1.2em;
  vertical-align: -0.25em;
}
.msg .uemoji { font-size: calc(1em * var(--emotesc, 1)); }
.msg .emote-stack .uemoji { font-size: calc(1.6em * var(--emotesc, 1)); }
.msg .emote-stack .zw { height: round(calc(1.75em * var(--emotesc, 1)), 1px); }

.msg a { color: var(--accent); }

/* emote reactions: emote-only replies collected into chips on the target */
.reactions {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 4px;
  margin-top: 3px;
}
.reaction {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  padding: 1px 8px;
  border: 1px solid var(--border);
  background: var(--bg-alt);
  font-size: calc(12 * var(--px));
  line-height: 20px;
}
.reaction .emote { height: calc(1.4em * var(--eh, 1)); margin: 0; }
.remoji { font-size: calc(15 * var(--px)); line-height: 1; }
.rcount {
  color: var(--muted);
  font-variant-numeric: tabular-nums;
}

.day-sep {
  text-align: center;
  color: var(--muted);
  font-size: calc(11 * var(--px));
  padding: 8px 0 2px;
}

/* the pet (#80): a pixel ghost under the chat's top edge (placePet sets
   the top — the rim is the chat's, under whatever bars stand above it;
   #186, #184: at the bottom it covered the newest rows — wholesome_: 'i
   wanna read messages'), a layer of its own — the slide across the chat
   and the bob are transforms, so the chat's tile beneath never
   re-rasterizes for it. data-mood sets the bob's pace; the sprite's size
   is dressPet's (the cell by stage, whole device pixels). It sits under
   the #jump pill and every card. The rests — the bob's ends, the
   sleeper's droop — are 4px: a whole device pixel at 1.25x, 1.5x, 1.75x
   and 2x alike, so the art stands crisp between bobs */
#pet {
  position: fixed;
  left: 0;
  top: 0;
  z-index: 15;
  transition: transform 1s linear;
  --petbob: 3s;
}
/* a hold on the pet opens its strip — never the OS text selection under it
   (Sarah, 2026-09-21: 'when I hold press on the pet on mobile it usually
   tries to select stuff'); the piles, the strip and the fight the same */
#pet, #piles, #petmenu, #petbattle {
  -webkit-touch-callout: none;
  -webkit-user-select: none;
  user-select: none;
}
#pet[data-mood="calm"] { --petbob: 2s; }
#pet[data-mood="busy"] { --petbob: 1s; }
#pet[data-mood="wild"] { --petbob: 0.45s; }
#pet[data-mood="hungry"] { --petbob: 5s; }
#pet .sprite {
  display: block;
  padding: 0;
  line-height: 0;
  animation: petbob var(--petbob) ease-in-out infinite;
}
#pet[data-mood="sleep"] .sprite { animation: none; transform: translateY(4px); }
#pet svg { display: block; }
@keyframes petbob { 50% { transform: translateY(-4px); } }
/* a note over its head: a pat's heart floats off once, the sleeper's z
   keeps floating */
#pet .petnote {
  position: absolute;
  left: 50%;
  bottom: 100%;
  font-size: 12px;
  line-height: 1;
  color: var(--petc, #d97757);
  pointer-events: none;
  animation: petfloat 1.2s ease-out forwards;
}
#pet .zz { color: var(--muted); animation: petfloat 2.4s ease-out infinite; }
@keyframes petfloat {
  from { opacity: 1; transform: translate(-50%, 0); }
  to { opacity: 0; transform: translate(-50%, -22px); }
}
/* the snack drops onto the open mouth and is gone */
#pet .petfood {
  position: absolute;
  left: 50%;
  bottom: 40%;
  width: 20px;
  height: 20px;
  pointer-events: none;
  animation: petdrop 1.1s ease-in forwards;
}
#pet .petfood img { width: 100%; height: 100%; }
@keyframes petdrop {
  from { opacity: 1; transform: translate(-50%, -28px); }
  70% { opacity: 1; }
  to { opacity: 0; transform: translate(-50%, 0) scale(0.2); }
}
/* the piles: fixed at the pet's feet (renderPiles sets each one's spot), under the pet; each one a button */
#piles { position: fixed; inset: 0; z-index: 14; pointer-events: none; }
#piles .pile {
  position: absolute;
  padding: 0;
  line-height: 0;
  pointer-events: auto;
}
#piles .pile svg { display: block; }
#piles .pile:disabled { animation: petfloat 0.6s ease-out forwards; }
#piles .sweep { position: static; color: var(--muted); font-size: 14px; animation: none; }
#petmenu .clip { color: var(--muted); }
/* the battle scene (#188): the two sprites face to face over their bars,
   the round's line, the moves; the other pet is mirrored to face ours */
#petbattle {
  display: grid;
  grid-template-columns: 1fr 1fr;
  width: calc(300 * var(--px));
  max-width: 94vw;
}
#petbattle .pb-side { padding: 30px 10px 6px; text-align: center; } /* headroom: a note floats 22px off the sprite */
#petbattle .pb-art { display: inline-block; position: relative; transition: transform 0.25s ease-out; }
#petbattle .pb-art svg { display: block; }
#petbattle .pb-them .pb-art svg { transform: scaleX(-1); }
#petbattle .pb-you.lunge .pb-art { transform: translateX(18px); }
#petbattle .pb-them.lunge .pb-art { transform: translateX(-18px); }
#petbattle .pb-name { font-size: calc(12 * var(--px)); margin-top: 4px; }
#petbattle .pb-bar { height: 6px; margin: 6px 12px 2px; background: var(--bg); border: 1px solid var(--border); }
#petbattle .pb-bar i { display: block; height: 100%; background: var(--accent); transition: width 0.4s ease-out; }
#petbattle .pb-hp { font-size: calc(11 * var(--px)); color: var(--muted); }
#petbattle .pb-log { grid-column: 1 / -1; text-align: center; padding: 6px 10px 8px; font-size: calc(13 * var(--px)); min-height: 18px; }
#petbattle .pb-moves { grid-column: 1 / -1; }
#petbattle .pb-moves button:disabled { color: var(--muted); }
#petbattle .pb-hint { grid-column: 1 / -1; text-align: center; font-size: calc(11 * var(--px)); color: var(--muted); padding: 6px 10px 8px; }
#petbattle .petnote { position: absolute; left: 50%; bottom: 100%; font-size: 13px; font-weight: 700; line-height: 1; color: var(--text); pointer-events: none; animation: petfloat 1s ease-out forwards; }
.uc-pet .uc-battle { margin-left: auto; padding: 0 6px; color: var(--accent); }
#petmenu .petname { padding: 4px; }
@media (prefers-reduced-motion: reduce) {
  #pet { transition: none; }
  #pet .sprite, #pet .zz { animation: none; }
}

/* an actual pill (the app's corners are square — this floater and the
   board's theme chips are the deliberate exceptions) */
#jump {
  position: fixed;
  left: 50%;
  transform: translateX(-50%);
  bottom: calc(50px + env(safe-area-inset-bottom) + var(--sugpad, 0px));
  z-index: 16;
  background: var(--bg-alt);
  color: var(--muted);
  border: 1px solid var(--border);
  border-radius: 999px;
  padding: 8px 16px;
  font: inherit;
  font-size: calc(12 * var(--px));
  cursor: pointer;
}
#jump:hover { color: var(--text); }

/* chatterino's scrollbar marks (#78): a canvas up the chat's right edge,
   one filled line per highlighted row at its place in the scrollback, in
   the row's --hl; app.js draws it and measures it onto the chat. It takes
   no pointer: the scrollbar sits under it (the thumb, on Windows), and a
   press at a mark is judged by place in app.js */
#hlmarks {
  position: fixed;
  right: 0;
  width: 4px;
  pointer-events: none;
}
.win #hlmarks { right: 2px; } /* centered on the 8px thumb */

/* a message landed behind the scrolled-up view (#90): the tabs' unread mix
   on the words and the rim; the quiet pill only offers the way down */
#jump.unread {
  color: color-mix(in srgb, var(--accent) 60%, var(--muted));
  border-color: color-mix(in srgb, var(--accent) 60%, var(--border));
}
/* the autocomplete overlay covers the pill's usual spot — one offset, both
   keyboard states (app.js sets the class with the strip: a :has() on body
   cost a recalc of body and html for every node inserted on the page) */
body.sugopen { --sugpad: 46px; }

/* chatsen-style seamless strip: no inset boxes, the input and its icon
   buttons partition the bar edge to edge under the top hairline */
#composer {
  position: relative; /* anchors the autocomplete overlay */
  flex: none;
  padding-bottom: env(safe-area-inset-bottom);
  background: var(--bg-alt);
  border-top: 1px solid var(--border);
}
/* the keyboard covers the home-indicator strip — the pad it was for would
   just be a dead gap above the keys (app.js toggles the class) */
body.kbopen #composer { padding-bottom: 0; }
/* the page gives up exactly what the keyboard covers (app.js sets --kbinset).
   100dvh is the right base in both of WKWebView's keyboard modes — unlike
   innerHeight, which one of them shrinks along with the keyboard */
body.kbopen { height: calc(100dvh - var(--kbinset, 0px)); }
/* the ?vvdebug=1 metrics chip (ui-test screenshots read it) */
.vvdebug {
  position: absolute;
  top: calc(50px + env(safe-area-inset-top));
  left: 4px;
  z-index: 99;
  padding: 2px 4px;
  background: rgba(0, 0, 0, 0.75);
  color: #7fff7f;
  font: 11px/1.3 ui-monospace, Menlo, monospace;
  pointer-events: none;
}
/* fixed elements don't follow the shrunken body — the measured --kb says
   how much of the fixed viewport the keyboard actually covers (0 when the
   viewport itself shrank), so the pill rides the keys in either mode */
body.kbopen #jump { bottom: calc(16px + var(--kb, 0px) + var(--sugpad, 0px)); }
/* the picker's 40vh is a fraction of a viewport the keyboard may not have
   shrunk — cap it to the free space so keyboard + picker leave chat alive */
body.kbopen #picker { max-height: calc((100dvh - var(--kbinset, 0px)) / 2); }

#inputrow {
  display: flex;
  /* the input grows upward as you type; keep the buttons on its bottom row */
  align-items: flex-end;
}

/* 30px wide reads as the header's button spacing (8px gap + 2px paddings
   between 18px icons); the row is the header's height (its 8px pads
   around one text line, the composer's hairline standing in for the
   header's), a finger gets the 42px tap target back */
#emotebtn,
#attachbtn {
  flex: none;
  width: 30px;
  height: 36px;
  color: var(--muted);
  line-height: 1;
}
@media (pointer: coarse) { #emotebtn, #attachbtn { height: 42px; } }
/* the first icon holds the header's 12px left edge */
#attachbtn { margin-left: 6px; }
#emotebtn:hover,
#attachbtn:hover { color: var(--text); }
/* lit while their surface stands on the composer (the uploads card
   counts as the attach button's too) — the same .open the header wears */
#emotebtn.open,
#attachbtn.open { color: var(--accent); }
#emotebtn .icon,
#attachbtn .icon { width: 18px; height: 18px; vertical-align: middle; }

/* the attach menu (a .card.fromcomposer, standing on the composer's top
   border — one shared seam line); the id keeps only where it stands */
#attachmenu {
  position: absolute;
  top: auto; /* not the header perch — it hangs its bottom on the composer */
  bottom: 100%;
  left: 6px;
  z-index: 15;
  width: 232px;
  /* nor the perch's scroll cap: a fixed handful of rows never scrolls */
  max-height: none;
  overflow-y: visible;
}
.amlabel { padding: 8px 10px 6px; font-size: calc(11 * var(--px)); color: var(--muted); }
.amthumbs { display: flex; gap: 6px; padding: 0 10px 10px; }
.amthumb {
  flex: none;
  width: 46px;
  height: 46px;
  padding: 0;
  cursor: pointer;
}
.amthumb:hover { border-color: var(--muted); }
.amthumb .upplay { font-size: calc(14 * var(--px)); }
.amrow {
  display: flex;
  align-items: center;
  gap: 8px;
  width: 100%;
  padding: 9px 10px;
  border-top: 1px solid var(--border);
  color: var(--muted);
  font-size: calc(12 * var(--px));
  text-align: left;
}
.amrow:hover { color: var(--text); }
/* no recents yet: the first row's divider would double the card's top edge */
.amrow:first-child { border-top: none; }

/* the two quiet strips that sit inside the composer, above the input: what
   is about to be uploaded, and what is being replied to */
#uploadbar,
#replybar {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 8px 12px 0;
  font-size: calc(12 * var(--px));
  color: var(--muted);
}
#uploadmute {
  display: flex;
  align-items: center;
  gap: 4px;
  white-space: nowrap;
  cursor: pointer;
}
#uploadbar select { padding: 4px 8px; }
/* a recording in the bar: the clock under a pulsing dot, no expiry or mute
   to pick until the clip exists */
#uploadbar.recording #uploadexpiry, #uploadbar.recording #uploadmute { display: none; }
#uploadbar.recording #uploadname::before {
  content: '';
  display: inline-block;
  width: 8px;
  height: 8px;
  margin-right: 6px;
  border-radius: 50%;
  background: var(--accent);
  animation: uc-pulse 1.4s ease-in-out infinite;
}

a.imglink { word-break: break-all; }

#imgpreview {
  z-index: 20;
  pointer-events: none;
  padding: 4px;
  /* the picture's 320 plus padding and hairline — app.js reads this to
     clamp the slab's left edge before the picture has a width */
  max-width: 330px;
}
#imgpreview img {
  display: block;
  max-width: 320px;
  max-height: 320px;
}
/* the link card's words (#87): the tip vocabulary under the picture, the
   description cut at three lines */
#imgpreview .tip-name,
#imgpreview .tip-sub {
  max-width: 312px;
  margin: 4px;
}
#imgpreview .tip-sub {
  display: -webkit-box;
  -webkit-box-orient: vertical;
  -webkit-line-clamp: 3;
  overflow: hidden;
}

#emotetip {
  z-index: 38;
  pointer-events: none;
  min-width: 130px;
  max-width: 240px;
  padding: 10px 12px;
  text-align: center;
}
#emotetip img {
  display: block;
  margin: 0 auto 6px;
  max-width: 128px;
  max-height: 128px;
}
/* big unicode emoji on reaction cards, sized like the emote image */
.tip-emoji {
  font-size: calc(64 * var(--px));
  line-height: 1.1;
  margin-bottom: 6px;
}
.tip-name {
  font-weight: 700;
  overflow-wrap: anywhere;
}
.tip-sub {
  color: var(--muted);
  font-size: calc(12 * var(--px));
  white-space: pre-line; /* the status dot's card stacks its stats one per line */
}
/* the one-line note (a timestamp's date, Sarah: 'a bit large and
   shouldn't be bolded, too much padding') */
#emotetip.small {
  min-width: 0;
  max-width: none;
  white-space: nowrap;
  padding: 4px 8px;
  font-size: calc(12 * var(--px));
}
#emotetip.small .tip-name { font-weight: 400; }
/* a hovered zero-width stack lists its overlays under the base emote,
   small, behind a divider (chatterino-style) */
.tip-more {
  border-top: 1px solid var(--border);
  margin-top: 8px;
  padding-top: 8px;
}
.tip-more img {
  max-width: 64px;
  max-height: 48px;
  margin: 0 auto 2px;
}

/* messages from users this viewer has blocked */
.msg.blocked, .msg.hid, .msg.muted { display: none; }

/* the other overlay strip (its slab is up with #filterbar's): this one
   stands on the composer, so its edge is a top border */
#suggestions {
  bottom: 100%;
  gap: 4px;
  overflow-x: auto;
  border-top: 1px solid var(--border);
}

#suggestions button {
  flex: none;
  display: flex;
  align-items: center;
  gap: 5px;
  padding: 4px 8px;
  border: 1px solid var(--border);
  background: var(--bg);
  color: var(--text);
  font: inherit;
  font-size: calc(12 * var(--px));
  cursor: pointer;
}
#suggestions button.selected { border-color: var(--accent); }
#suggestions img { height: 24px; }

#picker {
  max-height: 40vh;
  overflow-y: auto;
  border-bottom: 1px solid var(--border);
  background: var(--bg);
  padding: 6px;
}

/* sticky search + section tabs; full-bleed over the picker's padding */
.picker-nav {
  position: sticky;
  top: -6px;
  z-index: 2;
  display: flex;
  align-items: center;
  gap: 4px;
  margin: -6px -6px 0;
  padding: 6px;
  background: var(--bg);
  border-bottom: 1px solid var(--border);
}
/* the search grows into whatever the tabs don't need; when both won't fit,
   the search keeps its minimum and the tab strip scrolls */
.picker-nav input {
  flex: 1 1 90px;
  min-width: 90px;
  padding: 3px 6px;
  border: 1px solid var(--border);
  background: var(--bg-alt);
  color: var(--text);
  font-size: calc(12 * var(--px));
}
.picker-nav input:focus { outline: none; border-color: var(--accent); }
.pn-tabs {
  display: flex;
  align-items: center;
  gap: 2px;
  overflow-x: auto;
}
.pn-tabs button {
  flex: none;
  border: none;
  background: none;
  color: var(--muted);
  font-size: calc(10 * var(--px));
  font-weight: 700;
  text-transform: uppercase;
  padding: 4px 6px;
  cursor: pointer;
}
.pn-tabs button:hover { background: var(--bg-alt); color: var(--text); }
.pn-tabs .pn-emoji { font-size: calc(15 * var(--px)); padding: 2px 3px; }

.picker-head {
  padding: 8px 4px 4px;
  /* a tab's scrollIntoView must land the heading below the sticky nav */
  scroll-margin-top: 32px;
}

/* tracks come from layoutPicker; a button fills its cell */
.picker-grid {
  display: grid;
  gap: 2px;
}

.picker-grid button {
  min-width: 0;
  min-height: 0;
  display: flex;
  align-items: center;
  justify-content: center;
}
.picker-grid button:hover { background: var(--bg-alt); }
.picker-grid img { max-width: 32px; max-height: 32px; }
.picker-grid .emojibtn { font-size: calc(26 * var(--px)); line-height: 1; padding: 0; }

/* unicode emoji shown in an autocomplete chip where an emote image would go */
#suggestions .emojichar { font-size: calc(20 * var(--px)); line-height: 1; }

#input {
  width: 100%;
  /* 6px left + the emote icon's 6px inset = the buttons' own 12px rhythm,
     so the text starts as far from the smiley as the smiley sits from the pic */
  padding: 8px 12px 8px 6px;
  border: none;
  background: none;
  color: var(--text);
  font: inherit;
  /* textarea that grows with the message (autosize() sets the height);
     past ~5 lines it stops growing and scrolls inside */
  display: block;
  resize: none;
  max-height: calc(120 * var(--px));
  overflow-y: auto;
}
/* read-only is the in-flight state — it dims like disabled would, but keeps
   first responder so the send round trip can't fold the keyboard */
#input:read-only { opacity: 0.5; }
#input:focus { outline: none; }

/* #replybar's own strip is up with #uploadbar's */
#replybar #replydelete:hover { color: #ff6b6b; }

.msg.reply-target { background: color-mix(in srgb, var(--accent) 15%, transparent); }

#toast {
  position: fixed;
  left: 50%;
  transform: translateX(-50%);
  bottom: calc(110px + env(safe-area-inset-bottom));
  background: #3a1d1d;
  border: 1px solid #f44336;
  color: var(--text);
  padding: 8px 12px;
  max-width: 90vw;
  z-index: 10;
}

/* the ask (app.js ask()): the page's own confirm, a slab in the top layer
   over a dimmed page; the slab's fixed position centers it via the
   dialog's own auto margins */
#ask {
  inset: 0;
  margin: auto;
  padding: 14px 16px;
  max-width: min(90vw, 420px);
  color: var(--text);
}
#ask::backdrop { background: rgba(0, 0, 0, 0.5); }
#ask p { margin: 0 0 12px; line-height: 1.4; }
.askacts { display: flex; gap: 8px; justify-content: flex-end; }

/* about & privacy page (about.html) — a normal scrolling document,
   so undo the chat app's fixed full-height flex layout. */
body.aboutpage {
  display: block;
  height: 100dvh;
  /* the document scrolls inside body, not the viewport: the iOS shell pins
     the webview's own scroll offset (the chat app scrolls within itself,
     and a keyboard reveal would drag it off the glass), which froze these
     pages in the app — an inner scroller is invisible to that clamp. html
     hides its overflow so body's isn't propagated up to the viewport (the
     class sits on both: a :has() on html or body cost the engine a recalc
     of both for every node inserted anywhere on the page — every chat
     row, every scrollbar mark). */
  overflow-y: auto;
  overscroll-behavior: auto;
}
html.aboutpage { overflow: hidden; }
.about {
  max-width: 640px;
  margin: 0 auto;
  padding: 32px 20px 40px;
  padding-top: calc(32px + env(safe-area-inset-top));
}
/* only the masthead centers here — the prose below it stays ranged left */
.about h1 {
  margin: 0;
  font-size: calc(30 * var(--px));
  letter-spacing: -0.5px;
  text-align: center;
}
.about h1 a { color: inherit; text-decoration: none; }
/* the accent syllable — scoped, not flattened to .boo: an ancestor rule
   paints the whole wordmark and would otherwise win */
.about h1 .boo { color: var(--accent); }
.about .tagline {
  margin: 4px 0 24px;
  font-size: calc(16 * var(--px));
  font-weight: 600;
  text-align: center;
}
.about section { margin-top: 22px; }
.about h2 {
  margin: 0 0 6px;
  font-size: calc(15 * var(--px));
}
.about p { margin: 0; color: var(--muted); }
.about p + p { margin-top: 8px; }
.about b, .about em { color: var(--text); }
.about a { color: var(--accent); }
.about .backlink { margin-top: 32px; text-align: center; }
.about .agreerow { margin-top: 28px; text-align: center; }
.about .agreerow .abtn { font-size: calc(14 * var(--px)); padding: 10px 18px; }

/* --- the ideas board (@ideas) --- */

.ideas {
  max-width: 680px;
  margin: 0 auto;
  padding: 10px 12px 24px;
}
/* the board's one control row: sort picker left, the accent action right */
.ideabar {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 8px;
  margin: 4px 0 8px;
}
.idealink {
  padding: 0;
  color: var(--accent);
  font-size: inherit;
}
.idealink:hover { text-decoration: underline; }
.idealink.danger { color: var(--down); }
/* section headers (phases, themes), tracked out a little */
.ideahead {
  display: block;
  letter-spacing: 0.06em;
  margin: 16px 2px 4px;
}
/* the folded finished section — a header that opens (.head/.ideahead
   dress it; .bare doesn't zero UA button padding, so this does) */
button.donehead { padding: 0; }
button.donehead:hover { color: var(--text); }
/* a board card and a changelog entry are the same slab; the log entry
   carries its own padding (its children don't) */
.idearow,
.logrow {
  border: 1px solid var(--border);
  background: var(--bg-alt);
  margin: 6px 0;
}
.logrow { padding: 8px 10px; }
.ideamain {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 8px 10px;
  cursor: pointer;
}
.ideavotes { display: flex; gap: 4px; }
/* votes are read far more than pressed: quiet text, no boxes */
.votebtn {
  color: var(--muted);
  font-size: calc(12 * var(--px));
  padding: 4px 6px;
  white-space: nowrap;
}
.votebtn:hover { color: var(--text); }
.votebtn.on { color: var(--accent); }
.votebtn.dn.on { color: var(--down); }
.ideatitle { flex: 1; min-width: 0; overflow-wrap: anywhere; }
/* status chips and deploy-env chips as pills (a deliberate square-corner
   exception, like #jump) */
.ideastatus,
.logenv {
  font-size: calc(11 * var(--px));
  border: 1px solid var(--border);
  border-radius: 999px;
  color: var(--muted);
}
.ideastatus { padding: 2px 8px; white-space: nowrap; }
.logenv { padding: 1px 8px; }
.ideastatus.s-planned { color: #58a6ff; border-color: #58a6ff; }
.ideastatus.s-building { color: var(--accent); border-color: var(--accent); }
.ideastatus.s-shipped { color: var(--up); border-color: var(--up); }
.ideastatus.s-declined { color: var(--down); border-color: var(--down); }
.ideadetail { border-top: 1px solid var(--border); padding: 8px 10px; }
.ideabody {
  font-size: calc(14 * var(--px));
  white-space: pre-wrap;
  overflow-wrap: anywhere;
  margin-bottom: 8px;
}
.ideameta {
  display: flex;
  gap: 12px;
  align-items: center;
  color: var(--muted);
  font-size: calc(12 * var(--px));
}
.ideaform { margin: 6px 0; width: 100%; }
.idearowbtns { display: flex; gap: 12px; align-items: center; }
.idearow.focus, .logrow.focus { border-color: var(--accent); }
/* the dev pages, linked from the top of each: the one you're on is lit */
.devnav {
  display: flex;
  flex-wrap: wrap;
  gap: 4px 14px;
  margin: 0 0 10px;
  font-size: calc(13 * var(--px));
}
.devlink { padding: 0; color: var(--muted); text-decoration: none; }
.devlink:hover { color: var(--text); }
.devlink.on { color: var(--text); font-weight: 600; }
/* a card's own reply box */
.ideareply { display: flex; gap: 8px; margin-top: 8px; }
.ideareply .field { flex: 1; min-width: 0; }
/* the jumped-to message flashes so the eye finds it, then settles */
.msg.ctx-focus { animation: ctxflash 2.5s ease-out; }
@keyframes ctxflash {
  from { background: rgba(88, 166, 255, 0.22); }
  to { background: none; }
}
.ideanum { color: var(--muted); font-size: calc(12 * var(--px)); text-decoration: none; flex-shrink: 0; }
.ideanum:hover { color: var(--text); }
/* sub-cards inside a parent's detail: one row each, click hops to the card */
.subrow {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 3px 0;
  font-size: calc(13 * var(--px));
  cursor: pointer;
}
.subrow:hover .subtitle { color: var(--text); }
.subtitle { flex: 1; min-width: 0; overflow-wrap: anywhere; color: var(--muted); }
.ideaprog { color: var(--muted); font-size: calc(11 * var(--px)); white-space: nowrap; }
/* the card's story: status flips and shipped commits, quiet and dated */
.ideatale {
  margin: 8px 0;
  color: var(--muted);
  font-size: calc(12 * var(--px));
}
.ideataleline { padding: 1px 0; overflow-wrap: anywhere; }
.ideataledate { margin-right: 6px; opacity: 0.75; }
/* "#33" made live wherever text renders on a board instance */
.cardref { color: var(--accent); cursor: pointer; }
.cardref:hover { text-decoration: underline; }

/* the changelog: one block per deploy, commits and card chips below */
.logline { display: flex; align-items: center; gap: 8px; font-size: calc(13 * var(--px)); }
.logdate { color: var(--text); font-weight: 600; }
.logenv.env-prod { color: var(--up); border-color: var(--up); }
.logsha { color: var(--muted); font-size: calc(12 * var(--px)); font-family: ui-monospace, monospace; }
.logsum { margin: 4px 0 0; font-size: calc(14 * var(--px)); overflow-wrap: anywhere; }
.logcommits { margin-top: 6px; }
.logcommit {
  color: var(--muted);
  font-size: calc(12 * var(--px));
  padding: 1px 0;
  overflow-wrap: anywhere;
}
.logcards { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 8px; }
.logcard {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  background: none;
  border: 1px solid var(--border);
  color: var(--text);
  font: inherit;
  font-size: calc(12 * var(--px));
  padding: 3px 8px;
  cursor: pointer;
  max-width: 100%;
}
.logcard:hover { border-color: var(--muted); }

.ideacmts {
  margin-top: 8px;
  border-top: 1px solid var(--border);
  padding-top: 6px;
}
.ideacmt {
  font-size: calc(13 * var(--px));
  overflow-wrap: anywhere;
  padding: 2px 0;
}
.ideacmt.more { display: block; color: var(--muted); font-size: calc(12 * var(--px)); }
.ideacmt.more:hover { color: var(--text); }
.ideacmtwho { font-weight: 600; }

