/* ══════════════════════════════════════════════════════════════════════════
   Crew Social — Feed polish + Chats + Needs-attention strip  (Builder A)
   Loads AFTER dashboard.css, so rules here refine the concept's feed language.
   Tokens are owned by dashboard.css — this file only READS custom properties.
   ══════════════════════════════════════════════════════════════════════════ */

/* ─────────────────────────────  D1 · CHATS LIST  ───────────────────────── */
/* Section headers: font-round bold ink + soft count chip (small-caps labels
   are retired per the design language). */
.chats-sec { display: flex; align-items: center; gap: 9px; margin: 2px 2px 6px; }
.chats-h { font-family: var(--font-round); font-weight: 800; font-size: 17px;
  letter-spacing: -.01em; color: var(--ink); margin: 0; text-transform: none; }
.sec-count { font-family: var(--font-round); font-weight: 800; font-size: 12px;
  color: var(--muted); background: var(--surface-2); border: 1px solid var(--border);
  border-radius: 999px; min-width: 22px; height: 22px; padding: 0 7px;
  display: inline-grid; place-items: center; }

/* Avatar-forward conversation rows. */
.chat-item { gap: 13px; padding: 11px 2px; align-items: center; }
.chat-item .ci-av {
  width: 44px; height: 44px; border-radius: 50%; flex: none; display: grid; place-items: center;
  color: #fff; font-family: var(--font-round); font-weight: 900; font-size: 15px; letter-spacing: -.02em;
  border: 2px solid var(--surface); box-shadow: 0 0 0 1px var(--border);
}
.chat-item .ci-av svg { width: 21px; height: 21px; }
/* Channel tiles echo the story-rail palette (kind → hue). */
.ci-av.ci-everyone { background: var(--accent); }
.ci-av.ci-announcements { background: var(--mozz); color: #684d13; }
.ci-av.ci-custom { background: var(--basil); }
/* Game Room. The palette has exactly three hues — coral (--accent), gold (--mozz),
   green (--basil) — and --pepp is the SAME value as --accent in the live theme, so
   there is no fourth colour to take. The tile goes achromatic instead: --ink over
   --bg inverts with the theme by construction, and it is the one thing on the list a
   generated DM avatar (hsl 42%/44%) can never collide with. Reads as a console
   cabinet, which is the right note. The explicit color is not optional: .ci-av sets
   #fff for the coloured tiles, and #fff on a dark-theme --ink is an invisible glyph. */
.ci-av.ci-game { background: var(--ink); color: var(--bg); }
.ci-av.dm-av { color: #fff; }
.ci-av .dm-grp { display: grid; place-items: center; }
.chat-item .ci-name { font-family: var(--font-round); font-weight: 800; font-size: 15.5px;
  color: var(--ink); display: flex; align-items: center; gap: 4px; }
/* The store tag never shrinks. The name beside it is an anonymous text run and so
   cannot shrink, but the tag is a real flex item and a flex item's default
   `min-width: auto` is the only thing holding it at its own width — a long channel
   name squeezes it to an unreadable sliver before anything else gives. Organize
   mode is where this bites, because the checkbox takes the slack it used to have,
   and Organize is the mode where the store number is the entire point. */
.chat-item .ci-name .tag { flex: none; }
.chat-item .ci-sub { font-size: 13px; margin-top: 1px; }
/* Whose turn it is — the direction of the last message, stated on the row.
   ACHROMATIC ON PURPOSE. Coral is the unread badge, and coral means exactly one
   thing on this screen: there is something here you have not read. A thread you
   HAVE read can still be sitting on your side, so painting this coral would put
   two different meanings on one hue and make the badge worth less. --ink over --bg
   is the same escape the Game Room tile takes for the same reason, and it inverts
   with the theme by construction rather than by a second declaration.
   The weight is asymmetric because the states are: "Your turn" is the one that
   might make somebody act, so it is the solid one; "Their turn" is reassurance and
   recedes into the --tag idiom. */
.chat-item .ci-turn {
  /* margin-left:auto pins it to the END of the name line rather than letting it sit
     against the name. That is the difference between a label you read per row and a
     COLUMN you scan down in one pass, which is the whole request — and it costs the
     row nothing, because the space it takes is space the name was not using. */
  flex: none; margin-left: auto; white-space: nowrap;
  font-family: var(--font-round); font-weight: 800;
  font-size: 10px; letter-spacing: .01em; line-height: 15px; height: 15px;
  padding: 0 6px; border-radius: 999px;
}
.chat-item .ci-turn.yours { background: var(--ink); color: var(--bg); }
.chat-item .ci-turn.theirs {
  background: var(--surface-2); color: var(--muted); border: 1px solid var(--border);
  line-height: 13px;   /* the border owns the other 2px, so both pills sit at 15px */
}
/* The name is a real flex item now rather than an anonymous text run, which is what
   lets IT give way when a long name meets a chip. Without this the pair simply grows
   past the row and pushes the unread pill out. */
.chat-item .ci-name .ci-nm {
  min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}

/* Unread count as a coral pill in the story-dot idiom. */
.chat-item .ci-unread {
  flex: none; min-width: 22px; height: 22px; padding: 0 7px; border-radius: 999px;
  background: var(--accent); color: var(--accent-ink); font-weight: 900; font-size: 12px;
  font-family: var(--font-round); display: inline-grid; place-items: center;
  font-variant-numeric: tabular-nums; box-shadow: 0 2px 6px -2px color-mix(in srgb, var(--accent) 70%, transparent);
}

/* Archive / Restore / Archived — word buttons in the .iclink idiom (the shared
   icon set has no archive glyph, and the words are unambiguous). Quiet by
   default so they never compete with the unread pill; both themes read them
   through --muted / --ink, which dashboard.css owns. The 30px target keeps the
   tap area honest beside a 44px row on a 390px screen. */
.chat-item .ci-arch, .chats-sec .ci-arch {
  flex: none; font-family: var(--font-round); font-weight: 800; font-size: 12px;
  color: var(--muted); padding: 0 9px; height: 30px; border-radius: 999px;
  border: 1px solid var(--border); background: var(--surface-2);
  white-space: nowrap; line-height: 1;
}
.chat-item .ci-arch:hover, .chats-sec .ci-arch:hover { color: var(--ink); }

/* Play — the same word-pill, inverted into the primary action of a thread header.
   It has to declare the whole pill rather than only the emphasis: the .ci-arch rule
   above is scoped to .chat-item / .chats-sec and the Play button lives in .feed-head,
   where NONE of it applies. The unscoped selector is what actually styles it; the
   compound one is there so that if this pill is ever placed inside a chat row it still
   wins the tie against `.chat-item .ci-arch` on source order.
   Filled --accent over --accent-ink is the house's primary idiom (button.btn,
   .chip.on, .send-btn), so it inverts correctly in both themes with one declaration.
   34px keeps it under the 38px back button — clearly the loudest control in the
   header without becoming the tallest thing in it.
   The pill is a WORD and carries no glyph — ICONS.game exists, but the measured 390px
   header already ends at 376px — so there is deliberately no svg sizing rule here to go
   stale, and no flex gap for a child that never arrives. */
.ci-play, .ci-arch.ci-play {
  flex: none; display: inline-flex; align-items: center; justify-content: center;
  font-family: var(--font-round); font-weight: 800; font-size: 13px;
  height: 34px; padding: 0 14px; border-radius: 999px; line-height: 1; cursor: pointer;
  background: var(--accent); color: var(--accent-ink); border: 1px solid transparent;
  white-space: nowrap;
  box-shadow: 0 2px 6px -2px color-mix(in srgb, var(--accent) 70%, transparent);
}
/* Hover must restate the colour: .chat-item .ci-arch:hover would otherwise repaint
   the label with --ink on an --accent fill in the one place both selectors match. */
.ci-play:hover, .ci-arch.ci-play:hover { color: var(--accent-ink); }

/* ─────────────────────  D1b · CHAT FOLDERS (district manager+)  ───────────
   A folder row IS a .chat-item — same height, same gap, same avatar box — minus
   the .ci-sub line, which is what makes it read as one line against a two-line
   channel row without inventing a second row metric. Everything below is either
   the chevron, the tile that replaces the round avatar, or selection chrome. */
.cf-row { cursor: pointer; }
.cf-chev {
  flex: none; width: 16px; height: 16px; color: var(--muted); display: grid;
  place-items: center; transition: transform .18s ease;
}
.cf-chev svg { width: 16px; height: 16px; }
.cf-chev.open { transform: rotate(90deg); }
@media (prefers-reduced-motion: reduce) { .cf-chev { transition: none; } }
/* Squircle, not a circle: a folder is not a person or a room, and the shape is the
   fastest way to say so at 44px. Shape is also what lets a folder take one of the
   channel hues without being mistaken for a channel — a coral SQUIRCLE and a coral
   CIRCLE are different objects at a glance, so the palette can be reused rather than
   invented.
   It used to be --surface-3 on --muted, the two quietest tokens in the palette,
   which read as DISABLED rather than as a container next to saturated channel tiles.
   Colour is per folder now (see .cf-slate and friends), which also makes one folder
   tell itself apart from another — the tile finally carries information instead of
   repeating what the chevron already said. */
.chat-item .ci-av.cf-tile {
  border-radius: 13px; border: 2px solid var(--surface);
  box-shadow: 0 0 0 1px var(--border);
}
.chat-item .ci-av.cf-tile svg { width: 20px; height: 20px; }
/* The four choices. `slate` is the default and the achromatic one — --ink over --bg,
   the move the Game Room tile already makes, so it inverts with the theme by
   construction and is the one shade a generated DM avatar can never land on. The
   explicit foreground on each is not optional: .ci-av sets #fff, and #fff on gold is
   unreadable while #fff on a light-theme --ink is fine but on dark is invisible. */
.cf-tile.cf-slate { background: var(--ink);    color: var(--bg); }
.cf-tile.cf-coral { background: var(--accent); color: #fff; }
.cf-tile.cf-basil { background: var(--basil);  color: #fff; }
.cf-tile.cf-mozz  { background: var(--mozz);   color: #684d13; }

/* The picker. Selection is a RING rather than a fill change, because three of the
   four swatches are already a fill — and the ring is drawn as surface-then-ink so it
   stays visible on the slate swatch, where a bare --ink ring would sit invisibly
   against an --ink tile. */
.cf-swatches { display: flex; gap: 10px; margin: 2px 0 14px; }
.cf-swatch {
  width: 40px; height: 40px; border-radius: 12px; flex: none; padding: 0;
  cursor: pointer; border: none; box-shadow: 0 0 0 1px var(--border);
}
.cf-swatch.cf-slate { background: var(--ink); }
.cf-swatch.cf-coral { background: var(--accent); }
.cf-swatch.cf-basil { background: var(--basil); }
.cf-swatch.cf-mozz  { background: var(--mozz); }
.cf-swatch[aria-pressed="true"] {
  box-shadow: 0 0 0 3px var(--surface), 0 0 0 5px var(--ink);
}
/* A dot, not a tile: in the Move sheet the colour is a cue beside a label, not the
   subject of the row. */
.cf-dest { display: flex; align-items: center; gap: 10px; }
.cf-dot {
  width: 14px; height: 14px; border-radius: 5px; flex: none;
  box-shadow: 0 0 0 1px var(--border);
}
.cf-dot.cf-slate { background: var(--ink); }
.cf-dot.cf-coral { background: var(--accent); }
.cf-dot.cf-basil { background: var(--basil); }
.cf-dot.cf-mozz  { background: var(--mozz); }
/* The rail is the only thing saying "these belong to the folder above", so it has to
   survive both themes; --hair is the same divider the rows already use. */
.cf-kids { padding-left: 24px; margin-left: 12px; border-left: 2px solid var(--hair); }
.cf-kids .chat-item .ci-av { width: 36px; height: 36px; font-size: 12.5px; }
.cf-kids .chat-item .ci-av svg { width: 18px; height: 18px; }
.cf-empty { padding: 10px 2px; font-size: 13px; }

/* Selection. No drag anywhere in this feature — the checkbox IS the affordance, and
   a 22px box beside a 44px row keeps the tap target honest on a 390px screen. */
.cf-box {
  flex: none; width: 22px; height: 22px; border-radius: 7px;
  border: 2px solid var(--border); background: var(--surface-2);
  display: grid; place-items: center; color: transparent;
}
.cf-box svg { width: 13px; height: 13px; }
.chat-item.cf-pick.on .cf-box {
  background: var(--accent); border-color: var(--accent); color: var(--accent-ink);
}
.chat-item.cf-pick.on { background: color-mix(in srgb, var(--accent) 8%, transparent); }

/* The Organize action bar sits at the foot of the Channels card rather than fixed to
   the viewport: fixed would need safe-area and keyboard-inset handling on a surface
   that has neither today, and the card already scrolls into view with the list. */
.cf-bar {
  display: flex; align-items: center; gap: 9px; padding: 11px 2px 4px;
  border-top: 1px solid var(--hair); margin-top: 4px;
}
.cf-bar b { font-family: var(--font-round); font-weight: 800; font-size: 13.5px; }
.cf-bar .btn.cf-move { width: auto; margin: 0; padding: 0 16px; height: 34px; font-size: 13px; }
.cf-bar .btn.cf-move[disabled] { opacity: .45; cursor: not-allowed; }

/* ────────────────────────  D1 · THREAD (chat bubbles)  ──────────────────── */
/* No card chrome around the conversation — messages sit on the page background,
   Messenger-style. */
.card.thread {
  background: none; border: none; box-shadow: none; border-radius: 0;
  padding: 4px 2px 6px; margin: 0;
}
/* Messenger-style: each message is a column (caption + row). Runs from the same
   author sit tight; a new sender gets extra top space. */
.thread .msg {
  display: flex; flex-direction: column; gap: 2px; padding: 0; margin-top: 2px;
  align-items: stretch; border-bottom: none;
}
.thread .msg.grp-first { margin-top: 12px; }
.thread .msg:first-child { margin-top: 2px; }

/* Sender name caption above a run (others only), indented past the avatar gutter. */
.thread .mcap { font-size: 11.5px; color: var(--muted); font-weight: 600; margin: 0 0 1px 40px; }

/* The row: avatar gutter (others, left) + bubble column. */
.thread .mrow { display: flex; align-items: flex-end; gap: 8px; max-width: 100%; }
.thread .msg.own .mrow { justify-content: flex-end; }
.thread .mav { width: 28px; flex: none; align-self: flex-end; }
.thread .mav .avatar { width: 28px; height: 28px; font-size: 11px; border-radius: 50%; }
.thread .mcol { display: flex; flex-direction: column; gap: 2px; min-width: 0; max-width: 76%; }
.thread .mcol.poll-message { width: min(76%, 380px); }
/* A cover card takes a definite width for the same reason a poll does: it holds
   a date, a time span and two buttons, and a shrink-to-fit column would let the
   buttons resize with the length of the store name. */
.thread .mcol.cover-message { width: min(80%, 400px); }
/* A photo column takes the same definite width, and for a stronger reason than the
   poll's tidiness. `.msg.own .mcol` is align-items:flex-end, which makes an own
   bubble shrink to its content — and a `width: 100%` image has no width to take a
   percentage of until its bytes arrive. The 4/3 box reserved in dashboard.css
   therefore reserved nothing at all in your OWN messages: the row collapsed to a
   few pixels, then sprang to full height when the photo loaded, throwing the thread
   down by most of a message. Other people's bubbles stretch, so theirs always
   reserved correctly — which is why this looked like it picked and chose. */
.thread .mcol.photo-message { width: min(76%, 380px); }
.thread .msg.own .mcol { align-items: flex-end; }

/* The bubble: others = surface-2 gray, own = solid accent (Messenger blue equiv). */
.thread .msg .bubble {
  width: fit-content; max-width: 100%; background: var(--surface-2); color: var(--ink);
  border-radius: 18px; padding: 8px 13px; font-size: 15px; line-height: 1.4;
  white-space: pre-wrap; word-break: break-word;
}
.thread .msg.own .bubble { background: var(--accent); color: var(--accent-ink); border: none; }
/* Connected corners: flatten the side that touches a grouped neighbor. */
.thread .msg:not(.own):not(.grp-first) .bubble { border-top-left-radius: 5px; }
.thread .msg:not(.own):not(.grp-last)  .bubble { border-bottom-left-radius: 5px; }
.thread .msg.own:not(.grp-first) .bubble { border-top-right-radius: 5px; }
.thread .msg.own:not(.grp-last)  .bubble { border-bottom-right-radius: 5px; }

/* ── Autolinked URLs (every .bubble: feed cards, thread messages, comments) ──
   `color: inherit` is the deliberate choice, not a placeholder. An OWN bubble is
   solid --accent, so an accent-coloured link there is invisible and --ment-ink
   (the chip's darkened accent) is red-on-coral; inheriting means a link is
   always exactly as readable as the text it sits in, in both themes, in every
   bubble, with no new palette token to keep in sync. The UNDERLINE is the
   affordance — and being the affordance, it must not be the thing that gets
   tidied away later, because colour alone would then be carrying it in bubbles
   where colour is the one thing we cannot vary.
   `overflow-wrap: anywhere` because .bubble's word-break does not apply inside
   a nested inline box: a 200-character URL blew the bubble past 390px without it. */
.bubble a.blink {
  color: inherit; text-decoration: underline; text-underline-offset: 2px;
  overflow-wrap: anywhere;
}

/* Media + reactions ride under the bubble, same side. */
/* align-self:stretch is the other half of .photo-message below, and both are
   needed. The column gets a definite width there; this makes the grid actually
   TAKE it — .msg.own .mcol is align-items:flex-end, which shrink-wraps every child
   no matter how wide the column is. Without it the grid sized itself to the
   image's intrinsic width, which is zero until the bytes land. A no-op for other
   people's messages, whose column already stretches. */
.thread .msg .photo-grid { max-width: 100%; margin-top: 2px; align-self: stretch; }
.thread .msg .rx { margin-top: 3px; }
.thread .msg.own .rx { justify-content: flex-end; }
.thread .msg .seenby { align-self: stretch; }

/* Centered day/time divider. */
.thread .tdiv { text-align: center; margin: 14px 0 8px; }
.thread .tdiv span { font-size: 11px; color: var(--muted); font-weight: 600; letter-spacing: .02em; }

/* Reply affordance + nested replies stay full-width and left-read. */
.thread .msg .replybar,
.thread .msg .replies,
.thread .msg .reply-cmp { align-self: stretch; }
.thread .msg.own .replybar { justify-content: flex-end; }
/* A reply row STRETCHES, like every other message row. It used to be aligned to
   one end here, which reads as an alignment choice and is not: .msg is a flex
   column, so align-items:flex-end shrink-wraps its .mrow to the content — and
   .mcol's `max-width: 76%` then has a shrink-wrapped row to take 76% OF. The two
   constraints chase each other down to the widest single word, which is why a
   one-word reply came out as a tall ribbon one character wide while the identical
   text at top level was fine.
   Alignment was never coming from here anyway: .msg.own .mrow already carries
   justify-content: flex-end, and a non-own row is left-aligned by default. */
.thread .replies .msg,
.thread .replies .msg.own { align-items: stretch; }

/* Messenger-style compact composer: a single row — camera · rounded auto-grow
   input · circular send. No separate button row, no tall multiline box. */
.composer { border-top: 1px solid var(--border); padding: 5px 14px 7px; margin-top: 0; }
.composer .crow { display: flex; align-items: flex-end; gap: 8px; margin-top: 0; }
.composer textarea {
  flex: 1; min-width: 0; min-height: 40px; height: 40px; max-height: 120px;
  border-radius: 20px; padding: 9px 15px; resize: none; line-height: 1.35;
  background: var(--surface-2); border: 1px solid var(--border);
}
.composer .pick {
  width: 40px; height: 40px; flex: none; padding: 0; border-radius: 50%;
  background: none; border: none; color: var(--accent); display: grid; place-items: center; cursor: pointer;
}
.composer .pick svg { width: 24px; height: 24px; }
.composer .send-btn {
  width: 40px; height: 40px; flex: none; border-radius: 50%; border: none; cursor: pointer;
  background: var(--accent); color: var(--accent-ink); display: grid; place-items: center;
}
.composer .send-btn svg { width: 20px; height: 20px; }
/* Busy state while a post/upload is in flight: dim, no pointer, spin the icon so
   it clearly reads as "sending" (stops the tap-repeat double-send). */
.composer .send-btn:disabled { cursor: default; opacity: .6; }
.composer .send-btn.sending { pointer-events: none; }
.composer .send-btn.sending svg { animation: send-spin .7s linear infinite; }
@keyframes send-spin { to { transform: rotate(360deg); } }
.composer .previews { margin: 0 0 8px 48px; }

/* Poll creation expands from the existing three-control composer. The question
   remains the same textarea; only the bounded option editor appears below it. */
.poll-compose {
  min-width: 0; margin: 8px 0 2px 48px; padding: 9px;
  border: 1px solid var(--border); border-radius: 14px; background: var(--surface-2);
}
.poll-compose[hidden] { display: none; }
.poll-compose-head, .poll-compose-foot {
  display: flex; align-items: center; justify-content: space-between; gap: 8px;
}
.poll-compose-head b {
  min-width: 0; color: var(--ink); font: 800 13px/1.2 var(--font-round);
}
.poll-compose-head button, .poll-add {
  min-height: 36px; border: 0; border-radius: 999px; padding: 0 10px;
  background: transparent; color: var(--accent); cursor: pointer;
  font: 800 12px/1 var(--font-round);
}
.poll-compose-options {
  display: flex; flex-direction: column; gap: 6px; max-height: min(32vh, 250px);
  margin: 6px 0; overflow-y: auto; overscroll-behavior: contain;
}
.poll-compose-row { display: flex; align-items: center; gap: 6px; min-width: 0; }
.poll-compose-row input {
  flex: 1; min-width: 0; height: 40px; border: 1px solid var(--border);
  border-radius: 12px; padding: 8px 11px; background: var(--surface); color: var(--ink);
}
.poll-remove {
  width: 40px; height: 40px; flex: none; display: grid; place-items: center;
  border: 0; border-radius: 50%; background: transparent; color: var(--muted); cursor: pointer;
}
.poll-remove svg { width: 17px; height: 17px; }
.poll-add {
  display: inline-flex; align-items: center; gap: 5px; margin-left: -8px;
}
.poll-add svg { width: 16px; height: 16px; }
.poll-add:disabled { color: var(--muted); cursor: default; opacity: .7; }
.poll-compose-count { color: var(--muted); font-size: 11.5px; }
.poll-compose-error {
  min-height: 15px; color: var(--bad); font-size: 11.5px; line-height: 1.3;
}

/* Inline results stay visible before and after voting. Every option is a full
   width 44px target; the proportional fill sits behind escaped server text. */
.poll-card {
  width: 100%; min-width: 0; margin-top: 4px; padding: 8px;
  border: 1px solid var(--border); border-radius: 16px;
  background: var(--surface); color: var(--ink);
}
.poll-meta {
  display: flex; align-items: center; justify-content: space-between; gap: 8px;
  padding: 0 2px 7px; color: var(--muted); font-size: 11.5px; font-weight: 700;
}
.poll-closed {
  border: 1px solid var(--border); border-radius: 999px; padding: 2px 7px;
  background: var(--surface-2); color: var(--ink);
}
.poll-options { display: flex; flex-direction: column; gap: 6px; }
.poll-option {
  position: relative; isolation: isolate; width: 100%; min-width: 0; min-height: 44px;
  display: flex; align-items: center; justify-content: space-between; gap: 8px;
  overflow: hidden; padding: 8px 10px; border: 1px solid var(--border);
  border-radius: 12px; background: var(--surface-2); color: var(--ink);
  font: inherit; text-align: left; cursor: pointer;
}
.poll-option:disabled { opacity: 1; cursor: default; }
.poll-option.selected {
  border-color: var(--accent);
  box-shadow: inset 0 0 0 1px var(--accent);
}
.poll-fill {
  position: absolute; z-index: -1; inset: 0 auto 0 0;
  background: color-mix(in srgb, var(--accent) 18%, var(--surface-2));
  transition: width .18s ease;
}
.poll-option-main {
  display: flex; align-items: center; gap: 6px; min-width: 0; font-weight: 700;
}
.poll-option-label { min-width: 0; overflow-wrap: anywhere; }
.poll-check {
  width: 19px; height: 19px; flex: none; display: grid; place-items: center;
  border-radius: 50%; background: var(--accent); color: var(--accent-ink);
}
.poll-check svg { width: 12px; height: 12px; }
.poll-option-result {
  flex: none; display: inline-flex; align-items: baseline; gap: 5px;
  color: var(--muted); font-size: 11.5px; font-variant-numeric: tabular-nums;
}
.poll-option-result b { color: var(--ink); }
.poll-close {
  min-height: 36px; margin: 5px 0 -3px; padding: 0 8px; border: 0;
  border-radius: 999px; background: transparent; color: var(--accent);
  font: 800 12px/1 var(--font-round); cursor: pointer;
}
.poll-card.busy { opacity: .7; }

/* ── Fixed-composer chat layout (Messenger): only the message list scrolls; the
   composer stays pinned above the bottom nav. Toggled on <main> by loadThread(),
   removed by renderInbox()/activatePane(). ── */
main.chat-mode { display: flex; flex-direction: column; overflow: hidden; }
main.chat-mode .wrap {
  flex: 1; min-height: 0; display: flex; flex-direction: column;
  /* Override the base .wrap `max-width:660px; margin:0 auto`: as a FLEX item those
     make it shrink-to-content and center, so the composer got side gaps that grew/
     shrank with message width (the "gaps until you add a photo" bug). Fill instead. */
  width: 100%; max-width: none; margin: 0;
  /* full-bleed; clear the fixed tabbar. Held in a variable so the composing state
     below can retract it without also overriding the desktop rule further down. */
  padding: 0 0 var(--chat-bottom-reserve, calc(58px + env(safe-area-inset-bottom)));
}
main.chat-mode #view { flex: 1; min-height: 0; display: flex; flex-direction: column; }
main.chat-mode #view .feed-head { flex: none; margin: 10px 0 6px; padding: 0 14px; }
main.chat-mode #view .card.thread { flex: 1; min-height: 0; overflow-y: auto; display: flex; flex-direction: column; padding: 4px 14px 6px; }
/* Short conversations sit at the bottom (just above the composer), Messenger-style;
   margin-top:auto collapses once the messages overflow so the top stays reachable. */
.thread .thread-inner { margin-top: auto; }
main.chat-mode #view .composer { position: static; margin-top: 0; }
/* Keyboard up in a chat: hide the bottom chrome and retract the space held for it.
   The tab bar is pinned to a viewport bottom the browser keeps re-deciding while a
   keyboard is open, which made it visibly drop mid-message; behind a keyboard it is
   unreachable anyway. core.js sets .chat-composing from composer focus. Matching on
   `main.chat-mode ~` keeps the rule self-limiting — if the class ever outlived the
   chat view, the tab bar comes back on its own. */
html.chat-composing main.chat-mode ~ nav.tabbar,
html.chat-composing main.chat-mode ~ .fab { display: none; }
html.chat-composing { --chat-bottom-reserve: 0px; }
/* …and clamp the shell to the height that is actually visible, rather than trusting
   the viewport to have shrunk. `interactive-widget=resizes-content` in the shell head
   asks Chromium to shrink the layout viewport for the keyboard, but that key is
   Chromium-only and still needs the embedder to report a keyboard: WebKit ignores it,
   and a WebView whose window is not resized never learns one opened. Where it is not
   honoured `100dvh` keeps measuring the whole screen, the composer is laid out BELOW
   the keyboard's top edge, and the browser pans the visual viewport just far enough to
   keep the caret in sight — which is the composer unsticking from the keyboard and
   drifting as you type, and drifting further with a photo chip attached because the
   composer is then taller. core.js publishes visualViewport.height, true under every
   model; where the key IS honoured the two are equal and this clamps to what CSS
   already computed. Scoped to composing, so nothing else in the app changes size. */
html.chat-composing, html.chat-composing body {
  height: var(--rx-visual-height, 100%); min-height: 0; overflow: hidden;
}
/* Under that clamp the message list absorbs the squeeze, never the composer — a
   composer allowed to shrink puts its own send button back under the keyboard, which
   is the same complaint one layer down. */
main.chat-mode .composer { flex: none; }
/* Desktop: the tabbar is hidden, so no reservation is needed. */
@media (min-width: 980px) {
  main.chat-mode .wrap { padding-bottom: 14px; max-width: 660px; margin: 0 auto; }
}
.reply-cmp textarea { border-radius: 14px; }
.reply-cmp button.btn { border-radius: 999px; }

/* ───────────────────────  D2 · COMPOSER SHEET (modal)  ──────────────────── */
.compose-sheet h3 { font-family: var(--font-round); font-weight: 900; }
/* "Posting to <channel>" context, echoing the story-tile palette. */
.compose-ctx { display: flex; align-items: center; gap: 9px; margin: -2px 0 13px;
  font-size: 13.5px; color: var(--muted); }
.compose-ctx b { color: var(--ink); font-family: var(--font-round); font-weight: 800; }
.compose-ctx .cc-tile { width: 32px; height: 32px; border-radius: 50%; flex: none;
  display: grid; place-items: center; color: #fff; }
.compose-ctx .cc-tile svg { width: 17px; height: 17px; }
.cc-tile.ci-everyone { background: var(--accent); }
.cc-tile.ci-announcements { background: var(--mozz); color: #684d13; }
.cc-tile.ci-custom { background: var(--basil); }
.compose-ta { min-height: 118px; border-radius: 18px; padding: 13px 15px; line-height: 1.5; }
.compose-actions { display: flex; align-items: center; gap: 9px; margin-top: 11px; }
/* Photo picker as a rounded pill in the composer vocabulary. */
.compose-sheet .pick {
  display: inline-flex; align-items: center; gap: 7px; cursor: pointer;
  background: var(--surface-2); border: 1px dashed var(--border); color: var(--muted);
  border-radius: 999px; padding: 8px 15px; font-size: 13.5px; font-weight: 700; font-family: var(--font-round);
}
.compose-sheet .pick:hover { color: var(--ink); border-color: var(--accent); }
.compose-sheet .pick svg { width: 18px; height: 18px; }
.compose-sheet .btn { border-radius: 999px; font-family: var(--font-round); font-weight: 800; }

/* ─────────────────────────  D2 · NOTIFICATIONS  ─────────────────────────── */
.notif-list { gap: 2px; }
.notif-row { gap: 12px; padding: 12px 6px; border-radius: 16px; border-bottom: none; align-items: center; }
.notif-row.unread { background: color-mix(in srgb, var(--accent) 9%, transparent); }
.notif-row .ntitle { font-family: var(--font-round); font-weight: 800; font-size: 14px; }
.notif-row .nsnip { font-size: 12.5px; margin-top: 1px; }
/* "4 new" on a collapsed entry. Quieter than the title it follows — it is a
   quantity, not the thing itself — and it never wraps to its own line. */
.notif-row .ncount { margin-left: 6px; font-weight: 700; font-size: 11.5px;
  color: var(--pepp); white-space: nowrap; }
/* Circular kind-colored tiles instead of flat grey squares. */
.notif-row .nic { width: 40px; height: 40px; border-radius: 50%; }
.notif-row .nic svg { width: 18px; height: 18px; }
.notif-row .nic-mention { background: color-mix(in srgb, var(--accent) 20%, var(--surface-2)); color: var(--accent); }
.notif-row .nic-reply { background: color-mix(in srgb, var(--accent) 20%, var(--surface-2)); color: var(--accent); }
.notif-row .nic-dm { background: color-mix(in srgb, var(--basil) 22%, var(--surface-2)); color: var(--basil); }
.notif-row .nic-message { background: color-mix(in srgb, var(--basil) 22%, var(--surface-2)); color: var(--basil); }
.notif-row .nic-announcement { background: color-mix(in srgb, var(--mozz) 26%, var(--surface-2)); color: var(--warn); }
.notif-row .nic-system { background: var(--surface-2); color: var(--muted); }
.notif-row .nic-schedule { background: var(--surface-2); color: var(--muted); }
/* A waiting applicant is the one notification in this drawer that is a decision
   somebody has to make, so it carries the same alert colour as the count chip on the
   card it opens rather than the grey every other post-less notice gets. */
.notif-row .nic-join_request { background: color-mix(in srgb, var(--pepp) 20%, var(--surface-2)); color: var(--pepp); }
/* Unread dot marker on the tile. */
.notif-row.unread .nic { position: relative; }
.notif-row.unread .nic::after { content: ""; position: absolute; top: -1px; right: -1px;
  width: 11px; height: 11px; border-radius: 50%; background: var(--accent); border: 2px solid var(--surface); }

/* ═══════════════════════ ORG-WIDE FEED SHELL ═══════════════════════════ */
.org-feed-shell { width: 100%; max-width: 920px; margin: 0 auto; }
.org-feed-head { display: flex; align-items: end; justify-content: space-between; padding: 6px 2px 10px; }
.org-feed-head .eyebrow { display: block; color: var(--muted); font-size: 10.5px; font-weight: 800;
  letter-spacing: .12em; text-transform: uppercase; margin-bottom: 2px; }
.org-feed-head h1 { margin: 0; color: var(--ink); font-family: var(--font-round); font-size: 25px;
  line-height: 1.1; letter-spacing: -.025em; }
/* top:0, not a negative offset. `main` is the scroll container and the header is its
   SIBLING, so this sticks against the top of the scrollport — the edge immediately
   under the header. A negative top parks the card that far above that edge, which
   does not tuck it neatly, it clips it: at -14px the card lost its top border, its
   rounded corners and 9px of the 39px pill, so the segmented control arrived
   visibly cut in half. Flush with the scrollport is the whole intent. */
.feed-segments { position: sticky; top: 0; z-index: 12; display: grid; grid-template-columns: repeat(2, 1fr);
  gap: 4px; padding: 5px; margin: 0 0 14px; background: color-mix(in srgb, var(--surface) 92%, transparent);
  border: 1px solid var(--border); border-radius: 17px; box-shadow: 0 5px 18px -16px #000;
  backdrop-filter: blur(16px); -webkit-backdrop-filter: blur(16px); }
.feed-seg { min-width: 0; height: 39px; border: 0; border-radius: 12px; background: transparent;
  color: var(--muted); cursor: pointer; font: 800 13.5px/1 var(--font-round); display: flex;
  align-items: center; justify-content: center; gap: 6px; }
.feed-seg.on { background: var(--ink); color: var(--surface); box-shadow: 0 4px 12px -9px #000; }
.feed-seg > span { min-width: 18px; height: 18px; border-radius: 999px; padding: 0 5px;
  display: inline-grid; place-items: center; background: var(--accent); color: var(--accent-ink); font-size: 10px; }
.feed-section { min-width: 0; }
.activity-intro, .news-intro { display: flex; align-items: center; gap: 16px; justify-content: space-between;
  margin: 0 2px 14px; padding: 4px 2px; }
.activity-intro h2, .news-intro h2 h2 { margin: 0 0 3px; font: 900 20px/1.15 var(--font-round); color: var(--ink); }
.activity-intro p, .news-intro p p { margin: 0; color: var(--muted); font-size: 13px; line-height: 1.4; max-width: 560px; }
/* The header stacks before the action button can crowd the title on a 390px screen.
   Alignment is deliberately left to the 650px block below, which lands later and
   would override an align-items set here — both sections share it. */
@media (max-width: 430px) {
  .activity-intro, .news-intro { flex-direction: column; gap: 11px; }
}
.soft-action { flex: none; display: inline-flex; align-items: center; justify-content: center; gap: 7px;
  border: 1px solid var(--border); border-radius: 999px; padding: 8px 13px; background: var(--surface);
  color: var(--ink); cursor: pointer; font: 800 12.5px/1 var(--font-round); }
.soft-action svg { width: 16px; height: 16px; }
.soft-action.primary { background: var(--accent); color: var(--accent-ink); border-color: transparent; }
.soft-action.danger { color: var(--bad); }

/* Activity stays card-forward: recognition is visually distinct from chat. */
.activity-post { overflow: visible; }
.activity-band { display: flex; align-items: center; gap: 7px; margin: -2px 0 12px; color: var(--muted);
  font-size: 12px; }
.activity-band > span:first-child { width: 28px; height: 28px; display: grid; place-items: center;
  border-radius: 50%; background: color-mix(in srgb, var(--mozz) 25%, var(--surface-2)); font-size: 15px; }
.activity-band b { color: var(--ink); font-family: var(--font-round); font-size: 12.5px; }
.activity-subject { color: var(--muted); }
.activity-focus { animation: activity-focus 1.8s ease-out; }
@keyframes activity-focus { 0%,35% { box-shadow: 0 0 0 4px color-mix(in srgb, var(--accent) 45%, transparent); } }

.compose-help { margin: -5px 0 12px; font-size: 13px; }
.activity-comments-list { max-height: min(52vh, 520px); overflow: auto; margin: 4px -3px 12px; }
.activity-comment { display: flex; align-items: flex-start; gap: 9px; padding: 9px 3px; }
.activity-comment > .avatar { width: 34px; height: 34px; flex: none; font-size: 12px; }
.activity-comment-body { min-width: 0; flex: 1; }
.activity-comment-body > div:first-child { display: flex; align-items: baseline; gap: 7px; margin-bottom: 3px; }
.activity-comment-body > div:first-child b { font: 800 13px/1 var(--font-round); color: var(--ink); }
.activity-comment-body > div:first-child span { color: var(--muted); font-size: 10.5px; }
.activity-comment-body .bubble { display: inline-block; background: var(--surface-2); border-radius: 14px;
  padding: 8px 11px; color: var(--ink); white-space: pre-wrap; font-size: 14px; }
.activity-comment-compose { display: flex; align-items: flex-end; gap: 8px; border-top: 1px solid var(--border); padding-top: 9px; }
.activity-comment-compose textarea { flex: 1; min-height: 40px; max-height: 110px; border-radius: 20px;
  resize: none; padding: 9px 13px; background: var(--surface-2); }
.activity-comment-compose .send-btn { width: 40px; height: 40px; border: 0; border-radius: 50%;
  display: grid; place-items: center; background: var(--accent); color: var(--accent-ink); }

/* News cards are editorial previews; tap through for the durable article. */
.news-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 15px; }
.news-card { min-width: 0; overflow: hidden; cursor: pointer; background: var(--surface); border: 1px solid var(--border);
  border-radius: 23px; box-shadow: var(--shadow); transition: transform .16s ease, border-color .16s ease; }
.news-card:hover { transform: translateY(-1px); border-color: color-mix(in srgb, var(--accent) 40%, var(--border)); }
.news-card.draft { border-style: dashed; }
.news-cover { aspect-ratio: 16/8.6; overflow: hidden; background: var(--surface-2); }
/* Card thumbnails keep a fixed frame so the grid stays even. `fill` (default)
   crops a differently-shaped cover to that frame; `fit` letterboxes the whole
   picture inside it, for posters whose art must not lose its edges (D4). */
.news-cover img { width: 100%; height: 100%; object-fit: cover; display: block; }
.news-cover.fit img { object-fit: contain; }
.news-cover-fallback { display: grid; place-items: center; background: linear-gradient(135deg,
  color-mix(in srgb, var(--accent) 24%, var(--surface-2)), color-mix(in srgb, var(--mozz) 32%, var(--surface-2))); }
.news-cover-fallback svg { width: 34px; height: 34px; color: var(--ink); opacity: .65; }
.news-card-copy { padding: 15px 16px 14px; }
.news-meta { display: flex; align-items: center; gap: 7px; min-height: 21px; }
.news-category, .news-status { display: inline-flex; align-items: center; width: fit-content; border-radius: 999px;
  padding: 4px 9px; background: color-mix(in srgb, var(--mozz) 28%, var(--surface-2)); color: var(--ink);
  font: 800 10.5px/1 var(--font-round); letter-spacing: .02em; }
.news-status { background: var(--surface-2); color: var(--muted); border: 1px dashed var(--border); }
.news-card h3 { margin: 8px 0 6px; color: var(--ink); font: 900 20px/1.18 var(--font-round); letter-spacing: -.02em; }
.news-card p { margin: 0; color: var(--muted); font-size: 13px; line-height: 1.45; display: -webkit-box;
  -webkit-box-orient: vertical; -webkit-line-clamp: 3; overflow: hidden; }
.news-byline { display: flex; align-items: center; gap: 5px; margin-top: 12px; color: var(--muted); font-size: 10.5px; }
.news-byline svg { width: 13px; height: 13px; }
.news-empty { grid-column: 1/-1; text-align: center; padding: 52px 20px; border: 1px dashed var(--border);
  border-radius: 23px; background: var(--surface); }
.news-empty > span { display: inline-grid; place-items: center; width: 46px; height: 46px; border-radius: 50%;
  color: var(--warn); background: color-mix(in srgb, var(--mozz) 28%, var(--surface-2)); }
.news-empty > span svg { width: 24px; height: 24px; }
.news-empty h3 { margin: 9px 0 4px; font-family: var(--font-round); }
.news-empty p { margin: 0; color: var(--muted); }

.news-detail { max-width: 780px; margin: 0 auto 30px; }
.news-back { display: inline-flex; align-items: center; gap: 5px; border: 0; background: none; color: var(--muted);
  cursor: pointer; padding: 3px 0 12px; font: 800 12.5px/1 var(--font-round); }
.news-back svg { width: 16px; height: 16px; }
/* D4: the article hero NEVER crops. No aspect-ratio box — the image keeps its
   natural proportions and the container takes its height, capped so a very tall
   picture cannot push the headline off the first screen. min-height reserves
   space while the authenticated bytes are still loading (the img starts with no
   src), so the copy below does not jump. `contain` only matters when one of
   those bounds binds; it letterboxes instead of cutting. */
.news-hero { overflow: hidden; border-radius: 25px; background: var(--surface-2); box-shadow: var(--shadow); }
.news-hero img { width: 100%; height: auto; min-height: 140px; max-height: min(72vh, 620px);
  object-fit: contain; display: block; }
.news-detail-copy { max-width: 690px; margin: 0 auto; padding: 24px 10px 0; }
.news-detail h1 { margin: 9px 0 0; color: var(--ink); font: 900 clamp(29px, 5vw, 43px)/1.06 var(--font-round);
  letter-spacing: -.035em; }
.news-detail .news-byline { font-size: 12px; margin-top: 12px; }
.news-deck { margin: 22px 0 0; color: var(--ink); font: 700 17px/1.5 var(--font-round); }
.news-body { margin-top: 24px; color: var(--ink); font-size: 16px; line-height: 1.75; word-break: break-word; }
/* Pictures inside the body follow the hero's rule: full width, natural shape,
   never cropped. Tapping one opens the shared photo viewer. */
.news-inline-figure { margin: 22px 0; border-radius: 18px; overflow: hidden; background: var(--surface-2); }
.news-inline-figure img { width: 100%; height: auto; max-height: min(70vh, 560px);
  object-fit: contain; display: block; cursor: zoom-in; }
.news-engagement { display: flex; align-items: center; gap: 14px; margin-top: 30px; padding-top: 17px;
  border-top: 1px solid var(--border); color: var(--muted); font-size: 12px; }
.news-engagement > span { display: inline-flex; align-items: center; gap: 5px; }
.news-engagement svg { width: 16px; height: 16px; }
.news-ack { display: inline-flex; align-items: center; gap: 6px; border: 1px solid var(--border); border-radius: 999px;
  padding: 8px 12px; background: var(--surface); color: var(--muted); cursor: pointer; font-weight: 800; }
.news-ack.on { color: var(--basil); border-color: color-mix(in srgb, var(--basil) 40%, var(--border)); }
.news-owner-actions { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 20px; }
.news-detail-loading { padding: 60px 0; text-align: center; color: var(--muted); }
/* ── Article block editor ──────────────────────────────────────────────────
   The body is edited as a stack of paragraph and picture blocks. Every block
   carries up/down/remove BUTTONS as the guaranteed reordering path; the drag
   handle is a convenience on top of it. The handle and the buttons sit inside
   the bar's own horizontal padding rather than against the sheet edge, because
   Android's system navigation owns both screen edges outright. */
.news-blocks { display: flex; flex-direction: column; gap: 10px; margin-top: 4px; }
.news-block { border: 1px solid var(--border); border-radius: 15px; background: var(--surface);
  overflow: hidden; }
.news-block.dragging { border-color: color-mix(in srgb, var(--accent) 45%, var(--border));
  box-shadow: var(--shadow); opacity: .92; }
.news-block-bar { display: flex; align-items: center; gap: 4px; padding: 5px 12px;
  background: var(--surface-2); border-bottom: 1px solid var(--border); }
.news-block-kind { color: var(--muted); font: 800 10.5px/1 var(--font-round);
  letter-spacing: .06em; text-transform: uppercase; }
.news-block-grip, .news-block-btn { display: inline-grid; place-items: center; flex: none;
  width: 30px; height: 30px; border: 0; border-radius: 9px; background: transparent;
  color: var(--muted); cursor: pointer; }
.news-block-grip { touch-action: none; cursor: grab; margin-right: 2px; }
.news-block.dragging .news-block-grip { cursor: grabbing; }
.news-block-grip svg, .news-block-btn svg { width: 17px; height: 17px; }
.news-block-btn:hover:not([disabled]), .news-block-grip:hover { background: var(--surface-3); color: var(--ink); }
.news-block-btn[disabled] { opacity: .3; cursor: default; }
.news-block-btn.danger { color: var(--bad); }
.news-block-text { display: block; width: 100%; min-height: 96px; padding: 12px;
  border: 0; border-radius: 0; background: transparent; color: var(--ink);
  font: 400 15px/1.55 var(--font); resize: vertical; }
.news-block-text:focus { outline: 2px solid color-mix(in srgb, var(--accent) 35%, transparent);
  outline-offset: -2px; }
.news-block-picture { background: var(--surface-2); }
.news-block-picture img { display: block; width: 100%; height: auto;
  max-height: min(46vh, 340px); object-fit: contain; }
.news-block-picture.missing { display: grid; place-items: center; min-height: 74px;
  padding: 16px; color: var(--muted); font-size: 12.5px; }
.news-block-tools { display: flex; align-items: stretch; gap: 8px; margin-top: 10px; }
/* margin:0 matters — one of these is a <label>, which the shell gives a
   12px/5px block margin, and the other is a <button>, which it does not. */
.news-block-tool { display: inline-flex; align-items: center; justify-content: center; gap: 7px;
  flex: 1; min-width: 0; margin: 0; padding: 10px; border: 1px dashed var(--border);
  border-radius: 14px; background: var(--surface-2); color: var(--ink); cursor: pointer;
  font: 800 13px/1 var(--font-round); }
.news-block-tool svg { width: 16px; height: 16px; }
.news-note { margin-top: 8px; color: var(--muted); font-size: 12px; line-height: 1.4; }
.news-note:empty { display: none; }
.news-unplaced { margin-top: 12px; }
.news-unplaced-label { display: block; color: var(--muted); font: 800 10.5px/1 var(--font-round);
  letter-spacing: .06em; text-transform: uppercase; }
.news-image-place { position: absolute; bottom: 3px; right: 3px; width: 21px; height: 21px; border: 0;
  border-radius: 50%; cursor: pointer; background: color-mix(in srgb, var(--ink) 62%, transparent);
  color: var(--surface); font: 800 14px/1 var(--font-round); }
.news-cover-pick { display: flex; align-items: center; justify-content: center; gap: 8px; padding: 11px;
  margin-top: 12px; border: 1px dashed var(--border); border-radius: 15px; background: var(--surface-2);
  cursor: pointer; color: var(--ink); font-weight: 800; }
.news-cover-pick svg { width: 18px; height: 18px; }
/* Pictures stored on the article but not placed in the body, each with a
   "place it" control and a remove control. */
.news-image-strip { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 8px; }
.news-image-chip { position: relative; width: 74px; height: 74px; border-radius: 13px; overflow: hidden;
  border: 1px solid var(--border); background: var(--surface-2); }
.news-image-chip img { width: 100%; height: 100%; object-fit: cover; display: block; }
.news-image-remove { position: absolute; top: 3px; right: 3px; width: 21px; height: 21px; border: 0;
  border-radius: 50%; cursor: pointer; background: color-mix(in srgb, var(--ink) 62%, transparent);
  color: var(--surface); font: 800 14px/1 var(--font-round); }
.news-editor-actions { display: grid; grid-template-columns: 1fr 1.4fr; gap: 8px; }

/* Feed's Everyone thread is nested below the section switcher. Let the nested
   host, rather than only #view, participate in the fixed-composer flex shell. */
main.chat-mode #view .org-feed-shell { flex: 1; min-height: 0; display: flex; flex-direction: column; max-width: none; }
main.chat-mode #view .org-feed-head, main.chat-mode #view .feed-segments { flex: none; }
main.chat-mode #view .feed-segments { position: static; margin: 0 14px 6px; }
main.chat-mode #view .org-feed-head { padding: 7px 14px 8px; }
main.chat-mode #view .feed-section.thread-host { flex: 1; min-height: 0; display: flex; flex-direction: column; }
main.chat-mode #view .feed-section.thread-host .card.thread { flex: 1; min-height: 0; overflow-y: auto;
  display: flex; flex-direction: column; padding: 4px 14px 6px; }
main.chat-mode #view .feed-section.thread-host .composer { position: static; margin: 0; }

.notif-row .nic-activity { background: color-mix(in srgb, var(--accent) 18%, var(--surface-2)); color: var(--accent); }
.notif-row .nic-news { background: color-mix(in srgb, var(--mozz) 30%, var(--surface-2)); color: var(--warn); }

@media (max-width: 650px) {
  .org-feed-head { padding-left: 3px; padding-right: 3px; }
  .activity-intro, .news-intro { align-items: flex-start; }
  .activity-intro p, .news-intro p p { font-size: 12.5px; }
  .activity-intro .soft-action, .news-intro .soft-action .soft-action { padding: 8px 10px; font-size: 11.5px; }
  .news-grid { grid-template-columns: 1fr; }
  .news-card { border-radius: 20px; }
  .news-detail-copy { padding: 20px 3px 0; }
  .news-hero { border-radius: 20px; }
  .news-editor-actions { grid-template-columns: 1fr; }
}

/* ══════════════════ SWIPE BACK (left-edge gesture) ══════════════════════════
   A transient layer appended to <body>, never inside <main>. main computes
   overflow-x:auto, so translating anything inside it would open a real
   horizontal scrollbar; and #view must stay unique and unmoved for the pane
   ownership contract. Both the scrim and the edge hint read one custom property
   that JS writes as the drag progresses, so there is no per-frame style thrash.
   Sits under the photo viewer (80) and over the sheet backdrop (30). */
.navback-layer {
  position: fixed; inset: 0; z-index: 35; pointer-events: none;
  --navback-p: 0;
}
.navback-scrim {
  position: absolute; inset: 0;
  background: color-mix(in srgb, var(--ink) 22%, transparent);
  opacity: calc(var(--navback-p) * 0.6);
}
/* A slim bar that grows out of the edge — enough to say "something is happening
   and you can let go", without pretending to reveal a screen we have not
   rendered yet. Committing early to reveal the destination is what produces
   double navigations, so the drag deliberately has nothing behind it. */
.navback-hint {
  position: absolute; top: 0; bottom: 0; left: 0;
  width: calc(4px + var(--navback-p) * 44px);
  background: linear-gradient(to right,
    color-mix(in srgb, var(--accent) 70%, transparent),
    transparent);
  opacity: calc(0.25 + var(--navback-p) * 0.75);
}
/* The committed slide: a clone of the outgoing pane, id-stripped so it can never
   collide with #view, sliding out while the real pane re-renders underneath. */
.navback-snap {
  position: fixed; z-index: 36; pointer-events: none; overflow: hidden;
  background: var(--bg);
  will-change: transform;
  transition: transform .22s cubic-bezier(.22,.61,.36,1), opacity .22s linear;
}
.navback-snap.out { transform: translateX(100%); opacity: .35; }
/* Reduced motion keeps the affordance and drops the travel — the gesture must
   still be legible, it just must not animate. */
@media (prefers-reduced-motion: reduce) {
  .navback-snap { transition: none; }
  .navback-hint { transition: none; }
}

/* ══════════════════════════════ GAME ROOM ═══════════════════════════════════
   The play field lives in the modal sheet, and everything here is flow-laid and
   unpositioned: nothing is fixed, absolute, or wider than its parent, so the game
   can never be the thing that opens a horizontal scrollbar on main.
   GameRoom.mount() creates .game-canvas-wrap itself and appends it to the host it is
   given (.game-host in the sheet), sizing the canvas in px from host.clientWidth — so
   these rules decide how it is framed and how WIDE the host is, never the canvas size
   directly. destroy() removes the wrap, which is why the frame belongs on the wrap and
   nothing else lives in it. */
/* The host's horizontal margin is a gesture guard, not decoration, and it is a MARGIN
   rather than padding for a mechanical reason: clientWidth includes padding, so padding
   here would not shrink the canvas the JS derives from it — the canvas would simply
   overflow the frame and get clipped.
   Measured, the canvas's left edge sat at x=18 with the sheet's own 17px padding, inside
   app.js's NAVBACK_EDGE_PX strip (a pointerdown at clientX <= 24 starts an edge-back
   gesture). So the leftmost ~6px of the play field could swipe the sheet away instead of
   jumping. 10px puts the first playable pixel at ~28px, clear of the strip on both sides
   with a little slack, and costs 20px of a field that is width-limited anyway.
   Nothing reserves the host's height on purpose: a mounted game fills it synchronously,
   and the empty-host-behind-a-dead-sheet case is fixed where it belonged — stopGameRoom()
   now closes the sheet — rather than by holding blank space open for it. */
.game-host { margin: 0 10px 6px; }
.game-canvas-wrap {
  background: var(--surface-2); border-radius: var(--radius-sm);
  display: grid; place-items: center;
  /* The frame line is an INSET ring, not a border: the canvas is sized in JS from the
     host's width, so a 1px border would shrink the wrap's content box below the canvas
     and clip a hairline off both ends of the play field. A ring costs no layout. */
  box-shadow: inset 0 0 0 1px var(--border);
  /* The canvas floors its own width at 280px, which is the one case it can exceed a
     narrow host. Clip rather than overflow: main computes overflow-x, so the
     alternative is a real horizontal scrollbar on the whole app. */
  overflow: hidden;
}
.game-canvas {
  display: block;            /* inline canvas otherwise sits on a text baseline */
  /* The runner is a tap-per-jump game inside a scrollable surface. Without this the
     OS claims the pointer stream for scroll/zoom arbitration mid-press and the jump
     is simply lost — the same class of failure as the WKWebView swipe. */
  touch-action: none; user-select: none; -webkit-user-select: none;
  -webkit-touch-callout: none;
}
/* The result line and the error line sit in the tightest part of the sheet — between the
   play field and the bests line, above the fold at 390×844. Reserving a line for each
   before a run has ever ended cost a measured 51px of blank space there (dashboard.css
   gives .err a 16px floor and an 8px top margin of its own), which pushed the board down
   for nothing. So each one reserves its line only once it has something to say:
   setGameNote() writes textContent, so clearing it makes the element :empty again and the
   space goes back. This lives here rather than inline in feed.js precisely so that :empty
   can win — an inline min-height would outrank it. */
.game-note { margin: 6px 0 2px; font-size: 13.5px; min-height: 19px; }
[data-game-sheet] .game-note:empty { margin: 0; min-height: 0; }
[data-game-sheet] .err:empty { margin-top: 0; min-height: 0; }

/* Leaderboard row. gameBoardHtml() in feed.js sets the row's own flex layout, tint and
   truncation inline, so this deliberately carries only what that markup leaves to the
   cascade: twenty rows in a sheet want to be smaller than the 15px body copy the modal
   otherwise inherits. Nothing here competes with the inline styles — an inline
   declaration wins regardless, and a rule that half-loses is worse than no rule. */
.game-row { font-size: 13.5px; }


/* ───────────────  SHIFT COVER ASK — the card inside a DM  ─────────────────
   Somebody asking one named teammate to take a shift (Cole, 2026-08-15). Built
   on the poll card's vocabulary rather than a second one: same surface, same
   border, same radius, so a thread never has two dialects of "message with
   controls in it". */
.cover-card {
  background: var(--surface); border: 1px solid var(--border);
  border-radius: 16px; padding: 12px 13px;
}
/* Your own ask, tinted the way your own bubbles are — you are looking at
   something you sent, and it should not read as a request aimed at you. */
.msg.own .cover-card { background: color-mix(in srgb, var(--accent) 8%, var(--surface)); }
.cover-head {
  display: flex; align-items: center; gap: 7px; font-size: 11px; font-weight: 800;
  letter-spacing: .06em; text-transform: uppercase; color: var(--muted); margin-bottom: 9px;
}
.cover-head svg { width: 14px; height: 14px; flex: none; }
.cover-when {
  font-family: var(--font-round); font-weight: 900; font-size: 16px;
  line-height: 1.25; color: var(--ink);
}
.cover-where { font-size: 12.5px; color: var(--muted); margin-top: 2px; }
.cover-actions { display: flex; gap: 8px; margin-top: 12px; }
.cover-actions button {
  flex: 1; border-radius: 999px; padding: 9px 10px; font: inherit; font-size: 13.5px;
  font-weight: 800; cursor: pointer; border: 1px solid var(--border);
}
/* Green, not the app accent. The accent is the colour of "post", "send",
   "new" — the things you do to yourself. Taking somebody else's shift is a
   favour and reads better as a distinct, affirmative act. */
.cover-yes { background: var(--ok); color: #fff; border-color: transparent; }
.cover-no { background: var(--surface-2); color: var(--ink); }
.cover-actions button[disabled] { opacity: .55; cursor: default; }
.cover-state {
  display: flex; align-items: center; gap: 7px; margin-top: 11px; padding-top: 10px;
  border-top: 1px solid var(--hair); font-size: 12.5px; font-weight: 700; line-height: 1.35;
}
.cover-state svg { width: 15px; height: 15px; flex: none; }
.cover-state.yes { color: var(--ok); }
.cover-state.no { color: var(--muted); }
