/**
 * NUKE – Main Stylesheet
 * Dark theme · Monospace typography
 *
 * Grayscale reference:
 *   gray-1  #0e0e0e  Body/content background
 *   gray-2  #1a1a1a  Cards, inputs, raised surfaces
 *   gray-3  #2a2a2a  Borders, hover states, elevated surfaces
 *   gray-4  #bdbdbd  Bright text on dark (nav links)
 *   gray-5  #9e9e9e  Muted text (timestamps, hints)
 *   gray-6  #aaaaaa  Secondary text (descriptions, labels)
 *   gray-7  #333333  Strong borders, button hover
 *   gray-8  #252525  Button surfaces, interactive elements
 *   gray-9  #181818  Header, deep surfaces
 */

/* GLOBAL RESET — REQUIRED: Tailwind preflight is disabled on this page, so styles.css must supply this.
   Without it the UA default 8px body margin adds stray height/width below the 100vh/100vw .nuke-container
   (→ phantom vertical + horizontal scrollbars), and content-box sizing throws off every fixed-width panel
   (the right column no longer lines up with the search bar). Dropped in a refactor; restored. */
*, *::before, *::after {
  box-sizing: border-box;
  margin: 0;
  padding: 0;
}

/* ===========================================
   7. ONBOARDING — full-page x.com-style sign-up (two-column), nuke tokens
   =========================================== */
.onboarding-container {
  max-width: none;
  width: 100%;
  min-height: 100vh;
  display: flex;
  align-items: center;
  gap: 4vw;
  /* Signed-out has no viewer hue, so THEME the page to the site accent (X-blue): the beveled card, its
     bevels, and the primary button all resolve their oklch() tints from this — matching a blue site user. */
  --current-user-color: #1D9BF0;
  /* Full-bleed themed page + the site's brilliant fine-grain texture (matches body.page-nuke). */
  background-color: var(--nk-surface);
  background-image: url('/img/ui/brilliant.png');
  color: var(--nk-text);
  font-family: var(--font-conduit, 'Sunghyun Sans', -apple-system, sans-serif);
}

/* LEFT — content column, left-aligned like x.com. */
.onboarding-left {
  flex: 0 1 440px;
  box-sizing: border-box;
  margin-left: clamp(28px, 11vw, 240px);
  padding: 24px 0;
}

/* All onboarding content lives in ONE squared, beveled themed card — the SAME recipe as the right-panel
   Contribute container + nav items: SETTING-1 fill in the (accent) hue, brilliant texture + edge-shading,
   white top/left + darker themed right/bottom bevel, inset themed edges, and a hard 4px drop shadow. */
.onboarding-card {
  box-sizing: border-box;
  width: 100%;
  padding: 26px 26px 22px;
  border-radius: 0;
  --card-fill: oklch(from var(--current-user-color) var(--set-1-l) var(--set-1-c) h);
  background-color: var(--card-fill, var(--nk-surface));
  background-image:
    url('/img/ui/brilliant.png'),
    linear-gradient(to bottom, rgba(var(--nk-surface-rgb), 0.7) 0%, transparent 6px),
    linear-gradient(to bottom, rgba(0, 0, 0, 0.04) 0%, transparent 6px),
    linear-gradient(to top, rgba(0, 0, 0, 0.04) 0%, transparent 18px),
    linear-gradient(105deg, rgba(0, 0, 0, 0.04) 0%, transparent 12px),
    linear-gradient(255deg, rgba(0, 0, 0, 0.04) 0%, transparent 12px);
  --hover-edge-raw: oklch(from var(--current-user-color) var(--set-3-l) var(--set-3-c) h);
  --hover-edge: var(--hover-edge-raw, var(--nk-border-plain));
  --hover-edge-dark: oklch(from var(--hover-edge) calc(l * 0.8) c h);
  border: 2px solid #FFFFFF;
  border-right: 2px solid var(--hover-edge-dark);
  border-bottom: 2px solid var(--hover-edge-dark);
  box-shadow:
    inset 2px 0 0 0 var(--hover-edge),
    inset -2px 0 0 0 var(--hover-edge),
    inset 0 2px 0 0 var(--hover-edge),
    inset 0 -2px 0 0 var(--hover-edge),
    4px 4px 0 0 rgba(0, 0, 0, 0.2);
}

/* RIGHT — giant brand mark, filling the rest of the page. */
.onboarding-brand {
  flex: 1 1 auto;
  min-width: 0;
  display: flex;
  align-items: center;
  justify-content: center;
}
.onboarding-logo {
  color: var(--nk-text);
  line-height: 1;
  font-size: min(64vh, 38vw);
}

.onboarding-heading {
  font-family: var(--font-conduit, 'Sunghyun Sans', -apple-system, sans-serif);
  font-size: clamp(26px, 3.4vw, 34px);
  font-weight: 900;
  line-height: 1.1;
  letter-spacing: -0.02em;
  color: var(--nk-text);
  margin: 0 0 12px;
}

.onboarding-sub {
  font-size: calc(15px - var(--font-shrink, 0px));
  line-height: 1.5;
  color: var(--nk-text-2);
  margin: 0 0 20px;
  max-width: none;
}
.onboarding-sub a { color: var(--nk-accent); font-weight: 700; text-decoration: none; }
.onboarding-sub a:hover { text-decoration: underline; }

/* Shown only when migrating an existing account (JS toggles .hidden). */
.onboarding-migrate { font-size: calc(16px - var(--font-shrink, 0px)); font-weight: 700; color: var(--nk-accent); margin: 0 0 12px; }

/* Pairing code — a monospace chip you tap to copy. */
.onboarding-code {
  font-family: var(--font-mono, ui-monospace, SFMono-Regular, monospace);
  font-size: calc(22px - var(--font-shrink, 0px));
  font-weight: 700;
  letter-spacing: 0.16em;
  color: var(--nk-text);
  /* Recessed field: white bg, squared, INSET top/left shadow — reads as carved into the card. */
  background: var(--nk-surface);
  border: 1px solid var(--hover-edge, var(--nk-border));
  border-radius: 0;
  box-shadow: inset 2px 2px 0 0 rgba(0, 0, 0, 0.06);
  padding: 14px 16px;
  margin: 0 0 20px;
  max-width: none;
  cursor: pointer;
  user-select: all;
  transition: border-color 0.15s ease, transform 0.08s ease;
}
.onboarding-code:hover { border-color: var(--nk-accent); }
.onboarding-code:active { transform: scale(0.99); }

.onboarding-actions { margin: 0; max-width: none; }

/* Legal fine print under the button (x.com style). */
.onboarding-legal {
  margin-top: 16px;
  max-width: none;
  font-size: calc(13px - var(--font-shrink, 0px));
  line-height: 1.4;
  color: var(--nk-text-2);
}
.onboarding-legal a { color: var(--nk-text-2); font-weight: 700; text-decoration: none; }
.onboarding-legal a:hover { text-decoration: underline; }

/* Inline wallet chooser (replaces the sign-in intro in place). */
.onboarding-wallet-list { display: flex; flex-direction: column; gap: 10px; max-width: none; }
.onboarding-wallet-btn {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 10px;
  width: 100%;
  padding: 13px 20px;
  font-family: var(--font-conduit, 'Sunghyun Sans', -apple-system, sans-serif);
  font-size: calc(15px - var(--font-shrink, 0px));
  font-weight: 700;
  color: var(--nk-text);
  border-radius: 0;
  background-color: var(--nk-surface);
  border: 2px solid #FFFFFF;
  border-right: 2px solid var(--hover-edge-dark, var(--nk-border));
  border-bottom: 2px solid var(--hover-edge-dark, var(--nk-border));
  box-shadow:
    inset 2px 0 0 0 var(--hover-edge, var(--nk-border)),
    inset -2px 0 0 0 var(--hover-edge, var(--nk-border)),
    inset 0 2px 0 0 var(--hover-edge, var(--nk-border)),
    inset 0 -2px 0 0 var(--hover-edge, var(--nk-border)),
    3px 3px 0 0 rgba(0, 0, 0, 0.18);
  transition: transform 0.12s ease, box-shadow 0.12s ease;
}
.onboarding-wallet-btn:hover:not(:disabled) { transform: scale(1.02); }
.onboarding-wallet-btn:active:not(:disabled) {
  transform: scale(0.97);
  box-shadow:
    inset 2px 0 0 0 var(--hover-edge, var(--nk-border)),
    inset -2px 0 0 0 var(--hover-edge, var(--nk-border)),
    inset 0 2px 0 0 var(--hover-edge, var(--nk-border)),
    inset 0 -2px 0 0 var(--hover-edge, var(--nk-border)),
    1px 1px 0 0 rgba(0, 0, 0, 0.18);
}
.onboarding-wallet-btn:disabled { opacity: 0.6; cursor: default; }
.onboarding-wallet-icon { width: 24px; height: 24px; border-radius: 6px; flex: 0 0 auto; }

/* Big pill primary button — x.com sign-up shape, in the site accent (X-blue). */
.onboarding-btn {
  width: 100%;
  box-sizing: border-box;
  padding: 14px 24px;
  font-family: var(--font-conduit, 'Sunghyun Sans', -apple-system, sans-serif);
  font-size: calc(16px - var(--font-shrink, 0px));
  font-weight: 800;
  color: #fff;
  border-radius: 0;
  /* Saturated accent fill + a 3D bevel (lighter top/left, darker bottom/right of the SAME hue) — the
     chunky beveled button that matches the site's cards. */
  --btn-bg: oklch(from var(--current-user-color) 0.62 0.17 h);
  background-color: var(--btn-bg, var(--nk-accent));
  background-image:
    linear-gradient(to bottom, rgba(255, 255, 255, 0.10) 0%, transparent 50%),
    linear-gradient(to top, rgba(0, 0, 0, 0.10) 0%, transparent 12px);
  border: 4px solid;
  border-color:
    oklch(from var(--btn-bg, var(--nk-accent)) calc(l * 1.28) c h)
    oklch(from var(--btn-bg, var(--nk-accent)) calc(l * 0.72) c h)
    oklch(from var(--btn-bg, var(--nk-accent)) calc(l * 0.72) c h)
    oklch(from var(--btn-bg, var(--nk-accent)) calc(l * 1.28) c h);
  box-shadow: 4px 4px 0 0 rgba(0, 0, 0, 0.2);
  cursor: pointer;
  transition: transform 0.12s ease, box-shadow 0.12s ease, filter 0.15s ease;
}
.onboarding-btn:hover:not(:disabled) { transform: scale(1.02); filter: brightness(1.05); }
.onboarding-btn:active:not(:disabled) { transform: scale(0.97); box-shadow: 1px 1px 0 0 rgba(0, 0, 0, 0.2); }
.onboarding-btn:disabled { opacity: 0.55; cursor: default; }

.onboarding-error {
  margin-top: 16px;
  max-width: none;
  padding: 11px 14px;
  font-size: calc(14px - var(--font-shrink, 0px));
  line-height: 1.4;
  color: #b3261e;
  background: rgba(179, 38, 30, 0.08);
  border: 1px solid rgba(179, 38, 30, 0.3);
  border-radius: 0;
}

/* Mobile: stack — brand up top (smaller), content below. */
@media (max-width: 760px) {
  .onboarding-container { flex-direction: column-reverse; justify-content: center; gap: 12px; padding: 40px 20px; }
  .onboarding-left { flex: 0 0 auto; width: 100%; max-width: 420px; margin: 0 auto; padding: 0; }
  .onboarding-brand { flex: 0 0 auto; }
  .onboarding-logo { font-size: calc(96px - var(--font-shrink, 0px)); }
  .onboarding-heading { font-size: clamp(24px, 7vw, 32px); }
}

/* ── Shared modal system (compose / create-post / add-song / generic modals) ──────────
   RESTORED: a low-tier model deleted this whole block while removing the wallet-select
   modal, but these styles are shared by the compose (`.cred-compose-modal`) + generic
   (`.modal-*`) modals — the `.profile-modal-overlay` position:fixed is what makes them a
   centered overlay instead of inline HTML at the bottom of the page. */
/* Cred-compose modal — an X.com-style "compose" card, themed with the site tokens.
   Mounts on document.body, so it inherits the --nk-* tokens from body.page-nuke. */
.cred-compose-modal {
  box-sizing: border-box;
  width: min(440px, 92vw);
  display: flex;
  flex-direction: column;
  /* One even vertical rhythm between every row (title, textarea, count, attach, button) instead of
     a patchwork of per-element margins — kept seamless by zeroing those margins below. */
  gap: 12px;
  padding: 20px;
  background: var(--nk-surface, #FFFFFF);
  border: 2px solid var(--nk-border-soft, #EFF3F4);
  border-radius: 0;
  box-shadow: 4px 4px 0 0 rgba(0, 0, 0, 0.2); /* same hard-offset shadow as the right-side containers */
}
/* Open / close animation: the backdrop fades while the card rises + scales in. The close is driven by
   the .is-closing class (JS adds it, waits ~170ms for the out-anim, then removes the node). */
@keyframes nuke-compose-overlay-in { from { opacity: 0; } to { opacity: 1; } }
@keyframes nuke-compose-overlay-out { from { opacity: 1; } to { opacity: 0; } }
@keyframes nuke-compose-card-in {
  from { opacity: 0; transform: translateY(10px) scale(0.96); }
  to   { opacity: 1; transform: translateY(0) scale(1); }
}
@keyframes nuke-compose-card-out {
  from { opacity: 1; transform: translateY(0) scale(1); }
  to   { opacity: 0; transform: translateY(8px) scale(0.97); }
}
.profile-modal-overlay:has(.cred-compose-modal) {
  /* Sit the compose card higher up the page instead of vertically centered. */
  align-items: flex-start;
  padding-top: 6vh;
  animation: nuke-compose-overlay-in 0.16s ease both;
}
.cred-compose-modal { animation: nuke-compose-card-in 0.2s cubic-bezier(0.2, 0.8, 0.2, 1) both; }
.profile-modal-overlay.is-closing { animation: nuke-compose-overlay-out 0.16s ease both; pointer-events: none; }
.profile-modal-overlay.is-closing .cred-compose-modal { animation: nuke-compose-card-out 0.16s ease both; }
@media (prefers-reduced-motion: reduce) {
  .profile-modal-overlay:has(.cred-compose-modal),
  .cred-compose-modal,
  .profile-modal-overlay.is-closing,
  .profile-modal-overlay.is-closing .cred-compose-modal { animation-duration: 0.01ms; }
}
.cred-compose-modal .profile-modal-title {
  margin: 0;
  font-size: calc(20px - var(--font-shrink, 0px));
  font-weight: 800;
  color: var(--nk-text, #0F1419);
}
.cred-compose-modal .bio-modal-textarea {
  box-sizing: border-box;
  width: 100%;
  /* Height auto-grows to fit the typed content (JS: autoGrowTextarea) — one line when empty,
     up to this cap, past which it scrolls. No manual resize handle. */
  min-height: 44px;
  max-height: 40vh;
  overflow-y: auto;
  resize: none;
  padding: 12px 14px;
  border: 1px solid var(--nk-border, #CFD9DE);
  border-radius: 14px;
  background: var(--nk-surface, #FFFFFF);
  color: var(--nk-text, #0F1419);
  font-family: var(--font-conduit, "Sunghyun Sans", -apple-system, sans-serif); /* our site font for typed text */
  font-size: calc(15px - var(--font-shrink, 0px));
  line-height: 1.4;
  outline: none;
  transition: border-color 0.12s ease, box-shadow 0.12s ease;
}
.cred-compose-modal .bio-modal-textarea::placeholder { color: var(--nk-text-2, #536471); font-style: italic; font-family: var(--font-conduit, "Sunghyun Sans", -apple-system, sans-serif); }
.cred-compose-modal .bio-modal-textarea:focus {
  border-color: var(--nk-accent, #1D9BF0);
  box-shadow: 0 0 0 3px rgba(var(--nk-accent-rgb, 29, 155, 240), 0.16);
}
/* Self-post ("Post About Yourself") composer: match a feed post container's width (= --chat-w,
   600px) instead of the default 440px card. --chat-w lives on .nuke-container, out of reach for
   this body-level overlay, so the value is inlined here — keep the two in sync. */
.compose-selfpost {
  width: min(500px, 92vw);
  gap: 0; /* nothing has gaps — every row sits flush */
  padding: 10px 10px 9px; /* 9px bottom compensates the 2px bevel bottom border so the VISUAL gap is a uniform 11px all around */
  /* Look like a feed post card: author-tinted fill + the post bevel border (white 1px top, 2px coloured
     bottom, grey sides). --nuke-cmd-color is set inline to YOUR profile colour; colourless → the neutral
     --nk-hover-1 fill + --nk-border-plain. Tokens cascade from body.page-nuke. */
  --card-bg-themed: oklch(from var(--nuke-cmd-color) var(--set-2-l) var(--set-2-c) h);
  --card-bg: var(--card-bg-themed, var(--nk-hover-1));
  --card-border-themed: oklch(from var(--nuke-cmd-color) var(--set-3-l) var(--set-3-c) h);
  --card-border: var(--card-border-themed, var(--nk-border-plain));
  /* SETTING 6 icon hue (same as the post footer) so the attach icons + cred count are themed, not neutral. */
  --icon-themed: oklch(from var(--nuke-cmd-color) var(--set-6-l) var(--set-6-c) h);
  background: var(--card-bg);
  border-top: 1px solid #FFFFFF;
  border-bottom: 2px solid var(--card-border);
  border-left: 1px solid var(--nk-border-2);
  border-right: 1px solid var(--nk-border-soft);
  /* Side bevels retained as inset shadows so each side reads 2px EXACTLY like a post (1px frame border +
     1px inner bevel: white left, --card-border right). Plus the modal own float drop shadow. */
  --post-side-bevel: #FFFFFF;
  box-shadow: inset 1px 0 0 0 var(--post-side-bevel), inset -1px 0 0 0 var(--card-border), 4px 4px 0 0 rgba(0, 0, 0, 0.2); /* + right-side-container hard offset */
}
/* Self-post ("Post About Yourself") composer: the textarea is the top element — make it FULL-BLEED,
   flush to the very top + both side edges of the card (cancel the card's 20px top/side padding just
   for it; its siblings keep their inset), and borderless (no resting border, no focus ring). */
/* X.com compose layout: YOUR avatar top-left, the text input filling the rest of the row. */
.selfpost-compose-row {
  display: flex;
  gap: 12px;
  align-items: flex-start;
}
.selfpost-compose-pfp {
  flex-shrink: 0;
  width: 44px;
  height: 44px;
  /* Sharp square + 1px border, matching the post-container pfp (nuke pfps are un-rounded). */
  border-radius: 0;
  box-sizing: border-box;
  border: 1px solid var(--nk-border, #cfd9de);
  object-fit: cover;
  background: var(--nk-hover-1, #f0f3f4); /* placeholder tint while the pfp loads */
}
/* Avatar column: pfp on top, YOUR cred badge centred under it. */
.selfpost-pfp-col {
  flex-shrink: 0;
  align-self: flex-start;
  /* Vertical LEFT RAIL: pfp · cred badge · brush-type tools, centred on one axis. Width fits the widest
     child (the Clear button) so every item — pfp, cred, tools — shares the same centre. */
  min-width: 44px;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 6px;
}
.selfpost-compose-cred { display: flex; justify-content: center; }
/* Brush-type tools stack vertically in the rail, under the cred badge. */
.selfpost-pfp-col .selfpost-tool-row { flex-direction: column; width: 100%; align-items: center; }
/* Right column beside the rail: text input on top, doodle below. */
.selfpost-right-col { flex: 1; min-width: 0; display: flex; flex-direction: column; }
/* Cred pill inside the compose modal — the .nuke-container .nuke-post-vote rules dont reach this
   body-level modal, so replicate the feed cred badge here (tokens cascade from body.page-nuke; width
   is explicit since --nuke-chat-pfp-size is .nuke-container-only). */
.cred-compose-modal .nuke-post-vote {
  display: inline-flex; align-items: center; justify-content: center; flex-shrink: 0;
  box-sizing: border-box; height: 20px; width: 44px; padding: 0;
  background: var(--nk-surface-2);
  --cred-border-themed: oklch(from var(--nuke-cmd-color) var(--set-3-l) var(--set-3-c) h);
  border: 2px solid var(--cred-border-themed, var(--nk-border-plain));
  border-radius: 0;
}
.cred-compose-modal .nuke-post-vote-count {
  min-width: 1.4em; text-align: center;
  color: var(--icon-themed, var(--nk-text-2));
  font-family: var(--font-conduit); font-size: calc(12px - var(--font-shrink, 0px)); font-weight: 600;
}
.cred-compose-modal .nuke-post-vote[data-metal="0"] { background: #f83a44; border-color: #d42a33; }
.cred-compose-modal .nuke-post-vote[data-metal="0"] .nuke-post-vote-count { color: #ffc4c4; }
.cred-compose-modal .nuke-post-vote[data-metal]:not([data-metal="0"]) {
  background: var(--metal-bg); border-color: var(--metal-edge); box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.3);
}
.cred-compose-modal .nuke-post-vote[data-metal]:not([data-metal="0"]) .nuke-post-vote-count { color: var(--metal-ink); }
.cred-compose-modal .nuke-post-vote[data-metal="2"] { --metal-bg: linear-gradient(180deg, #e3a659, #f8c980 35%, #d68b3b 62%, #b47530); --metal-edge: #b47530; --metal-ink: #4a2b0d; }
.cred-compose-modal .nuke-post-vote[data-metal="3"] { --metal-bg: linear-gradient(180deg, #d5dde6, #f2f6fa 35%, #c6d0dc 62%, #abb7c4); --metal-edge: #abb7c4; --metal-ink: #222932; }
.cred-compose-modal .nuke-post-vote[data-metal="4"] { --metal-bg: linear-gradient(180deg, #f6cb2e, #ffe486 35%, #eec015 62%, #d5a212); --metal-edge: #d5a212; --metal-ink: #6b4e00; box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.4), 0 0 5px rgba(255, 215, 0, 0.5); }
.compose-selfpost .bio-modal-textarea {
  flex: 1;             /* fill the row beside the avatar */
  min-width: 0;
  width: auto;
  margin: 0;
  border: none;
  border-radius: 0;
  background: transparent; /* show the card (compose) background — matches the doodle canvas fill */
}
.compose-selfpost .bio-modal-textarea:focus {
  border-color: transparent;
  box-shadow: none;
}
.cred-compose-modal .bio-char-count {
  text-align: right;
  font-size: calc(12px - var(--font-shrink, 0px));
  color: var(--nk-text-2, #536471);
  font-family: var(--font-conduit, "Sunghyun Sans", -apple-system, sans-serif); /* our site font */
}
.cred-compose-modal .action-btn.primary {
  height: 34px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  border: none;
  border-radius: 9999px;
  background: var(--nk-accent, #1D9BF0);
  color: #FFFFFF;
  font-weight: 800;
  line-height: 1;
  /* Shared engine: the submit button is ALWAYS pinned to the bottom of the card, whatever content
     each modal stacks above it — so a modal's own layout can flex above while the action stays put. */
  margin-top: auto;
}
.cred-compose-modal .action-btn.primary:hover:not(:disabled) { background: #1A8CD8; }

/* Cred-compose modal: Like / Dislike segmented toggle (mounts on document.body, so it
   inherits the --nk-* tokens from body.page-nuke). */
.cred-compose-toggle {
  display: flex;
  gap: 8px;
}
.cred-compose-choice {
  flex: 1;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 6px;
  padding: 9px 10px;
  border: 1px solid var(--nk-border, #CFD9DE);
  border-radius: 9999px;
  background: transparent;
  color: var(--nk-text-2, #536471);
  font-family: inherit;
  font-weight: 700;
  font-size: calc(14px - var(--font-shrink, 0px));
  line-height: 1;
  cursor: pointer;
  transition: background 0.12s ease, border-color 0.12s ease, color 0.12s ease;
}
.cred-compose-choice i { font-size: calc(16px - var(--font-shrink, 0px)); }
.cred-compose-like.is-active { background: rgba(255, 69, 0, 0.12); border-color: #FF4500; color: #FF4500; }
.cred-compose-dislike.is-active { background: rgba(113, 147, 255, 0.18); border-color: #7193FF; color: #7193FF; }

/* Modals */
.modal-overlay,
.profile-modal-overlay {
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  bottom: 0;
  background: rgba(0, 0, 0, 0.7);
  display: flex;
  align-items: center;
  justify-content: center;
  z-index: 1000;
}
.modal-content {
  background: var(--color-gray-2);
  color: var(--color-white);
  border-radius: var(--border-radius);
  padding: var(--spacing-lg);
  max-width: 500px;
  width: 90%;
  max-height: 80vh;
  overflow-y: auto;
}
.modal-title {
  font-size: var(--font-size-xl);
  font-weight: 700;
  margin-bottom: var(--spacing);
}
.modal-actions {
  display: flex;
  gap: var(--spacing);
  margin-top: var(--spacing-lg);
}
.modal-btn {
  flex: 1;
  padding: 12px;
  font-weight: 700;
  border-radius: var(--border-radius);
  transition: all 0.2s;
}
.modal-btn.primary { background: var(--color-nuke-primary); color: var(--color-white); }
.modal-btn.secondary { background: var(--color-gray-7); color: var(--color-white); }

body.page-chat .content {
  padding: 0;
  overflow: hidden;
}

.chat-pill-header .dock-pill {
  cursor: grab;
}
.chat-pill-header.dragging .dock-pill {
  cursor: grabbing;
}

@media (hover: hover) {
  .chat-pill-nuke-time.nuke-time-day:hover {
    transform: translateY(-1.5px) rotate(0deg) scale(1.01);
    /* Match .cc-entry:hover — profile color blended into gray-6, same
       as the search bar / notif button / dropdown rings. */
    --cc-outer-border-color: color-mix(in srgb, var(--current-user-color, #6a6a6a) 45%, var(--color-gray-6));
  }
  .chat-pill-nuke-time.nuke-time-day:hover::before {
    border-color: color-mix(in srgb, var(--current-user-color, #6a6a6a) 45%, var(--color-gray-6));
  }
}

.chat-secondary-header > .user-search-wrapper {
  order: 2;
  margin-left: auto;
  margin-right: auto;
}

.chat-secondary-header > .notif-wrapper {
  order: 5;
}

body.is-mobile #chat-secondary-header-card .cc-pfp-wrapper {
  width: 30px;
  height: 30px;
  margin: 3px;
}

body.is-mobile #chat-secondary-header-card .cc-pfp {
  width: 30px;
  height: 30px;
}

.notif-wrapper {
  position: relative;
  display: inline-flex;
}

/* ============ Header user-search dropdown ============ */

/* Wrapper carries the chrome (bg, outer ring, drop shadow). Motion
   stays on the input so it doesn't drag the dropdown around. */
/* Wrapper is just a positioning container — it holds the bar and the
   dropdown as siblings (mirrors `.notif-wrapper` → button + popover).
   No chrome, no transform. The dropdown lives outside the bar's
   transformed coordinate space, so it stays put when the bar lifts. */
.user-search-wrapper {
  position: relative;
  display: inline-flex;
  flex-shrink: 0;
}

/* The visual search bar — owns the chrome (bg, ring, drop shadow) and
   the lift/scale transform. Input + glyph sit inside; both follow the
   transform via DOM nesting. */
.user-search-bar {
  position: relative;
  display: flex;
  width: 423px;
  height: 45px;
  border-radius: 6px;
  background: var(--nk-surface);
  /* Rest ring matches .cc-entry's rest ring (gray-7). */
  --cc-outer-border-color: var(--color-gray-7);
  box-shadow:
    0 0 0 1px var(--cc-outer-border-color),
    var(--cc-shadow-rest),
    var(--soft-ambient-shadow);
  transform: translateY(var(--cc-lift)) scale(var(--cc-press));
  transition:
    --cc-lift 0.18s cubic-bezier(0.22, 1, 0.36, 1),
    --cc-press 0.2s cubic-bezier(0.34, 1.4, 0.64, 1),
    box-shadow 0.04s ease;
}
/* Search input + placeholder are styled in ONE place — the scoped
   `.nuke-container .nuke-search-row .user-search-input` rule (and its ::placeholder)
   further down. A loose `.user-search-input` rule here loses to that on specificity, so
   edits to it silently no-op — do NOT re-add one. */

.user-search-bar:hover,
.user-search-bar:focus-within {
  --cc-lift: -1.5px;
  --cc-press: 1.01;
  /* Same desaturated profile-color tint .cc-entry:hover uses, just keyed
     to the logged-in user (--current-user-color). */
  --cc-outer-border-color: color-mix(in srgb, var(--current-user-color, #6a6a6a) 45%, var(--color-gray-6));
}
/* Keep the bar's outer ring colored while the cursor is in the dropdown
   — moving from the bar into the dropdown would otherwise drop `:hover`
   on the bar and snap the ring back to gray. Only the ring carries
   over (no lift/scale) so the bar itself stays put. */
.user-search-wrapper:has(.user-search-dropdown:hover) .user-search-bar {
  --cc-outer-border-color: color-mix(in srgb, var(--current-user-color, #6a6a6a) 45%, var(--color-gray-6));
}
.user-search-bar:has(.cc-pressed) {
  --cc-press: 0.99;
  --cc-lift: 0px;
}

/* Glyph sits inside the search bar (its parent `.user-search-wrapper`
   carries the bg/ring/lift+scale transform). The button itself is
   transparent; the inner span carries the chrome. The wrapper's
   transform composes onto the glyph via DOM nesting — no per-element
   motion replication. */
.user-search-glyph {
  position: absolute;
  top: 50%;
  right: 4px;
  width: 37px;
  height: 37px;
  padding: 0;
  margin: 0;
  background: transparent;
  border: none;
  cursor: pointer;
  z-index: 2;
  transform: translateY(-50%);
  color: var(--color-text);
}
.user-search-glyph .chat-secondary-action-inner {
  width: 100%;
  height: 100%;
}

.user-search-input::-webkit-search-cancel-button {
  -webkit-appearance: none;
  appearance: none;
}

/* Dropdown opens only when there's a typed query AND either the input is
   focused OR the wrapper is hovered. Hovering the wrapper covers both
   hovering the bar and hovering the dropdown (it's a descendant), so
   moving the cursor from bar → dropdown keeps it open after blur. */

.user-search-list {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 8px;
  width: 100%;
}

.user-search-row {
  position: relative;
  display: flex;
  align-items: center;
  gap: 8px;
  width: 100%;
}

/* When the dropdown is closed (no typed query), keep all rows hidden so
   the closing fade doesn't flash an unfiltered list mid-transition. */
.user-search-wrapper:not(.has-query) .user-search-row {
  display: none;
}

/* User-search dropdown row width override. Base design lives in
   CALLING CARD DESIGN SYSTEM (~line 2796). */
.user-search-list .cc-entry {
  width: 240px;
  flex-shrink: 0;
}

/* Standalone online dot, sibling to the cc-entry — only the user-search
   dropdown shows this. Anchors to the row's top-left corner so it sits
   just outside the pfp without being a child of `.cc-pfp-wrapper`. */
.user-search-online-dot {
  position: absolute;
  top: -2px;
  left: -2px;
  width: 8px;
  height: 8px;
  z-index: 4;
  pointer-events: none;
}

.user-search-empty {
  padding: 16px;
  text-align: center;
  color: var(--color-text-muted);
  font-size: var(--font-size-sm);
}

/* Spotlight overlay — desktop only. When hovering the notification button
   or the header calling card, the rest of the page dims slightly while the
   hovered surface (and only that surface) stays bright. */
.hover-spotlight {
  position: fixed;
  inset: 0;
  background: rgba(0, 0, 0, 0.3);
  pointer-events: none;
  opacity: 0;
  transition: opacity 0.18s ease;
  z-index: 90;
}

body.is-mobile .hover-spotlight {
  display: none;
}

body.spotlight-notif .hover-spotlight,
body.spotlight-profile .hover-spotlight,
body.spotlight-search .hover-spotlight {
  opacity: 1;
}

/* Lift only the spotlighted surface above the overlay. The notif popover
   already lives at z-index 200 so it stays above the dim regardless. */
body.spotlight-notif .notif-wrapper {
  position: relative;
  z-index: 95;
}

body.spotlight-search .user-search-wrapper {
  position: relative;
  z-index: 95;
}

.notif-popover {
  position: absolute;
  top: calc(100% + 8px);
  left: auto;
  right: 0;
  width: 300px;
  /* Same cap as the search dropdown — ends at the chat container's
     bottom edge. See `.user-search-dropdown` for the offset breakdown. */
  max-height: calc(100dvh - 116px);
  overflow-y: auto;
  scrollbar-width: none;
  -ms-overflow-style: none;
  /* Background + 1px ring mirror `.user-search-bar:hover`; drop shadow
     is stronger than the bar's so the floating panel reads as elevated. */
  background: var(--color-gray-10);
  border-radius: 6px;
  box-shadow:
    0 0 0 1px color-mix(in srgb, var(--current-user-color, #6a6a6a) 45%, var(--color-gray-6)),
    0 12px 32px rgba(0, 0, 0, 0.5);
  padding: 12px 8px;
  opacity: 0;
  visibility: hidden;
  z-index: 200;
}

.notif-popover::-webkit-scrollbar {
  display: none;
}

.notif-wrapper:hover .notif-popover,
.notif-popover:hover {
  opacity: 1;
  visibility: visible;
}

.notif-popover-list {
  display: flex;
  flex-direction: column;
  gap: 8px;
}

.notif-group-header {
  font-size: calc(13px - var(--font-shrink, 0px));
  font-weight: 700;
  color: var(--color-black);
  padding: 6px 4px 2px;
  /* First header has no top space; subsequent headers gain a small gap
     above to separate from the previous group. */
  margin-top: 4px;
}
.notif-group-header:first-child {
  margin-top: 0;
}

@media (hover: hover) {
  .chat-secondary-action-btn:hover {
    --cc-lift:  -1.5px;
    --cc-press:  1.01;
    /* Match .cc-entry:hover — profile-tinted bg (mixed into gray-9) and
       outer ring (mixed into gray-6). The glyph icon's pfp ring tracks
       the same hover color so the inner chrome lines up with the
       calling card's pfp on hover. */
    --cc-outer-border-color: color-mix(in srgb, var(--current-user-color, #6a6a6a) 45%, var(--color-gray-6));
    --pfp-outer-border-color: var(--cc-outer-border-color);
    background:               color-mix(in srgb, var(--current-user-color, #6a6a6a) 50%, var(--color-gray-9));
  }
  .chat-secondary-action-btn:hover .chat-secondary-action-inner {
    transform: rotate(1deg);
  }
  .chat-secondary-action-btn:hover .chat-secondary-action-inner::after {
    transform: rotate(1deg);
  }
  /* Keep the notif button's outer ring colored while the cursor is in
     the popover — moving from the button into the popover would
     otherwise drop `:hover` on the button and snap the ring back to
     gray, which reads as a hand-off bug. Only the ring carries over
     (no lift/scale) so the button itself stays put. */
  .notif-wrapper:has(.notif-popover:hover) .chat-secondary-notif-btn {
    --cc-outer-border-color: color-mix(in srgb, var(--current-user-color, #6a6a6a) 45%, var(--color-gray-6));
    --pfp-outer-border-color: var(--cc-outer-border-color);
    background:               color-mix(in srgb, var(--current-user-color, #6a6a6a) 50%, var(--color-gray-9));
  }
}

/* Press + rebound — same as the calling card click animation. */
.chat-secondary-action-btn:hover.cc-pressed,
.chat-secondary-action-btn.cc-pressed {
  --cc-press: 0.97;
  --cc-lift: 0px;
}

.chat-secondary-action-btn.cc-rebound {
  animation: cc-rebound 0.42s cubic-bezier(0.45, 0.05, 0.55, 0.95);
}

.chat-secondary-action-btn.cc-rebound .chat-secondary-action-inner,
.chat-secondary-action-btn.cc-rebound .chat-secondary-action-inner::after {
  animation: cc-pfp-shake 0.36s ease-in-out;
}

/* Visual chrome (size, border, radius, drop shadow, gloss, rim) is now
   centralized in the PFP DESIGN SYSTEM — `.chat-secondary-action-inner`
   is a variant in the same selector lists. Only the layout / icon
   positioning lives here. The `.chat-secondary-action-btn` parent gives
   higher specificity than the PFP `display: block` rule. */
.chat-secondary-action-btn .chat-secondary-action-inner,
.user-search-glyph .chat-secondary-action-inner {
  display: flex;
  align-items: center;
  justify-content: center;
  z-index: 1;
}

/* Right-side panel — chat by default; swaps to the notifications view for
   the currently-viewed profile when a `/~handle` page is open. */
.right-stack,
.notif-stack {
  /* Sized + placed by the .chat-container grid (column 2, row 3 — sits
     under the secondary header in row 2 and the pill header in row 1).
     `min-width: 0` lets the column shrink past its content's intrinsic
     min-width (default `auto`); content that doesn't fit is clipped by
     the `overflow: hidden` below. `position: relative` so the pager
     arrows (absolute-positioned) anchor to this container. */
  grid-column: 2;
  grid-row: 3;
  min-width: 0;
  position: relative;
  align-self: stretch;
  background: blue;
  padding: 0;
  box-sizing: border-box;
  display: flex;
  flex-direction: column;
  gap: 0;
  overflow: hidden;
}
.notif-stack { background: green; }
.notif-stack-header {
  flex-shrink: 0;
  height: 32px;
  background: lime;
  display: flex;
  align-items: stretch;
}
.notif-stack-header .ns-tab {
  flex: 1;
  height: 100%;
  padding: 0 12px;
  border: none;
  background: transparent;
  color: var(--color-white);
  font-family: var(--font-conduit);
  font-size: calc(12px - var(--font-shrink, 0px));
  font-weight: 700;
  cursor: pointer;
  opacity: 0.5;
  transition: opacity 0.15s ease, background 0.15s ease;
}
.notif-stack-header .ns-tab:hover {
  opacity: 0.8;
}
.notif-stack-header .ns-tab.active {
  opacity: 1;
  background: rgba(0, 0, 0, 0.18);
}
.notif-stack[data-active-tab="profile"] .ns-tab-view[data-tab-view="profile"],
.notif-stack[data-active-tab="notifs"]  .ns-tab-view[data-tab-view="notifs"] {
  display: flex;
}

/* ════════════════════════════════════════════════════════════════════
   PFP DESIGN SYSTEM
   ════════════════════════════════════════════════════════════════════
   Every profile picture site-wide shares this design. Tweak any value
   in `:root` and all variants update together.

   Variants:
     • cc-pfp              calling-card pfp           37×37
     • msg-pfp             chat-message pfp           37×37
     • profile-panel-pfp   large profile-page pfp     72×72

   Layers (bottom → top, stacked inside the wrapper):
     1. img content        the actual pfp photo
     2. ::before           gloss sheen          mix-blend-mode: screen
     3. ::after            reflective rim       mix-blend-mode: overlay
     4. .{x}-pfp-border    optional asset ring  (off by default)

   Modules:
     A. SIZES        per-variant dimensions
     B. BASE         img chrome (radius, shadow, background, transitions)
     C. GLOSS        domed sheen (::before)
     D. RIM          reflective inset highlights (::after)
     E. HOVERED      tilt + shadow boost on hover
     F. PRESSED      snappier press tilt
     G. ACTIVE       active shadow + suppression + rebound shake
     H. BORDER RING  asset-driven decorative ring (off by default)
   ════════════════════════════════════════════════════════════════════ */

/* Sunghyun Sans — the site's primary typeface (`--font-conduit`), bundled locally in three
   weights. These @font-face rules were dropped by the legacy-font cleanup (commit 307d687), which
   left the .woff2 files but removed the rules that load them — so the whole site fell back to the
   system sans-serif. Restored here (only Sunghyun; the removed legacy faces stay gone). */
@font-face {
  font-family: 'Sunghyun Sans';
  src: url('/fonts/SunghyunSans-Regular.woff2') format('woff2');
  font-weight: 400;
  font-style: normal;
  font-display: optional;
}
@font-face {
  font-family: 'Sunghyun Sans';
  src: url('/fonts/SunghyunSans-Bold.woff2') format('woff2');
  font-weight: 700;
  font-style: normal;
  font-display: optional;
}
@font-face {
  font-family: 'Sunghyun Sans';
  src: url('/fonts/SunghyunSans-Black.woff2') format('woff2');
  font-weight: 900;
  font-style: normal;
  font-display: optional;
}
/* Marlboro — display face (added Jul 2026). Use via var(--font-marlboro). */
@font-face {
  font-family: 'Marlboro';
  src: url('/fonts/Marlboro-Regular.ttf') format('truetype');
  font-weight: 400;
  font-style: normal;
  font-display: swap;
}
:root {
  /* Primary typeface for the site (also the same cleanup dropped this token, so `var(--font-conduit)`
     with no fallback was resolving to nothing / inheriting). */
  --font-conduit: 'Sunghyun Sans', 'Inter', 'Noto Color Emoji', 'Apple Color Emoji', 'Segoe UI Emoji', sans-serif;
  --font-marlboro: 'Marlboro', 'Sunghyun Sans', sans-serif;   /* display face — apply where wanted */
  /* Dotted-paper texture (3px dot on a 6px grid, 5% black) — the 6×6 unit cell of the supplied 48×48
     tile, which repeats to the identical pattern. TRANSPARENT base (no #fafafa fill) so it overlays the
     element's own background COLOUR instead of covering it. Used on the profile id block + nav. */
  --nk-dot-pattern: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='6' height='6'%3E%3Crect x='1' y='1' width='3' height='3' fill='%23060606' opacity='0.015'/%3E%3C/svg%3E");
  /* — Geometry — */
  --pfp-radius:        16%;   /* % so corners scale proportionally  */
  --pfp-border-width:  1px;   /* rim thickness (overridden on profile pfp) */

  /* — Outer border (1px ring around the pfp, gray everywhere — does not
       react to state). Painted via box-shadow spread, no layout space. */
  --pfp-outer-border-width: 1px;
  --pfp-outer-border-color: var(--color-gray-6);

  /* — Drop shadows (img box-shadow per state) — */
  --pfp-shadow:                0 1px 3px rgba(0, 0, 0, 0.15);
  --pfp-hover-shadow:          0 3px 6px rgba(0, 0, 0, 0.45);
  --pfp-active-shadow:         0 2px 4px rgba(0, 0, 0, 0.30);
  --pfp-active-resting-shadow: 0 1px 3px rgba(0, 0, 0, 0.15);

  /* — Tilt angles — */
  --pfp-hover-tilt:          -2deg;
  --pfp-pressed-tilt:        -4deg;
  --pfp-active-pressed-tilt:  0deg;

  /* — Transitions — */
  --pfp-tilt-transition:    transform 0.18s cubic-bezier(0.22, 1, 0.36, 1);
  --pfp-pressed-transition: transform 0.10s ease-out;
  --pfp-shadow-transition:  box-shadow 0.18s ease;

  /* — Gloss (domed sheen, screen-blended) —
     Radial gradient centered above the pfp so the visible edge curves
     like the bottom of a circle (vs a flat linear gradient). */
  --pfp-gloss: radial-gradient(
    ellipse 120% 90% at 50% -45%,
    rgba(var(--nk-surface-rgb), 0.40) 0%,
    rgba(var(--nk-surface-rgb), 0.12) 60%,
    rgba(var(--nk-surface-rgb), 0.00) 100%);

  /* — Rim (inset highlights, overlay-blended) —
     App-icon "lit edge": bright top (primary light), dim sides
     (ambient reflection), dark bottom (anchors the icon). */
  --pfp-rim-top:    rgba(var(--nk-surface-rgb), 0.36);
  --pfp-rim-side:   rgba(var(--nk-surface-rgb), 0.14);
  --pfp-rim-bottom: rgba(0,   0,   0,   0.25);

  /* — Border ring (asset-driven, off by default) — */
  --pfp-border-ring-asset: none;
  --pfp-border-ring-inset: -3px;
}

/* ─── A. SIZES ──────────────────────────────────────────────────────
   Per-variant dimensions, positioning, and interaction props. The
   larger profile pfp gets a thicker rim via `--pfp-border-width`. */

.cc-pfp-wrapper,
.msg-pfp-frame,
.profile-panel-pfp-wrapper,
.chat-secondary-action-inner {
  position: relative;
  isolation: isolate;        /* contain blend modes to this stacking ctx */
  flex-shrink: 0;
}
.cc-pfp-wrapper,
.msg-pfp-frame,
.msg-pfp-spacer,
.cc-pfp,
.chat-secondary-action-inner { width: 37px; height: 37px; }

.cc-pfp-wrapper            { margin: 4px; z-index: 2; }

/* ─── B. BASE ───────────────────────────────────────────────────────
   The pfp img: rounded edges, drop shadow, fallback bg, transitions.
   The wrapper inherits the same radius so its pseudo-elements
   (gloss + rim) clip cleanly. */

.cc-pfp,
.msg-pfp,
.profile-panel-pfp,
.chat-secondary-action-inner {
  display: block;
  box-sizing: border-box;
  border-radius: var(--pfp-radius);
  background: var(--color-gray-2);   /* placeholder while img loads */
  box-shadow:
    0 0 0 var(--pfp-outer-border-width) var(--pfp-outer-border-color),
    var(--pfp-shadow);
  object-fit: cover;
  transform-origin: center center;
  transition: var(--pfp-tilt-transition), var(--pfp-shadow-transition);
}
.cc-pfp-wrapper,
.msg-pfp-frame,
.profile-panel-pfp-wrapper { border-radius: var(--pfp-radius); }

/* ─── C. GLOSS ──────────────────────────────────────────────────────
   `::before` paints the domed sheen. `mix-blend-mode: screen` always
   lightens, so the highlight reads against any pfp color. */

.cc-pfp-wrapper::before,
.msg-pfp-frame::before,
.profile-panel-pfp-wrapper::before,
.chat-secondary-action-inner::before,
.notif-item::before,
.notif-item-glyph::before,
.cc-entry::before,
.pill-pfp::before {
  content: '';
  position: absolute;
  inset: 0;
  border-radius: inherit;
  background: var(--pfp-gloss);
  mix-blend-mode: screen;
  pointer-events: none;
  z-index: 3;
  transform-origin: center center;
  transition: var(--pfp-tilt-transition);
}

/* ─── D. RIM ────────────────────────────────────────────────────────
   `::after` paints four inset shadows for an app-icon "lit edge"
   look. `mix-blend-mode: overlay` adapts to the underlying colors
   (lightens lights, darkens darks). Blur radius scales with
   `--pfp-border-width` (1.5×) for soft edges instead of a hard line. */

.cc-pfp-wrapper::after,
.msg-pfp-frame::after,
.profile-panel-pfp-wrapper::after,
.chat-secondary-action-inner::after,
.notif-item::after,
.notif-item-glyph::after,
.cc-entry::after,
.pill-pfp::after {
  content: '';
  position: absolute;
  inset: 0;
  border-radius: inherit;
  box-shadow:
    inset  0                                 var(--pfp-border-width)            calc(var(--pfp-border-width) * 1.5) var(--pfp-rim-top),
    inset  0                                 calc(-1 * var(--pfp-border-width)) calc(var(--pfp-border-width) * 1.5) var(--pfp-rim-bottom),
    inset  var(--pfp-border-width)           0                                  calc(var(--pfp-border-width) * 1.5) var(--pfp-rim-side),
    inset  calc(-1 * var(--pfp-border-width)) 0                                 calc(var(--pfp-border-width) * 1.5) var(--pfp-rim-side);
  mix-blend-mode: overlay;
  pointer-events: none;
  z-index: 4;
  transform-origin: center center;
  transition: var(--pfp-tilt-transition);
}

/* ─── D2. CLEAN BORDER (pfps only) ──────────────────────────────────
   Keep the gloss + rim OFF the outer ring so every pfp's border reads
   as one clean color. Scoped to the pfp WRAPPERS (not the calling-card
   surface or icon buttons, which keep the full edge treatment):
     • gloss  -> confined to the inner face, fully transparent before
                 the edges (the default is centered above the pfp and is
                 brightest right at the top edge, bleeding onto the ring)
     • rim    -> edge highlights neutralized
   3D still comes from the face sheen + the drop shadow. */
.cc-pfp-wrapper,
.msg-pfp-frame,
.profile-panel-pfp-wrapper {
  --pfp-gloss: radial-gradient(ellipse 82% 46% at 50% 33%,
    rgba(var(--nk-surface-rgb), 0.36) 0%,
    rgba(var(--nk-surface-rgb), 0.12) 48%,
    rgba(var(--nk-surface-rgb), 0.00) 78%);
  --pfp-rim-top: transparent;
  --pfp-rim-side: transparent;
  --pfp-rim-bottom: transparent;
}

/* ─── E. HOVERED ────────────────────────────────────────────────────
   Tilt the img + both pseudos in unison (so gloss & rim follow the
   pfp). Deepen the drop shadow on the img only. Desktop pointers
   only — touch never triggers tilt. */

/* Calling-card / message-frame / profile-panel pfp HOVER tilt + shadow REMOVED per request — site-wide pfp
   hover animations are gone; only the chat message/post author-pfp hover ring (in the nuke block below) is
   kept. The .active resting-elevation + press animations for these elements are unchanged. */

/* ─── F. PRESSED ────────────────────────────────────────────────────
   Snappier tilt + shorter transition while held. */

.cc-entry.cc-pressed                  .cc-pfp,
.cc-entry.cc-pressed                  .cc-pfp-wrapper::before,
.cc-entry.cc-pressed                  .cc-pfp-wrapper::after,
.msg-pfp-frame.cc-pressed             .msg-pfp,
.msg-pfp-frame.cc-pressed::before,
.msg-pfp-frame.cc-pressed::after,
.profile-panel-pfp-wrapper.cc-pressed .profile-panel-pfp,
.profile-panel-pfp-wrapper.cc-pressed::before,
.profile-panel-pfp-wrapper.cc-pressed::after {
  transform: rotate(var(--pfp-pressed-tilt));
  transition: var(--pfp-pressed-transition);
}

/* ─── G. ACTIVE ─────────────────────────────────────────────────────
   `.active` = this pfp represents the currently-viewed user.
     • Resting:     elevated drop shadow.
     • Header card: suppress the elevation (it'd look weird on the
                    user's own pinned card, which is always "active").
     • Pressed:     no tilt (already feels lifted).
     • Released:    soft rebound shake on img + both pseudos. */

/* resting elevation */
.cc-entry.active                  .cc-pfp,
.msg-pfp-frame.active             .msg-pfp,
.profile-panel-pfp-wrapper.active .profile-panel-pfp {
  box-shadow:
    0 0 0 var(--pfp-outer-border-width) var(--pfp-outer-border-color),
    var(--pfp-active-shadow);
}
/* pressed-while-active = no tilt */
.cc-entry.active.cc-pressed                  .cc-pfp,
.cc-entry.active.cc-pressed                  .cc-pfp-wrapper::before,
.cc-entry.active.cc-pressed                  .cc-pfp-wrapper::after,
.msg-pfp-frame.active.cc-pressed             .msg-pfp,
.msg-pfp-frame.active.cc-pressed::before,
.msg-pfp-frame.active.cc-pressed::after,
.profile-panel-pfp-wrapper.active.cc-pressed .profile-panel-pfp,
.profile-panel-pfp-wrapper.active.cc-pressed::before,
.profile-panel-pfp-wrapper.active.cc-pressed::after {
  transform: rotate(var(--pfp-active-pressed-tilt));
}
/* rebound shake on release */
.cc-entry.cc-rebound-soft                  .cc-pfp,
.cc-entry.cc-rebound-soft                  .cc-pfp-wrapper::before,
.cc-entry.cc-rebound-soft                  .cc-pfp-wrapper::after,
.msg-pfp-frame.cc-rebound-soft             .msg-pfp,
.msg-pfp-frame.cc-rebound-soft::before,
.msg-pfp-frame.cc-rebound-soft::after,
.profile-panel-pfp-wrapper.cc-rebound-soft .profile-panel-pfp,
.profile-panel-pfp-wrapper.cc-rebound-soft::before,
.profile-panel-pfp-wrapper.cc-rebound-soft::after {
  animation: cc-pfp-shake 0.36s ease-in-out;
}

/* ─── H. BORDER RING ────────────────────────────────────────────────
   Asset-driven decorative ring around the pfp (e.g. for special
   user-tier borders later). Off by default — opt in per element by
   adding `data-has-border` to the wrapper, optionally overriding
   `--pfp-border-ring-asset: url(...)` inline. */

.cc-pfp-border,
.msg-pfp-border,
.profile-panel-pfp-border {
  position: absolute;
  inset: var(--pfp-border-ring-inset);
  background-image: var(--pfp-border-ring-asset);
  background-size: contain;
  background-repeat: no-repeat;
  pointer-events: none;
  z-index: 5;
  display: none;
}
.cc-pfp-wrapper[data-has-border]            > .cc-pfp-border,
.msg-pfp-frame[data-has-border]             > .msg-pfp-border,
.profile-panel-pfp-wrapper[data-has-border] > .profile-panel-pfp-border {
  display: block;
}

/* ════════════════════════════════════════════════════════════════════
   CALLING CARD DESIGN SYSTEM
   ════════════════════════════════════════════════════════════════════
   The `.cc-entry` calling card is rendered on the leaderboard, header
   self-card, user-search dropdown, and mention toast. All visual
   design lives here; context-specific overrides (mobile sizing,
   header-square mirrored layout, leaderboard width) stay in their
   own surface blocks and reference these vars.

   Sub-elements:
     • .cc-banner / .cc-banner-img    background banner image + mask
     • .cc-info                       absolute-positioned overlay
       ├─ .cc-top                     row 1: name + rank (or pfp side)
       │  ├─ .cc-name                 username + level icon
       │  └─ .cc-rank                 rank #
       └─ .cc-bottom                  row 2: balance + dpr-pill
          ├─ .cc-balance              $ amount
          └─ .cc-dpr-pill             pill containing .cc-dpr
             └─ .cc-dpr               daily-percent rate

   Layers (bottom → top, stacked inside the cc-entry):
     1. cc-entry bg               solid card color (--cc-surface-rest)
     2. ::after                   blurred + darkened banner backdrop
     3. .cc-banner-img            sharp banner with right-side fade mask
     4. .cc-pfp-wrapper           pfp (z-index 2)
     5. .cc-info                  text overlay (z-index 3)
     6. ::before                  domed gloss + reflective rim, blends
                                  into the layers below via overlay

   Animation custom properties (registered via @property near the top
   of this file): `--cc-lift`, `--cc-press`, `--cc-tilt`. Set by
   `attachCcPressAnimation` in TS via class toggles (`cc-pressed`,
   `cc-rebound`, `cc-rebound-soft`).

   State summary:
     hover      motion (lift + scale) + outer ring → profile color +
                inner-content reactions (text shadow / banner / dpr)
     pressed    scale 0.97 (motion only, no chrome change)
     active     outer ring → profile color + inner-content reactions
                (header self-card resting still gets the ring, but
                 inner-content reactions are suppressed)
     rebound /  outer ring → profile color (animation runs, ring colored
     rebound-soft  during the brief animation)

   Modules:
     A. BASE        flex layout, sizing, transform composition, transitions
     B. SURFACE     bg + outer ring + drop shadow at rest
     C. STRUCTURE   .cc-info / .cc-top / .cc-bottom / .cc-banner positioning
     D. CONTENT     typography + .cc-banner-img + .cc-dpr-pill chrome
     E. EFFECTS     `::after` blurred backdrop + `::before` rim + gloss
     F. HOVERED     hover state (motion + outer ring color + inner content)
     G. PRESSED     pressed state (scale only)
     H. ACTIVE      active state + header self-card inner-content suppression
     I. ANIMATIONS  @keyframes cc-rebound + cc-rebound-soft
   ════════════════════════════════════════════════════════════════════ */

:root {
  /* — Geometry — */
  --cc-width:        240px;
  --cc-height:       45px;
  --cc-radius:       6px;
  --cc-banner-width: 180px;

  /* — Surface (solid card color, no gradient — translucent bases caused
        edge artifacts at the rounded corners). */
  --cc-surface-rest: #2c2c2c;

  /* — Outer 1px ring around the card. Painted via box-shadow spread (no
        layout space). Gray at rest; HOVERED / ACTIVE / ANIMATIONS swap
        the color to the user's profile color. */
  --cc-outer-border-width: 1px;
  --cc-outer-border-color: var(--color-gray-7);

  /* — Drop shadow at rest (soft only; states don't change this). */
  --cc-shadow-rest: 0 2px 4px rgba(0, 0, 0, 0.36), 0 1px 2px rgba(0, 0, 0, 0.20);

  /* — Lift / scale (drive the existing transform composition) — */
  --cc-lift-hover:           -1.5px;
  --cc-lift-active-hover:    -1px;
  --cc-press-hover:           1.01;
  --cc-press-pressed:         0.97;
  --cc-press-active-pressed:  0.99;

  /* — Transitions — */
  --cc-transition-base:
    --cc-lift  0.18s cubic-bezier(0.22, 1,    0.36, 1),
    --cc-press 0.20s cubic-bezier(0.34, 1.4,  0.64, 1),
    --cc-tilt  0.20s cubic-bezier(0.22, 1,    0.36, 1),
    box-shadow 0.04s ease,
    background 0.18s ease;
  --cc-transition-pressed:
    --cc-press 0.08s ease-out,
    --cc-lift  0.18s cubic-bezier(0.22, 1,    0.36, 1);

  /* — Text shadows on the inner labels — */
  --cc-text-shadow-hover:  0 1px 3px rgba(0, 0, 0, 0.85);
  --cc-text-shadow-active: 0 1px 2px rgba(0, 0, 0, 0.65);

  /* — Banner image opacity per state — */
  --cc-banner-rest:   0.3;
  --cc-banner-hover:  0.5;
  --cc-banner-active: 0.5;

  /* — DPR pill background (hover + active) — */
  --cc-dpr-pill-bg: rgba(0, 0, 0, 0.2);

  /* — Blurred backdrop (`::after`): banner image + heavy blur + dim.
        --cc-backdrop-image is unset by default (no banner ships with
        the site). Set it via inline style or override when a banner
        is assigned. */
  --cc-backdrop-filter: blur(12px) brightness(0.8);

  /* — Rim + gloss (`::before`, `mix-blend-mode: overlay`). Rotated 90°
        from the pfp version: RIGHT side is the "top" (brightest light),
        LEFT side is the "bottom" (anchor shadow). Top/bottom edges are
        the "sides" (ambient reflection). */
  --cc-gloss: radial-gradient(
    ellipse 90% 120% at 145% 50%,
    rgba(var(--nk-surface-rgb), 0.40) 0%,
    rgba(var(--nk-surface-rgb), 0.12) 60%,
    rgba(var(--nk-surface-rgb), 0.00) 100%);
  --cc-rim-right: rgba(var(--nk-surface-rgb), 0.36);
  --cc-rim-side:  rgba(var(--nk-surface-rgb), 0.14);
  --cc-rim-left:  rgba(0,   0,   0,   0.25);
}

/* ─── E. EFFECTS ────────────────────────────────────────────────────
   `::after` paints the blurred + dimmed banner backdrop (z-index 0;
   sits below `.cc-banner-img` at z-index 1).
   `::before` paints the domed gloss + reflective rim with
   `mix-blend-mode: overlay`, on top of all card content (z-index 5).

   Note: the hit-area `::before` (snap-to-cursor on leaderboard /
   header / user-search) was previously here — removed because rim +
   gloss now own `::before` and the visual treatment is more important
   than the small hover-snap nicety. */

/* `.cc-entry::before` and `.cc-entry::after` are defined alongside
   `.cc-pfp-wrapper`'s in the PFP DESIGN SYSTEM — gloss + rim with
   `--pfp-gloss` / `--pfp-rim-*` (see ~styles.css:2594). The card's banner
   image sits BELOW these layers (z-index 1) so the gloss/rim glaze
   over the visible banner like the notif-item glyph. */

/* ─── F. HOVERED ────────────────────────────────────────────────────
   Motion (lift + scale) + outer ring/bg → profile-color tint + banner
   opacity bump. Text labels do NOT change on hover. Desktop pointers
   only — touch never triggers tilt/lift. */

@media (hover: hover) {
  .cc-entry:hover {
    --cc-lift:                var(--cc-lift-hover);
    --cc-press:               var(--cc-press-hover);
    /* Bg / border both tint toward the profile color while keeping the
       gray-9 / gray-8 lightness pair from the rest state — bg mixes
       into gray-9, border into gray-8. The pfp border stays neutral. */
    --cc-outer-border-color:  color-mix(in srgb, var(--cc-profile-color, #6a6a6a) 45%, var(--color-gray-6));
    background:               color-mix(in srgb, var(--cc-profile-color, #6a6a6a) 50%, var(--color-gray-9));
  }
  /* Active card hover: lighter lift (it's already raised at rest) */
  .cc-entry.active:hover {
    --cc-lift:  var(--cc-lift-active-hover);
    --cc-press: 1;
  }
  /* Banner brightens on hover */
  .cc-entry:hover .cc-banner-img { opacity: var(--cc-banner-hover); }
}

/* ─── G. PRESSED ────────────────────────────────────────────────────
   Scale only (no chrome color change). `transition` overridden for a
   snappier press feel. */

.cc-entry.cc-pressed {
  --cc-press: var(--cc-press-pressed);
  transition: var(--cc-transition-pressed);
}
.cc-entry.active.cc-pressed { --cc-press: var(--cc-press-active-pressed); }

/* ─── I. ANIMATIONS ─────────────────────────────────────────────────
   Two rebound keyframe sets fire on press release via
   `attachCcPressAnimation`: `cc-rebound` (full spring) for non-active
   cards, `cc-rebound-soft` (muted) for active cards. Outer ring stays
   colored during the animation so the rebound reads as a continuation
   of the pressed-and-released state. */

.cc-entry.cc-rebound,
.cc-entry.cc-rebound-soft { --cc-outer-border-color: color-mix(in srgb, var(--cc-profile-color, #6a6a6a) 45%, var(--color-gray-6)); }
.cc-entry.cc-rebound      { animation: cc-rebound      0.42s cubic-bezier(0.45, 0.05, 0.55, 0.95); }

.dock-pill {
  height: 20px;
  border-radius: 9px;
  border: none;
  padding: 0 10px;
  font-size: var(--font-size-sm);
  font-weight: 600;
  display: flex;
  align-items: center;
  white-space: nowrap;
  flex-shrink: 0;
  cursor: default;
  user-select: none;
  transform-origin: top center;
}

/* Tiny inline pfp injected next to each {userN} placeholder in header
   pills. Plugged into the PFP design system above (gloss/rim selector
   lists) so it gets the same shading as cc-pfp / notif-item etc. */
.pill-pfp {
  position: relative;
  isolation: isolate;
  display: inline-flex;
  flex-shrink: 0;
  width: 14px;
  height: 14px;
  border-radius: 3px;
  margin-right: 4px;
  /* Slightly tighter rim for the small footprint. */
  --pfp-border-width: 0.5px;
  /* `middle` aligns the pfp's vertical center with the line's x-height
     midpoint. The 1px translate compensates for the gap between that
     midpoint and the pill's geometric center (font metrics put the
     x-height midpoint slightly below the pill's true center). */
  vertical-align: middle;
  transform: translateY(-1px);
}
.pill-pfp-img {
  width: 100%;
  height: 100%;
  border-radius: inherit;
  background: var(--color-gray-2);
  object-fit: cover;
  display: block;
}

/* Single wrapper around the rendered pill body. Keeps the templated
   {pfp + name + literal text} fragments inside one flex item of the
   parent .dock-pill so leading whitespace after a `</span>` isn't
   collapsed (block-box rule on anonymous flex items). */
.pill-content {
  display: inline;
  white-space: nowrap;
}

/* Wraps the {pfp + name} pair from a templated pill so they read as one
   clickable unit. Hovering anywhere within the wrapper underlines the
   name; the pfp does NOT get the underline (it's a sibling).
   Display is plain `inline` (not inline-flex) so the trailing text
   following the link sits on the same baseline as the name — the pfp's
   own `vertical-align: -2px` handles its baseline nudge. */
.pill-user-link {
  display: inline;
  cursor: pointer;
}
.pill-user-link:hover .pill-name {
  text-decoration: underline;
}

.pill-red { background: #6e1a1a; color: #ff8080; }
.pill-orange { background: #6e3a1a; color: #ffb380; }
.pill-blue { background: #1a3a6e; color: #80b3ff; }
.pill-purple { background: #4a1a6e; color: #c080ff; }
.pill-green { background: #1a6e2e; color: #80ff99; }
.pill-yellow { background: #6e5a1a; color: #ffe080; }
.pill-transparent { background: transparent; color: var(--color-white); font-size: calc(10px - var(--font-shrink, 0px)); }

.recording-time {
  color: var(--color-white);
  font-weight: 700;
  font-size: var(--font-size-lg);
  min-width: 50px;
}

/* --- Chat Panel: XP Bar --- */
/* Drawer */
.chat-xp-bar {
  height: 8px;
  background: transparent;
  overflow: hidden;
  flex-shrink: 0;
  position: fixed;
  bottom: 0;
  left: 0;
  right: 0;
  z-index: 100;
  pointer-events: none;
}

/* Both the XP bar and the inventory drawer are global overlays. They only
   make sense on the chat page — hide them on signin / onboarding for both
   desktop and mobile. */
body:not(.page-chat) .chat-xp-bar {
  display: none !important;
}

/* Mobile: XP bar lives at the top of the screen above the pill header,
   since the bottom is now occupied by the inventory drawer. */
body.is-mobile .chat-xp-bar {
  top: 0;
  bottom: auto;
}

.chat-xp-fill {
  height: 100%;
  background: #c0762d;
  width: 0;
  transition: width 1.2s linear, background 0.5s ease;
}

.chat-xp-fill.xp-loss {
  background: #c02d2d;
}

/* --- Chat Panel: Animations --- */

@keyframes msg-jiggle {
  0%   { transform: scale(1); }
  25%  { transform: scale(1.08); }
  50%  { transform: scale(0.97); }
  75%  { transform: scale(1.02); }
  100% { transform: scale(1); }
}

.chat-message-new {
  animation: msg-slide-in-other 0.35s cubic-bezier(0.175, 0.885, 0.32, 1.275);
}
/* A newly-arrived FEED post also flashes the author's profile-colour tint (the SAME highlight as the
   hover, via `--nuke-post-hl` set inline by renderPost) and then slowly fades it out once the slide
   settles. Scoped to `.nuke-post` so the profile-page top (which also uses .chat-message-new for its
   slide) doesn't pick up a stray grey tint. The class is stripped on the TINT's animationend (the
   longer of the two animations) — see the animationend handler in chat-render.ts. */
.nuke-container .nuke-post.chat-message-new {
  animation: msg-slide-in-other 0.35s cubic-bezier(0.175, 0.885, 0.32, 1.275),
    nuke-post-arrive-tint 1.8s ease-out;
}
/* New message: flash a STRONGER shot of the author's colour (raw --nuke-cmd-color, ~3× the
   resting tint) held through the slide, then ease down to the card's RESTING profile tint
   (--nuke-post-hl) — a pop that settles into the default, no longer a fade to transparent. */
@keyframes nuke-post-arrive-tint {
  0%, 20% { background-color: var(--card-darken); }
  100%    { background-color: var(--card-bg, var(--nuke-post-hl, var(--nk-hover-1))); }
}
.player-panel-section .action-btn { padding: 8px 12px; font-size: var(--font-size-sm); }
.player-panel-section .notifications-empty {
  font-size: var(--font-size-sm);
  color: var(--color-gray-8);
  text-align: center;
  padding: 4px;
  display: block;
}

/* Shared shake keyframe used by the PFP system + a few other elements
   (pill scroll buttons, chat secondary action buttons). Not cc-entry-
   specific but lives here for historical reasons — leave in place. */
@keyframes cc-pfp-shake {
  0%   { transform: rotate(0deg); }
  18%  { transform: rotate(-5deg); }
  36%  { transform: rotate(4deg); }
  54%  { transform: rotate(-3deg); }
  72%  { transform: rotate(2deg); }
  88%  { transform: rotate(-1deg); }
  100% { transform: rotate(0deg); }
}

/* Numeric font (Nunito) + tabular digits for all number-displaying elements */
.cc-rank,
.msg-balance, .msg-level-num,
.stat-value,
.mission-timer,
.leaderboard-value, .leaderboard-online-count,
.presale-user-value, .presale-input,
.slot-charges, .item-ammo-label,
.notif-badge-count {
  font-family: var(--font-numeric);
  font-variant-numeric: tabular-nums;
}

.slot-icon { color: var(--color-text-primary); font-weight: 400; font-size: var(--font-size-xxl); }
.inventory-slot.equipped .slot-icon { filter: invert(1); }

.dock-pill.pill-landed {
  animation: pill-land 0.3s ease-out;
}

.pawn-action .action-btn {
  width: 100%;
}

/* --- Profile Panel: Header & Navigation --- */
.notification-header {
  height: 34px;
  display: flex;
  align-items: center;
  flex-shrink: 0;
  background: transparent;
  border-radius: 0;
  position: relative;
}

.profile-dropdown-bank .action-btn {
  flex: 1;
}
.bank-modal-actions .action-btn {
  flex: 1;
}
.inventory-panel-slots .inventory-slot {
  width: auto;
  height: auto;
  aspect-ratio: auto;
  background: transparent;
}

.inventory-slot {
  position: relative;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  width: 72px;
  height: 72px;
  padding: var(--spacing);
  box-sizing: border-box;
  border: none;
  border-radius: 2px;
  background: var(--color-gray-1);
  cursor: pointer;
  transition: all 0.2s;
}

.inventory-slot:hover { border-color: var(--color-nuke-primary); }
.inventory-slot.empty { cursor: default; }

.inventory-slot.selected {
  border-color: var(--color-nuke-primary);
  background: rgba(237, 28, 36, 0.05);
}

/* Notification header bar */
.notification-header {
  display: flex;
  align-items: center;
  gap: 6px;
  padding: 0 12px;
  font-size: var(--font-size-md);
  border-bottom: none;
}

.notif-rotate-item { display: none; }
.notif-rotate-item.active { display: inline; }

.notif-season {
  font-family: var(--font-conduit);
  font-size: var(--font-size-xl);
  font-weight: 900;
  color: var(--color-white);
  letter-spacing: 1px;
  text-transform: uppercase;
}

.notif-online {
  font-size: var(--font-size-sm);
  font-weight: 600;
  color: var(--color-text-secondary);
}

.notif-jackpot {
  font-size: var(--font-size-lg);
  font-weight: 700;
  color: var(--color-nuke-accent);
}

.notif-info {
  font-size: var(--font-size-sm);
  font-weight: 600;
  color: var(--color-text-secondary);
}

.notifications-header-right {
  display: flex;
  align-items: center;
  gap: 8px;
  flex-shrink: 0;
}

#notifications-count {
  display: inline-block;
  min-width: 16px;
  height: 16px;
  padding: 0 4px;
  background: var(--color-nuke-primary);
  color: var(--color-text-primary);
  font-size: var(--font-size-sm);
  font-weight: 700;
  line-height: 16px;
  text-align: center;
  border-radius: 8px;
}

#notifications-count:empty {
  display: none;
}

.notifications-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  margin-bottom: var(--spacing);
}

.notifications-title {
  font-size: var(--font-size-lg);
  font-weight: 700;
}

.notifications-count {
  font-size: var(--font-size-sm);
  color: var(--color-gray-5);
  text-transform: uppercase;
  letter-spacing: 0.5px;
}

.notifications-list {
  display: flex;
  flex-direction: column;
  gap: 2px;
  max-height: 100%;
  overflow-y: auto;
  overscroll-behavior: contain;
  scrollbar-width: none;
}

.notifications-list::-webkit-scrollbar {
  display: none;
}

.notifications-empty {
  color: var(--color-text-muted);
  font-size: var(--font-size-sm);
  font-weight: 400;
  padding: var(--spacing);
  text-align: center;
}

/* Base rule for EVERY `.notif-item` (created by `createNotificationItem`, shared across the toast,
   the /nuke notifications list, and the profile notifs). It intentionally sets ONLY the positioning
   anchor for the absolute `.notif-unread-dot` — no surface, padding, or text colour. Each consumer
   fully owns its own look (scoped rules below), so nothing forced here can bleed into them. This is
   deliberate: the old forced gray bg + `color: black` here caused theme/specificity conflicts. */
/* ── NOTIFICATION CARD — SHARED DESIGN TOKENS ─────────────────────────────────
   The toast banner (.nuke-notif-toast) and the notifications-menu rows
   (.nuke-notif-list .notif-item) BOTH build their card from these tokens, so one
   change here moves both surfaces. Per-surface-only bits stay in their own rules
   below: the menu adds the inter-card gap + timestamps + unread tint + day dividers;
   the toast adds its fixed stack + hover/press + hides the timestamp. */
.nuke-container {
  --notif-card-bg: var(--nk-surface);
  --notif-card-border: 1px solid var(--nk-border);
  --notif-card-radius: 2px;
  --notif-row-pad: 6px 10px;
  --notif-row-gap: 12px;
  --notif-row-align: flex-start;   /* avatar pinned TOP-left on every notif (tall or short) */
  --notif-card-gap: 8px;                              /* menu only: gap between cards */
  --notif-pfp-size: var(--nuke-chat-pfp-size, 40px);
  --notif-pfp-radius: 0px;
  --notif-pfp-border: 1px solid var(--nk-border);
  /* Whole-app top/bottom frame — consumed by the containing-block rule below. */
  --page-pad: 0px;
}
/* Enclose the whole (position:fixed) app: the transform makes .nuke-container the
   containing block for its fixed/absolute children so they re-anchor to its box.
   Use INSET (not padding) for the 16px top/bottom gap — padding does NOT offset
   positioned children (they anchor to the padding-box edge). Full width (0 sides);
   the trio-width border is the ::before frame below. .hidden still hides it. */
.nuke-container {
  position: fixed;
  inset: var(--page-pad) 0;
  box-sizing: border-box;
  transform: translate(0);
}
.notif-item { position: relative; }
/* Base for the actor/global avatar in a notif's left slot (createNotificationItem) — a squared 1px-border
   avatar. Per-surface sizes (modal list 34 / profile 40 / toast 38) override below; this base keeps the img
   sane (never its intrinsic size) on any other surface that reuses createNotificationItem. */
.notif-item-pfp {
  flex-shrink: 0;
  width: var(--notif-pfp-size);
  height: var(--notif-pfp-size);
  border-radius: var(--notif-pfp-radius);
  border: var(--notif-pfp-border);
  box-sizing: border-box;
  object-fit: cover;
  display: block;
}

/* ── Notification banner toasts (top-centre) ─────────────────────────────────
   A new self-notification pops a banner in at the top with the `.chat-message-new` jiggle (wired in
   notif-toast.ts). Fully theme-token based — the styled card is the `.nuke-notif-toast` block below. */
.nuke-notif-toast-stack {
  position: fixed;
  top: calc(var(--page-header-h) + 10px); /* rest BELOW the page header so toasts slide OUT from under it, not over it */
  /* Centre over the CHAT column, not the viewport: `.nuke-stage` sits at
     `50% - var(--trio-shift)`, so match it. Inherited from `.nuke-container`
     (the stack now mounts inside it); falls back to screen-centre if absent. */
  left: calc(50% - var(--trio-shift, 0px));
  transform: translateX(-50%);
  z-index: 26; /* BELOW the page header (z 27) so the opaque header OCCLUDES the toast as it slides down — it emerges from under the header. Still above the app columns/stage + their subview modals (all z 25). */
  display: flex;
  flex-direction: column;
  gap: 8px;
  width: min(380px, calc(100vw - 24px));
  pointer-events: none; /* the stack ignores clicks; each toast re-enables them */
}
/* ── Notification banner (toast) ──────────────────────────────────────────────
   Its OWN class (`nuke-notif-toast`) — the generic action pill `toast.ts` also uses
   `.nuke-toast` (bottom-center, `.nuke-container .nuke-toast`), so the notification
   banner is namespaced separately to avoid that collision entirely.
   Matches the app's design language 1:1: the site font (--font-conduit), the standard
   card treatment (--nk-surface + 1px --nk-border + 16px radius), and the notifications
   row type scale. Body-mounted, so it re-states the row layout the
   `.nuke-container .nuke-notif-list` rules can't reach — FULLY token-based, so it
   themes across light / dark / true / accent modes with no hardcoded colours. */
.nuke-notif-toast {
  pointer-events: auto;
  font-family: var(--font-conduit);
  background: var(--notif-card-bg);
  border: var(--notif-card-border);
  border-radius: 0;   /* square, matching the notif page cards */
  overflow: hidden;
  /* Retro hard offset drop-shadow (no blur), bottom-right — same as the notif cards + right-panel .nuke-side-box. */
  box-shadow: 4px 4px 0 0 rgba(0, 0, 0, 0.2);
  cursor: pointer;
  transition: transform 0.12s ease, background 0.12s ease;
}
/* The toast mounts INSIDE `.nuke-container`, whose mini-preflight reset (`.nuke-container *`,
   specificity 0,1,0) zeroes border-width and out-orders the rule above. Re-assert the 2px
   chat-panel-coloured border at higher specificity (0,2,0) so it actually renders. */
.nuke-container .nuke-notif-toast { border: var(--notif-card-border); }
.nuke-notif-toast:hover { background: var(--nk-hover-1); }
.nuke-notif-toast:active { transform: scale(0.985); }

.nuke-notif-toast .notif-item {
  display: flex;
  align-items: var(--notif-row-align);
  gap: var(--notif-row-gap);
  padding: var(--notif-row-pad);
  background: transparent;
  border: none;
  cursor: pointer;
}
/* A banner never shows an unread highlight (it's already "new") — the row stays on the card surface. */
.nuke-notif-toast .notif-item.unread { background: transparent; }
.nuke-notif-toast .notif-unread-dot { display: none; } /* a banner is already "new" */
.nuke-notif-toast .notif-item-time { display: none; } /* a banner means it just happened — no timestamp */

/* Glyph: flat accent-tinted circle (the app-icon gloss/rim ::before/::after are off). */
.nuke-notif-toast .notif-item-glyph {
  flex-shrink: 0;
  width: 38px;
  height: 38px;
  display: flex;
  align-items: center;
  justify-content: center;
  border-radius: 50%;
  background: rgba(var(--nk-accent-rgb), 0.14);
}
/* Actor pfp in the toast's left slot — squared 1px-border avatar (matches the notif page). */
.nuke-notif-toast .notif-item-pfp {
  flex-shrink: 0;
  width: var(--notif-pfp-size);
  height: var(--notif-pfp-size);
  border-radius: var(--notif-pfp-radius);
  border: var(--notif-pfp-border);
  box-sizing: border-box;
  object-fit: cover;
  display: block;
}
.nuke-notif-toast .notif-item-glyph::before,
.nuke-notif-toast .notif-item-glyph::after { display: none; }
.nuke-notif-toast .notif-item-glyph,
.nuke-notif-toast .notif-item-glyph svg,
.nuke-notif-toast .notif-item-glyph svg * { color: var(--nk-accent); }
.nuke-notif-toast .notif-item-glyph svg { width: 20px; height: 20px; display: block; }

.nuke-notif-toast .notif-item-content { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 1px; }
.nuke-notif-toast .notif-item-title {
  font-weight: 700;
  font-size: calc(15px - var(--font-shrink, 0px));
  line-height: 1.35;
  letter-spacing: -0.01em;
  color: var(--nk-text);
}
.nuke-notif-toast .notif-item-row { display: flex; align-items: baseline; justify-content: space-between; gap: 8px; }
.nuke-notif-toast .notif-item-message {
  flex: 1;
  min-width: 0;
  color: var(--nk-text-2);
  font-size: calc(14px - var(--font-shrink, 0px));
  font-weight: 500;
  line-height: 1.35;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.nuke-notif-toast .notif-item-time { flex-shrink: 0; color: var(--nk-text-2); font-size: calc(13px - var(--font-shrink, 0px)); font-weight: 500; }

/* Unread state + text colour are OWNED BY EACH CONSUMER (theme-token based), not forced globally —
   the old `.notif-item.unread { background: #1e1e1e }` and `.notif-item * { color: black }` here were
   the source of the gray-toast + invisible-text-in-dark bugs. Only the structural dot + the mention
   quote (theme-tokened) remain global. */
.notif-unread-dot {
  position: absolute;
  top: 8px;
  right: 8px;
  width: 8px;
  height: 8px;
  background: var(--nk-accent, #4caf50);
  border-radius: 50%;
}

.nuke-container .notif-mention-quote {
  /* @mention quote tinted with the MENTIONING user's OWN colours: their post-container fill (SETTING-2)
     as the background + their copy text (SETTING-7), derived via OKLCH from --mention-user-color (a hex
     set per-notif in createNotificationItem). Colourless / unresolved → the *-themed var is invalid, so
     the var() fallbacks give neutral tokens. No border. `.nuke-container` (0,2,0) also beats the
     mini-preflight reset. No vertical padding/margin → the box is one text line tall, matching a normal notif. */
  --mention-fill-themed: oklch(from var(--mention-user-color) var(--set-2-l) var(--set-2-c) h);
  --mention-copy-themed: oklch(from var(--mention-user-color) var(--set-7-l) var(--set-7-c) h);
  margin: 0;
  /* Hug the TEXT — size to content instead of stretching to the full row (base .notif-item-message
     is flex:1). Long mentions still shrink + ellipsis (flex-shrink:1 + inherited min-width:0/overflow). */
  flex: 0 1 auto;
  padding: 0 6px;
  background: var(--mention-fill-themed, var(--nk-hover-1));
  color: var(--mention-copy-themed, var(--nk-text-2));
  font-style: italic;
}

.notifications-header-text-wrapper {
  overflow: hidden;
  min-width: 0;
}

#notifications-header-text {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  min-width: 0;
  font-weight: 400;
  font-size: var(--font-size-md);
  color: var(--color-text-primary);
}

.notif-header-quote {
  margin-left: 8px;
  color: var(--color-gray-5);
  font-style: italic;
}
.notif-header-quote::before {
  content: '"';
}
.notif-header-quote::after {
  content: '"';
}

/* --- Profile Panel: Missions --- */

@keyframes mission-bg-in {
  0% { opacity: 0; }
  100% { opacity: 1; }
}

#mission-list .notif-item.mission-locked {
  opacity: 0.45;
  background: rgba(var(--nk-surface-rgb), 0.015);
  border-color: var(--color-gray-4);
}

#mission-list .notif-item.mission-claimed {
  opacity: 0.7;
  text-decoration: line-through;
  text-decoration-color: var(--color-gray-7);
}

#mission-list .notif-item.mission-exhausted {
  opacity: 0.5;
  font-style: italic;
}

#mission-list .notif-item {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 8px;
}

.shortcuts-panel.hidden { display: none; }

.presale-input-group .action-btn {
  flex-shrink: 0;
}

.notification-header-toast-clip {
  position: absolute;
  top: 100%;
  left: 0;
  width: 100%;
  height: 400px;
  overflow: hidden;
  pointer-events: none;
}

.notification-header .mention-toast {
  display: flex;
  flex-direction: column;
  align-items: stretch;
  position: relative;
  left: 0;
  right: 0;
  width: 100%;
  transform: translateY(-100%);
  transition: transform 0.15s ease-out;
  pointer-events: auto;
}

.notification-header .mention-toast-visible {
  transform: translateY(4px);
}

/* ===========================================
   13. ANIMATIONS
   =========================================== */
@keyframes crt-scroll {
  0% { background-position: 0 0, 0 0; }
  100% { background-position: 0 0, 0 80px; }
}

/* ============ Announcement Popup ============ */

.announcement-popup {
  position: fixed;
  inset: 0;
  z-index: 9999;
  display: flex;
  align-items: center;
  justify-content: center;
  background: rgba(0, 0, 0, 0.7);
}

.announcement-popup-content {
  background: var(--bg-primary, #1a1a1a);
  border: 1px solid var(--border-color, #333);
  border-radius: 8px;
  padding: 24px;
  max-width: 360px;
  width: 90%;
  display: flex;
  flex-direction: column;
  gap: 12px;
}

.announcement-popup-header {
  font-size: calc(16px - var(--font-shrink, 0px));
  font-weight: bold;
  color: var(--text-primary, #fff);
}

.announcement-popup-desc {
  font-size: calc(13px - var(--font-shrink, 0px));
  color: var(--text-secondary, #999);
  margin: 0;
}

.announcement-msg-input {
  background: var(--bg-secondary, #222);
  border: 1px solid var(--border-color, #444);
  border-radius: 4px;
  color: var(--color-white);
  padding: 8px 10px;
  font-size: var(--font-size-md);
  font-weight: 400;
  font-family: inherit;
}

.announcement-confirm-btn {
  background: var(--accent-color, #4a9eff);
  color: var(--color-white);
  border: none;
  border-radius: 4px;
  padding: 12px;
  font-size: var(--font-size-md);
  font-weight: 400;
  cursor: pointer;
  font-family: inherit;
}

.announcement-confirm-btn:disabled {
  opacity: 0.5;
  cursor: not-allowed;
}

.announcement-cancel-btn {
  background: transparent;
  color: var(--color-text-secondary);
  border: 1px solid var(--border-color, #444);
  border-radius: 4px;
  padding: 8px;
  font-size: var(--font-size-sm);
  font-weight: 400;
  cursor: pointer;
  font-family: inherit;
}

.announcement-status {
  font-size: calc(12px - var(--font-shrink, 0px));
  color: var(--text-secondary, #999);
  min-height: 1em;
}

/* ============ Announcement Overlay (Radio) ============ */

.announcement-overlay {
  position: fixed;
  bottom: 24px;
  left: 50%;
  transform: translateX(-50%);
  z-index: 9000;
  background: rgba(0, 0, 0, 0.85);
  border: 1px solid var(--accent-color, #4a9eff);
  border-radius: 8px;
  padding: 12px 24px;
  color: var(--color-white);
  font-size: calc(14px - var(--font-shrink, 0px));
  pointer-events: none;
  animation: announcement-pulse 1.5s ease-in-out infinite;
}

/* ============ Item Preview Modal ============ */
/* Item-preview modal — redesigned to the nuke light aesthetic (white card, hairline
   var(--nk-border) border, #0F1419/#536471 text, green buy pill, x.com-style dim overlay). */
.item-preview-modal-overlay {
  position: fixed;
  inset: 0;
  z-index: 1200;
  display: flex;
  align-items: center;
  justify-content: center;
  background: rgba(15, 20, 25, 0.4);
}

.item-preview-modal {
  position: relative;
  width: 300px;
  box-sizing: border-box;
  background: var(--nk-surface);
  border: 1px solid var(--nk-border);
  border-radius: 16px;
  padding: 28px 22px 20px 22px;
  display: flex;
  flex-direction: column;
  align-items: center;
  box-shadow: 0 12px 40px rgba(15, 20, 25, 0.18);
}

.item-preview-close {
  position: absolute;
  top: 10px;
  right: 10px;
  width: 30px;
  height: 30px;
  background: transparent;
  border: none;
  border-radius: 9999px;
  color: var(--nk-text-2);
  cursor: pointer;
  display: flex;
  align-items: center;
  justify-content: center;
  transition: background 0.15s, color 0.15s;
}

.item-preview-close:hover {
  background: var(--nk-border-soft);
  color: var(--nk-text);
}

.item-preview-icon {
  width: 96px;
  height: 96px;
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: calc(60px - var(--font-shrink, 0px));
  margin-bottom: 10px;
}

.item-preview-name {
  font-size: calc(19px - var(--font-shrink, 0px));
  font-weight: 800;
  color: var(--nk-text);
  text-align: center;
  margin-bottom: 8px;
}

.item-preview-description {
  font-size: calc(14px - var(--font-shrink, 0px));
  color: var(--nk-text-2);
  text-align: center;
  line-height: 1.45;
  margin-bottom: 20px;
  min-height: 38px;
}

.item-preview-buy-btn,
.item-preview-action-btn {
  height: 44px;
  padding: 0 16px;
  border: none;
  border-radius: 9999px;
  font-family: inherit;
  font-size: calc(15px - var(--font-shrink, 0px));
  font-weight: 800;
  cursor: pointer;
  transition: filter 0.15s, background-color 0.15s;
  color: #FFFFFF;
}

.item-preview-buy-btn {
  width: 100%;
  background: #0E8A53;
}

.item-preview-actions {
  display: flex;
  gap: 8px;
  width: 100%;
}

.item-preview-action-btn {
  flex: 1;
}

.item-preview-action-btn.primary {
  background: #0E8A53;
}

.item-preview-action-btn.secondary {
  background: #536471;
}

.item-preview-buy-btn:hover,
.item-preview-action-btn:hover {
  filter: brightness(1.06);
}

.item-preview-buy-btn:active,
.item-preview-action-btn:active {
  filter: brightness(0.92);
}

/* /nuke sandbox styles have been moved to src/css/styles.css. Loaded
   alongside styles.css via a separate <link> in index.html. Keep
   nuke-page rules out of this file. */

/* ============================================
   Cursors — the custom Notwaita bitmap cursors were removed site-wide. These keep
   the STANDARD SYSTEM cursor semantics (pointer on clickables, text in inputs,
   etc.) so nothing regresses to a bare arrow. The ONLY custom cursor left is the
   notepad pen (`.nuke-notebook-canvas` in styles.css). `!important` so per-element
   rules don't shadow these.
   ============================================ */
a,
button,
[role="button"],
[role="tab"],
[role="menuitem"],
[role="option"],
[role="link"],
[onclick],
label[for],
summary,
select,
.cursor-pointer,
.classic-button,
.action-btn,
.action-button,
.nuke-stage-tab,
.nuke-stage-tab-action,
.contact-group-header,
.nuke-row,
.nuke-profile-name-row,
.nuke-profile-mail-item,
.item-card:not(.item-card-empty),
.nuke-post-quote,
.nuke-post[data-has-account="true"],
.nuke-reaction-badge,
.nuke-chat-img-wrap {
  cursor: pointer !important;
}

/* ============================================================
   ANIMATION LIBRARY — @keyframes preserved from the old prod UI.
   The prod rules that referenced these were removed; the keyframes
   themselves are kept, named, and grouped here for reuse later.
   ============================================================ */

@keyframes btn-spin { to { transform: rotate(360deg); } }

@keyframes cc-rebound {
  0%   { --cc-press: 0.97;   }
  28%  { --cc-press: 1.018;  }
  52%  { --cc-press: 0.995;  }
  72%  { --cc-press: 1.003;  }
  88%  { --cc-press: 0.9995; }
  100% { --cc-press: 1;      }
}

@keyframes cc-rebound-soft {
  0%   { --cc-press: 0.99; --cc-tilt: 0deg; }
  100% { --cc-press: 1;    --cc-tilt: 0deg; }
}

@keyframes msg-row-fade {
  0%, 30% { opacity: 1; }
  100%    { opacity: 0; }
}

@keyframes msg-slide-in-other {
  0% { transform: translateY(20px) scale(0.95) rotate(0deg); }
  20% { transform: translateY(-4px) scale(1.02) rotate(var(--tilt, -0.8deg)); }
  40% { transform: translateY(3px) scale(0.99) rotate(0.5deg); }
  60% { transform: translateY(-1px) scale(1.005) rotate(-0.25deg); }
  80% { transform: translateY(0) scale(1) rotate(0.1deg); }
  100% { transform: translateY(0) scale(1) rotate(0deg); }
}

@keyframes shop-jiggle {
  0%   { transform: translateX(0); }
  15%  { transform: translateX(-4px); }
  30%  { transform: translateX(4px); }
  45%  { transform: translateX(-3px); }
  60%  { transform: translateX(3px); }
  75%  { transform: translateX(-1px); }
  90%  { transform: translateX(1px); }
  100% { transform: translateX(0); }
}
/* Applies the shake above — added by JS (`.shop-jiggle`) and removed on
   animationend. Used for the signed-out shop/season-pass tap feedback and the
   notification/store jiggles in main.ts / services/shop.ts. */
.shop-jiggle { animation: shop-jiggle 0.4s ease both; }

@keyframes pill-land {
  0%   { transform: scale(1); }
  25%  { transform: scale(1.08); }
  50%  { transform: scale(0.97); }
  75%  { transform: scale(1.02); }
  100% { transform: scale(1); }
}

@keyframes mission-bg-out {
  0% { opacity: 1; }
  100% { opacity: 0; }
}

@keyframes mission-in {
  0% { transform: scale(0.9); opacity: 0; }
  100% { transform: scale(1); opacity: 1; }
}

@keyframes mission-out {
  0% { transform: scale(1); opacity: 1; }
  100% { transform: scale(0.9); opacity: 0; }
}

@keyframes pulse {
  0%, 100% { opacity: 1; }
  50%      { opacity: 0.7; }
}

@keyframes blink {
  0%, 100% { opacity: 1; }
  50%      { opacity: 0.3; }
}

@keyframes announcement-pulse {
  0%, 100% { opacity: 1; }
  50% { opacity: 0.7; }
}

/* ===================================================================
   NUKE PAGE — styles scoped under .nuke-container (was styles.css)
   =================================================================== */

/* =============================================================
   /nuke sandbox stylesheet — MSN Messenger design
   =============================================================
   All visual rules for the /nuke sandbox live in this file,
   scoped under `.nuke-container ...` (or the `page-nuke` body /
   html class for global mode). Imported alongside styles.css from
   index.html. Keep this file the sole source of truth for any
   visual property used on the nuke page; HTML and TS template
   strings should hold layout primitives only.
   ============================================================= */

/* --- Page-mode global rules ------------------------------------ */

body.page-nuke .content {
  padding: 0;
  overflow: hidden;
}
/* Block all overscroll rubber-band on the /nuke sandbox — both the
   document itself and the inner scroll containers. The `page-nuke`
   class is set on BOTH `html` and `body` (see main.ts) so we don't
   need a `:has()` walk to reach `html`. */
body.page-nuke,
html.page-nuke {
  overscroll-behavior: none;
}
/* Themed page background on /nuke — overrides the global `body` `#eaeaea`. The tiled
   "brilliant" texture is painted by `body.page-nuke::before` (see below). */
/* ╔══════════════════════════════════════════════════════════════════════════╗
   ║  NUKE THEME TOKENS — single source of truth for Light + Lights Out (dark)   ║
   ╠══════════════════════════════════════════════════════════════════════════╣
   ║  Every colour / elevation in the .nuke-container UI flows through these     ║
   ║  --nk-* tokens. LIGHT values live in this block; the dark values live in    ║
   ║  the mirrored `.theme-lightsout body.page-nuke` block (search              ║
   ║  "LIGHTS OUT TOKENS"). Both blocks list the SAME tokens in the SAME order   ║
   ║  so it is obvious what each theme flips.                                    ║
   ║                                                                            ║
   ║  ADDING A NEW COMPONENT  →  style it with these tokens, never a raw hex /   ║
   ║  rgba. It then themes in BOTH modes automatically from the two blocks.      ║
   ║  The only legitimate theme-specific CSS is a *structural* flip that can't   ║
   ║  be a single token (e.g. a shadow that must change DIRECTION, not just      ║
   ║  colour). Write those as `.theme-lightsout <selector>` next to the          ║
   ║  component, prefixed and commented with WHY — see the reply/image shadow    ║
   ║  + notification overrides for the pattern. Avoid them when a token works.   ║
   ╚══════════════════════════════════════════════════════════════════════════╝ */

/* Register the page top-fade colour as a real <color> so it can be TRANSITIONED (a plain custom
   property can't interpolate; a gradient stop that reads it then animates smoothly). initial-value
   `transparent` matches its only fallback (`var(--page-grad-top, transparent)` in the page gradient),
   so a colourless user still gets no top fade — nothing else references it. The page's SOLID colour
   (`--page-bg-themed`) is NOT registered (it must stay invalid→--nk-surface for the opaque subview
   overlays); its background-color transitions on its own as a normal animatable property. */
@property --page-grad-top {
  syntax: '<color>';
  inherits: true;
  initial-value: transparent;
}

body.page-nuke,
body.page-onboarding {
  /* ── Surfaces ── cool white (X.com light) ── */
  --nk-surface: #FFFFFF;            /* page / card background */
  --nk-surface-2: #F7F9FA;          /* recessed / secondary surface */
  --nk-surface-rgb: 255, 255, 255;  /* triplet for rgba() gradients that fade to the surface */

  /* ── Text ── near-black ── */
  --nk-text: #0F1419;               /* primary text */
  --nk-text-2: #536471;             /* muted / secondary text */

  /* ── Borders ── COLOR SETTING 3 ──────────────────────────────────────────
     The logged-in user's hue at setting 3 themes every divider site-wide — the
     chat-panel frame, the date dividers, and all center-panel lines all resolve
     from these tokens. Isolated in --nk-border-themed so a colourless user (var
     unset → invalid) falls back to the original neutral greys. */
  --nk-border-themed: oklch(from var(--current-user-color) var(--set-3-l) var(--set-3-c) h);
  --nk-border: var(--nk-border-themed, #CFD9DE);        /* default card / divider border */
  --nk-border-plain: #CFD9DE;   /* FIXED neutral (never viewer-themed): other-authored content (posts/replies)
                                   uses THIS when its author/target is colourless, so it stays neutral instead
                                   of inheriting the VIEWER hue (--nk-border is viewer-themed in light mode). */
  --nk-border-soft: var(--nk-border-themed, #EFF3F4);   /* hairline / soft divider */
  /* Self card FILL (SETTING 2) for the radio/stream left-highlight composite: profile tint when coloured,
     the SAME neutral card fill the posts use (--nk-hover-1) when colourless. */
  --self-card-fill-themed: oklch(from var(--current-user-color) var(--set-2-l) var(--set-2-c) h);
  --self-card-fill: var(--self-card-fill-themed, var(--nk-hover-1));
  --nk-border-2: #E0E0E0;           /* misc control border */
  /* "Lined paper" texture tiled across the post feed (see #nuke-chat-feed): a faint DOTTED horizontal
     rule every 12px (the provided SVG, as a 48×48 repeating tile). Light mode = dark dots; the dark
     mirror below swaps to light dots. Tune faintness via the `opacity=` in the data-URI. */
  --nk-paper-svg: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='48' height='48'%3E%3Cg fill='none' stroke='%23000000' stroke-width='1' stroke-dasharray='1 1' opacity='0.016'%3E%3Cline x1='0' y1='11.5' x2='48' y2='11.5'/%3E%3Cline x1='0' y1='23.5' x2='48' y2='23.5'/%3E%3Cline x1='0' y1='35.5' x2='48' y2='35.5'/%3E%3Cline x1='0' y1='47.5' x2='48' y2='47.5'/%3E%3C/g%3E%3C/svg%3E");

  /* ── Accent (X-style link blue — same hue in both themes, readable on each) ─ */
  --nk-accent: #1D9BF0;             /* links, unread dots, toggles, selected states */
  --nk-accent-rgb: 29, 155, 240;    /* triplet for rgba() accent tints */

  /* ── Interaction / hover ──────────────────────────────────────────────────
     --nk-hover-tint sets the hover DIRECTION: black darkens (light) so a hover
     always reads as a shade; the dark block flips it to white so a hover always
     reads as a highlight. The lift tokens resolve tint + surface at use, so they
     auto-adapt per theme. They double as the var-fallback for profile-colour
     tints, so a missing colour can never make a hover invalid/transparent.
       hover-1 = message row · hover-2 = reply (indirect) · hover-3 = reply (direct) */
  --nk-hover: rgba(0, 0, 0, 0.04);  /* generic translucent hover (notif rows, etc.) */
  --nk-hover-quote: #F8F8F8;
  --nk-hover-tint: #000000;
  --nk-hover-1: color-mix(in srgb, var(--nk-hover-tint) 2.5%, var(--nk-surface));
  --nk-hover-2: color-mix(in srgb, var(--nk-hover-tint) 2.5%, var(--nk-surface));
  --nk-hover-3: color-mix(in srgb, var(--nk-hover-tint) 5%, var(--nk-surface));
  --nk-hl-soft: color-mix(in srgb, var(--nk-hover-tint) 5%, var(--nk-surface)); /* no-colour scroll-to pulse */

  /* ── Profile-colour tint strength (card FILL) ─────────────────────────────
     A user's profile colour is mixed over the surface at these %. The reply mixes
     LESS colour than the post — a very slightly LIGHTER nested panel, same hue — so
     a reply reads as a lighter card inside its message. Dark uses higher % overall
     (a tint needs more over black); the reply stays 2 points under the post there too. */
  --nk-hl-color-pct: 16%;
  --nk-hl-color-pct-quote: 14%;

  /* ── THEME COLOR SETTINGS (Yotsuba-B recipe, OKLCH) ───────────────────────
     A hue-parameterised palette: each numbered SETTING is a fixed OKLCH
     LIGHTNESS + CHROMA step. Feed any source colour through a setting to get
     that colour's tone at that step — the hue comes from the content, the L/C
     from the setting:
         oklch(from <src-colour> var(--set-N-l) var(--set-N-c) h)
     OKLCH (not HSL) so a deep blue stays BLUE when tinted light, instead of
     drifting to lavender the way hsl(240) does. L is 0–1, C is chroma (~0–0.4).
     Change a setting's numbers here and every element built from it re-tints.
     LIVE ASSIGNMENTS:
       1 → PAGE BACKGROUND   (body.page-nuke, logged-in user's hue)
       2 → CHAT CONTAINERS   (.nuke-post + .nuke-post-quote fill, author hue)
       3 → BORDERS/DIVIDERS  (card borders, pfp, reply/cred arrows = author hue;
                              chat-panel + date + site-wide dividers = user hue)
       4 · muted text   5 · link   6 · heading   7 · body text   (not applied yet) */
  /* Values are the TRUE Yotsuba B palette measured in OKLCH (so they match 4chan,
     just re-hued per content): 1=#EEF2FF 2=#D6DAF0 3=#B7C5D9 5=#34345C 6=#0F0C5D. */
  --set-1-l: 0.962; --set-1-c: 0.018;   /* 1 · page bg   (#EEF2FF) — PAGE BACKGROUND */
  --set-2-l: 0.930; --set-2-c: 0.024;   /* 2 · card — pulled CLOSE to the page bg/fade (Yotsuba is .892; user wants cards to blend) — CHAT CONTAINERS */
  --set-3-l: 0.819; --set-3-c: 0.032;   /* 3 · border    (#B7C5D9) — BORDERS/DIVIDERS */
  --set-4-l: 0.55;  --set-4-c: 0.030;   /* 4 · muted / secondary text */
  --set-5-l: 0.345; --set-5-c: 0.069;   /* 5 · link      (#34345C) */
  --set-6-l: 0.240; --set-6-c: 0.134;   /* 6 · heading   (#0F0C5D) */
  --set-7-l: 0.400; --set-7-c: 0.080;   /* 7 · body text — dark but clearly the uploader's hue (readable) */

  /* ── Elevation ────────────────────────────────────────────────────────────
     Light uses dark-cast drop shadows declared inline on each component (reply
     boxes, images). Dark can't (a dark shadow is invisible on black) so it remaps
     them via --nk-shadow-lift in the dark block. */

  /* COLOR SETTING 1 (page background): the LOGGED-IN user's OWN profile hue
     (--current-user-color, tracked on :root) run through setting 1's OKLCH L/C.
     Isolated in --page-bg-themed so a colourless user (var unset → invalid)
     falls cleanly to the neutral --nk-surface. */
  --page-bg-themed: oklch(from var(--current-user-color) var(--set-1-l) var(--set-1-c) h);
  /* Yotsuba B TOP FADE (real fade-blue.png = a 200px vertical fade from #D1D5EE
     down to the #EEF2FF page bg). Re-hued: grad top = the user's hue at L.878/C.035,
     fading over 200px to TRANSPARENT — which reveals the solid `background-color` below (that solid IS
     --page-bg-themed), so the visual is identical to fading to the page bg, but now the SOLID carries
     the whole-page colour and can transition on its own. Colourless → grad-top transparent (no fade) +
     background-color falls to --nk-surface. oklab interpolation keeps the fade hue-true. */
  --page-grad-top: oklch(from var(--current-user-color) 0.878 0.035 h);
  /* Plain PURE WHITE page background — no gradient, no texture (light). Dark mode
     overrides to pure black in the .theme-lightsout block below. */
  background-color: #FFFFFF;
  background-image: none;
  background-repeat: no-repeat;
  background-attachment: fixed;
  /* Smoothly fade the page theme when it changes (e.g. visiting a profile re-themes to that user's
     colour). background-color is a normal animatable property; --page-grad-top is @property-registered
     so the top-fade colour interpolates too. */
  transition: background-color 0.4s ease, --page-grad-top 0.4s ease;
}

/* /nuke page-background layer — a fixed ::before painting the mde.tv "brilliant" fine-grain texture,
   tiled at natural size. The tile is gray + alpha, so the themed --page-bg-themed / --nk-surface
   shows through it. z-index -1 = behind all content, above the body surface colour. */
body.page-nuke::before {
  content: '';
  display: none; /* page texture removed — plain white/black page bg */
  position: fixed;
  inset: 0;
  z-index: -1;
  pointer-events: none;
  background-image: url('/img/ui/brilliant.png');
  background-repeat: repeat;
  background-size: auto;
  background-position: 0 0;
  opacity: 1;
}
/* Full-page boot splash (mounted in index.html). ALWAYS the dark-mode look — solid black + light logo,
   regardless of the user theme; initBootSplash adds .is-hiding to fade it out once the view is ready. */
#nuke-boot-splash {
  background: #000000; /* always dark (index.html + boot-splash.ts set the same inline bg for first paint) */
  transition: opacity 0.4s ease;
  display: flex;
  align-items: center;
  justify-content: center;
}
/* Brand logo (the header's radioactive mark) — large + centered on the load screen. FIXED light colour
   (the dark-mode text tone) so it always reads on the black splash, whatever the theme. */
#nuke-boot-splash .nuke-boot-splash-logo {
  font-size: calc(128px - var(--font-shrink, 0px));
  line-height: 1;
  color: #E7E9EA;
  /* Continuous spin while the app loads (reuses the btn-spin rotate keyframe). */
  animation: btn-spin 2.5s linear infinite;
}
#nuke-boot-splash.is-hiding {
  opacity: 0;
  pointer-events: none;
}

/* --- Pill overrides (nuke-page color/typography family) -------- */

/* Nuke-page pill overrides — soft MSN-style chrome that fits the
   light-blue/white nuke chat aesthetic. Lighter pastel backgrounds
   with dark text for legibility on the white chat bg, plus a thin
   matching border (same var(--nk-border-2) used everywhere else in the nuke
   sandbox) so the pills read as the same component family as the
   chat bubbles and white containers. */
.nuke-container .dock-pill {
  height: auto;
  /* Tight vertical padding + a pinned line-height keep the pill short
     so it clears the 32px header strip with margin to spare (avoids the
     top edge being clipped by the ticker's `overflow-y: hidden`). */
  padding: 1px 9px;
  line-height: 1.3;
  /* Match the local chat-bubble's typography exactly — same font
     family + weight as messages sent by you (the inline `<span>`
     in the bubble template uses `var(--font-conduit)` with no
     explicit weight, so a regular 400 here mirrors it). */
  font-family: var(--font-conduit);
  font-size: calc(13px - var(--font-shrink, 0px));
  font-weight: 500;
  border-radius: 10px;
  border: 1px solid var(--nk-border-2);
  box-shadow: 0 1px 0 rgba(0, 0, 0, 0.02);
}
.nuke-container .pill-red    { background: #FFE5E5; color: #8B1F1F; --pill-bd: #F5C7C7; border-color: var(--pill-bd); }
.nuke-container .pill-orange { background: #FFEEDD; color: #8B5A1F; --pill-bd: #F5D9B7; border-color: var(--pill-bd); }
.nuke-container .pill-blue   { background: #E6F1FF; color: #1F478B; --pill-bd: #C8DDF2; border-color: var(--pill-bd); }
.nuke-container .pill-purple { background: #F0E5FF; color: #5A1F8B; --pill-bd: #DFC8F2; border-color: var(--pill-bd); }
.nuke-container .pill-green  { background: #E5F5E5; color: #1F6B1F; --pill-bd: #C7E2C7; border-color: var(--pill-bd); }
.nuke-container .pill-yellow { background: #FFF5D9; color: #8B6B1F; --pill-bd: #F2E2B0; border-color: var(--pill-bd); }
.nuke-container .pill-transparent {
  background: transparent;
  color: #666;
  border-color: transparent;
  box-shadow: none;
  font-size: calc(12px - var(--font-shrink, 0px));
  font-weight: 500;
}

/* --- Mini-preflight reset (scoped to nuke subtree) ------------- */

/* Mini-preflight for borders, scoped to the nuke sandbox subtree.
   Without this, Tailwind's `border-t` / `border-l` / `border-r` /
   `border-b` only set `border-*-width: 1px` — the other three sides
   default to `border-width: medium` (3px) and `border-style: none`,
   so adding `border-solid` would render borders on all four sides
   instead of just the intended one. Setting `border-width: 0` and
   `border-style: solid` globally for the subtree means
   side-specific Tailwind border classes work the way Tailwind
   documents them. Specificity stays 0,0,0 so explicit `.foo {
   border: 1px solid X }` rules still override. */
.nuke-container *,
.nuke-container *::before,
.nuke-container *::after {
  border-width: 0;
  border-style: solid;
}

/* --- Nuke-container shell -------------------------------------- */

/* ── Tap-and-hold / long-press ────────────────────────────────────────────────
   Suppress the mobile "Save Image / Copy" callout site-wide so the app's chrome ASSETS (pfps, icons, CD
   art, banners, level icons, glyphs…) feel native and can't be lifted off with a long-press. It's an
   INHERITED property, so one rule on the root covers everything; content the user may legitimately want to
   save — post images, the fullscreen lightbox, and presale receipts — opts back IN below. */
body {
  -webkit-touch-callout: none;
}
.nuke-container .nuke-post-img-wrap img,
.nuke-container .nuke-chat-lightbox-img,
.nuke-container .nuke-lightbox-clone,
.nuke-container .nuke-receipt,
.nuke-container .nuke-receipt * {
  -webkit-touch-callout: default;
}

.nuke-container {
  /* Shared size ratio for the post-header @handle pill AND the timestamp next to it,
     so both stay in lockstep — retune once here. Consumed by .nuke-post-handle /
     .nuke-post-time / their reply-quote twins (font-size: calc(1em * this)). */
  --handle-scale: 0.82;
  /* Profile-colour HUE rotation (deg), shared by message cards (.nuke-post fill/border/hover/darken)
     AND sticky notes (.nuke-post-doodle) — renders each per-author colour as a related-but-different
     shade rather than the exact profile colour. One knob moves both. 0 = exact profile colour. */
  --nk-hue-shift: 25;
  /* Chat author-pfp size (single source). The cred badge (.nuke-post-vote) locks its
     WIDTH to this too, so the badge is always exactly one pfp wide, whatever cred sits
     in it — retune once here and both track together. */
  --nuke-chat-pfp-size: 40px;
  /* COLOR SETTING 4 (muted) themed hue — the DEFAULT (not inside a post) is the LOGGED-IN
     user's own hue; .nuke-post / .nuke-post-quote override it to the poster's hue below.
     Consumed by TIMESTAMPS via var(--set4-themed, <neutral>). */
  --set4-themed: oklch(from var(--current-user-color) var(--set-4-l) var(--set-4-c) h);
  /* SVG ICONS use SETTING 6 (heading colour), same in-post-poster / else-you hue rule as above. */
  --icon-themed: oklch(from var(--current-user-color) var(--set-6-l) var(--set-6-c) h);
  /* Hover / press LIFT shadow, shared by every card (message, reply, image, video, doodle, CD).
     A SUBTLE, tight drop-shadow offset DOWN + RIGHT (not a big even spread) — small enough that its
     right edge fits inside the feed's side padding (#nuke-chat-scroll's `contain:paint` clips past
     it). Dark theme remaps the colour to --nk-shadow-lift below (black is invisible on the dark page). */
  --card-lift: 2px 2px 5px rgba(0, 0, 0, 0.1);
  --card-lift-strong: 3px 4px 11px rgba(0, 0, 0, 0.18);
  --card-lift-press: 1px 1px 2px rgba(0, 0, 0, 0.09);
  /* Chat/post MESSAGE text colour — a muted gray blend, NOT pure --nk-text (black/white). Declared HERE,
     not on :root, so var(--nk-text)/(--nk-text-2) resolve against the inherited per-theme values and the
     blend recomputes for light/dark automatically. TUNE the --nk-text share: lower = grayer, higher =
     toward black/white. Used by .nuke-chat-text-self / -remote and .nuke-post-quote-text. */
  /* Card/head base font size — shared by .nuke-post message cards AND the search-dropdown rows so
     they scale together (the 14px body text overrides it per-element). */
  --nk-card-font: calc(15px - var(--font-shrink, 0px));
  --nk-text-msg: color-mix(in srgb, var(--nk-text) 20%, var(--nk-text-2));
  /* Card top-sheen (white top → dark mode, faint dark-bottom → light mode). SHARED here so .nuke-post
     message cards AND the chat-header profile card both inherit it; a .nuke-post-scoped var is invisible
     to the header and invalidates its whole background-image (texture included). */
  --card-highlight: linear-gradient(to bottom, rgba(255, 255, 255, 0.24), rgba(255, 255, 255, 0) 45%, rgba(0, 0, 0, 0) 65%, rgba(0, 0, 0, 0.05));
  /* Box size + position now come from the inset containing-block rule above
     (position:fixed; inset: var(--page-pad) 0 — the 16px top/bottom frame). A stray
     height:100vh / position:relative here would override it and kill the BOTTOM gap. */
  display: flex;
  flex-direction: column;
  overflow: hidden;
  user-select: none;
  /* Reserve space for the fixed-position chrome at the top: the 62px search
     header + the 40px pill header = 102px. The .nuke-page-footer was
     removed, so no bottom reservation is needed — the stage's own 16px
     padding-bottom is the only bottom gap. */
  padding-top: 102px;
  padding-bottom: 0;
  /* Sunghyun Sans is the main site's calling-card font. Bundled
     locally in three weights only (400 / 700 / 900) — no thin variant,
     so secondary fonts on this page stay as the source design's
     inline values. `Noto Color Emoji` is already in the var's fallback
     chain (see :root) so emoji content renders in color here too. */
  font-family: var(--font-conduit);
  font-size: calc(13px - var(--font-shrink, 0px));
  color: var(--nk-text);
  /* Transparent so the fixed `body::before` "brilliant" texture + themed page bg read
     through. The chat feed + side cards keep their own surfaces, so the pattern shows in
     the gutters/gaps (and faintly behind the deliberately-transparent feed). */
  background-color: transparent;
  /* Defined here so `.nuke-stage`, `.nuke-side-left`, and
     `.nuke-side-right` (siblings) can all read these. */
  /* LEFT column holds the profile card + notebook (fixed 320px). */
  --left-col-w: 320px;
  /* The vw-flexed column (now LEFT = chat): 30vw clamped to
     [360px, 480px]. Safe to use vw-based widths here because the
     column is `position: fixed; right: 8px` — its right edge is
     anchored directly to the viewport, so width changes only move
     the LEFT edge (no detachment from the viewport-right edge). */
  /* RIGHT column holds chat/online (the wider, vw-flexed one). */
  /* 384px column = 360px content + the 12px --right-inset on each side. */
  --right-col-w: 398px; /* 350px content (x.com's sidebarColumn) + the 24px --right-inset on each side */
  /* Inner horizontal inset for the right column's content. The column keeps its
     full width; this just pads the content inward by 24px on each side, leaving
     350px of content. */
  --right-inset: 24px;
  /* Shared side inset for the right-column CONTAINERS + the search bar/dropdown
     so they line up on one knob. */
  --right-box-inset: 16px;
  /* Missions column (far right) — matches the profile column width. */
  --missions-col-w: 320px;
  /* X.com-style 3-column layout, centered as a group with a gap between each.
     The middle chat column is the X.com feed width (600px). The left (Profile)
     column is `--col-w`; the right (Shop) column is a fixed `--right-col-w`
     (350px, x.com's sidebar width) that collapses on narrow viewports — see the
     media query at the end of this file. */
  --chat-w: 600px;
  --col-w: clamp(340px, 25vw, 480px);
  /* No inter-column gap — the only spacing between the chat and the side panels
     is each panel's 8px content inset (--right-inset). */
  --col-gap: 0px;
  /* x.com-style left nav rail (replaces the old profile side panel). A fixed,
     narrow icons+labels column glued to the left of the centered feed — see
     `.nuke-nav`. Narrower than the old profile column (`--col-w`).
     284px column → 236px content (284 − 24×2 inset). */
  --nav-col-w: 284px;
  /* The trio (nav | chat | right) is anchored around the chat's center, so when
     the side columns differ in width the GROUP sits off-center. Shift every
     chat-center anchor left by half that imbalance to center all three as a unit.
     Zeroed in the ≤1360px media query (right column hides; nav+chat recenter on
     their own there). */
  --trio-shift: calc((var(--right-col-w) - var(--nav-col-w)) / 2);
  /* Flat 16px gutter on each side, matching the 16px top/bottom gap. The page
     renders at native 1:1 (no zoom/scaling), so the columns just anchor to the
     viewport at this fixed inset on every monitor. */
  --page-edge-x: 16px;
  /* Mobile-build chrome heights — consumed by the responsive tiers at the
     bottom of this file (≤688px): the slim top app bar + the bottom tab bar
     that frame the full-width feed on phones. */
  --m-topbar-h: 48px;
  --m-botnav-h: 46px;
}
.nuke-container.hidden { display: none; }

/* --- Nuke-page header / footer / stage / side columns ---------- */

/* Empty full-width top bar — solid white, spans the whole page at the very top.
   48px tall. Everything else below is anchored 48px lower (+12px gap) to clear
   it. Placeholder; no content yet. */
.nuke-container .nuke-page-topbar {
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  height: 48px;
  /* The element stays full-width (its logo + pill ticker are positioned with 50%
     relative to it), but the white fill is clipped to the panel CONTENT span — the
     left nav's content edge (column + 24px padding) to the right column's content
     edge (column − --right-inset) — so it lines up with the visible cards below,
     not the column boxes. */
  background: linear-gradient(
    to right,
    transparent 0,
    transparent calc(50% - var(--trio-shift) - var(--chat-w) / 2 - var(--col-gap) - var(--nav-col-w) + 24px),
    rgb(var(--nk-surface-rgb))     calc(50% - var(--trio-shift) - var(--chat-w) / 2 - var(--col-gap) - var(--nav-col-w) + 24px),
    rgb(var(--nk-surface-rgb))     calc(50% - var(--trio-shift) + var(--chat-w) / 2 + var(--col-gap) + var(--right-col-w) - var(--right-inset)),
    transparent calc(50% - var(--trio-shift) + var(--chat-w) / 2 + var(--col-gap) + var(--right-col-w) - var(--right-inset)),
    transparent 100%
  );
  z-index: 35;
  /* The top bar sits ABOVE the spotlight scrim (z-28), so the scrim can't darken
     it. Instead dim it with brightness(0.7) — which equals the scrim's
     rgba(0,0,0,.3) — when the spotlight is active (see rule below). Transitioned
     over the scrim's 0.18s so they fade together (no z-index snap). */
  transition: filter 0.18s ease;
}
/* Spotlight active (search hovered/focused, or a profile hovercard open) → dim the
   top bar in lockstep with the rest of the page. */
.nuke-container:has(.nuke-subheader-search .user-search-bar:hover) .nuke-page-topbar,
.nuke-container:has(.nuke-subheader-search:focus-within) .nuke-page-topbar {
  filter: brightness(0.4);
}
/* Brand logo in the header — left-aligned over the nav column (its left edge +
   the nav's 12px inset), the same horizontal alignment it had in the left rail.
   Vertically centered in the bar (transform-free; margins handle the 50px button
   in the 48px bar). */
.nuke-container .nuke-page-topbar .nuke-nav-logo {
  position: absolute;
  left: calc(50% - var(--trio-shift) - var(--chat-w) / 2 - var(--col-gap) - var(--nav-col-w) + 24px);
  top: 0;
  bottom: 0;
  margin: auto 0;
}
/* Recent-actions pill ticker, hosted in the top bar. Spans from the chat
   column's LEFT edge to the right panel's RIGHT edge (chat is centered at
   width --chat-w; the right panel ends at 50% + chat-w/2 + col-gap + right-col-w),
   and is vertically centered in the bar (overrides the old bottom-aligned band). */
.nuke-container .nuke-page-topbar .nuke-pill-header {
  position: absolute;
  top: 0;
  bottom: 0;
  height: auto;
  left: calc(50% - var(--trio-shift) - var(--chat-w) / 2);
  right: calc(50% + var(--trio-shift) - var(--chat-w) / 2 - var(--col-gap) - var(--right-col-w) + var(--right-inset));
  align-items: center;
  /* First pill sits flush at the left edge; 12px trailing gutter at the right
     (the original gutters — the leading/trailing padding scrolls off-screen once
     the nav buttons appear, so button clearance is handled by the frosted edges,
     not here). */
  padding-left: 0;
  padding-right: 12px;
  /* Alpha mask: pills are fully HIDDEN only at the very outer edge (0→l1), then
     ramp to opaque quickly by l2 — kept SHORT (much less than the ~80px frost) so
     the pill becomes opaque well within the blur zone. The frosted edge (below)
     then blurs that now-visible pill, tapering to crisp ~140px in: hidden sliver →
     opaque blurred → crisp. Per-side vars collapse to 0 when that end's nav button
     is hidden, leaving the first/last pill crisp. */
  --pe-l1: 20px;
  --pe-l2: 60px;
  --pe-r1: 20px;
  --pe-r2: 60px;
  -webkit-mask: linear-gradient(to right, transparent 0, transparent var(--pe-l1), #000 var(--pe-l2), #000 calc(100% - var(--pe-r2)), transparent calc(100% - var(--pe-r1)), transparent 100%);
  mask: linear-gradient(to right, transparent 0, transparent var(--pe-l1), #000 var(--pe-l2), #000 calc(100% - var(--pe-r2)), transparent calc(100% - var(--pe-r1)), transparent 100%);
}
/* Left button hidden (at the start) → no left hide-mask, first pill crisp. */
.nuke-container .nuke-page-topbar:has(.nuke-pill-scroll-cap--left.is-hidden) .nuke-pill-header {
  --pe-l1: 0px;
  --pe-l2: 0px;
}
/* Right button hidden (at the end) → no right hide-mask, last pill crisp. */
.nuke-container .nuke-page-topbar:has(.nuke-pill-scroll-cap--right.is-hidden) .nuke-pill-header {
  --pe-r1: 0px;
  --pe-r2: 0px;
}
/* Frosted edges, layered OVER the strip's hide-mask: a graduated blur (no white
   tint — the tint washed out the blur; the strip's alpha mask already lightens the
   edge) that sits across the strip's reveal ramp, so a pill emerging from the hidden
   outer edge appears blurred first, then sharpens. `backdrop-filter` blurs the pills
   behind; the layer's own `mask` ramps the blur from strongest at the very edge to
   nothing ~140px inward. Sits below the nav buttons (z-2) and above the pill strip. */
.nuke-container .nuke-page-topbar::before,
.nuke-container .nuke-page-topbar::after {
  content: '';
  position: absolute;
  top: 0;
  bottom: 0;
  width: 140px;
  z-index: 1;
  pointer-events: none;
  backdrop-filter: blur(4px);
  -webkit-backdrop-filter: blur(4px);
  /* Swift fade so a side's frost appears/vanishes with its nav button. */
  transition: opacity 0.16s ease;
}
.nuke-container .nuke-page-topbar::before {
  left: calc(50% - var(--trio-shift) - var(--chat-w) / 2);
  -webkit-mask: linear-gradient(to right, #000, transparent);
  mask: linear-gradient(to right, #000, transparent);
}
.nuke-container .nuke-page-topbar::after {
  right: calc(50% + var(--trio-shift) - var(--chat-w) / 2 - var(--col-gap) - var(--right-col-w) + var(--right-inset));
  -webkit-mask: linear-gradient(to left, #000, transparent);
  mask: linear-gradient(to left, #000, transparent);
}
/* Left button hidden (scrolled to the start) → no left frost, first pill crisp. */
.nuke-container .nuke-page-topbar:has(.nuke-pill-scroll-cap--left.is-hidden)::before {
  opacity: 0;
}
/* Right button hidden (scrolled to the end) → no right frost, last pill crisp. */
.nuke-container .nuke-page-topbar:has(.nuke-pill-scroll-cap--right.is-hidden)::after {
  opacity: 0;
}
/* Round nav button inset from each end of the pill strip. The frosted edge
   (topbar ::before / ::after) blurs the pills as they slide past, so the buttons
   read as floating scroll controls (X-style) rather than hard white blocks. */
.nuke-container .nuke-page-topbar .nuke-pill-scroll-cap {
  position: absolute;
  /* Center vertically WITHOUT a transform (top/bottom + auto margins): a
     `translateY(-50%)` under the container's `zoom` lands on a fractional pixel,
     which the browser snaps differently when the opacity fade promotes/demotes
     the layer — causing the arrow to jump a couple px. This is transform-free. */
  top: 0;
  bottom: 0;
  margin-top: auto;
  margin-bottom: auto;
  width: 28px;
  height: 28px;
  box-sizing: border-box;
  /* Match the cred pill (.nuke-post-vote): #F7F9FA fill + 1px var(--nk-border) border. */
  background: var(--nk-surface-2);
  border: 1px solid var(--nk-border);
  border-radius: 50%;
  display: flex;
  align-items: center;
  justify-content: center;
  color: var(--nk-text-2);
  line-height: 1;
  cursor: pointer; /* clicking snaps to the next / previous pill */
  z-index: 2; /* above the pill strip so it sits over the fading pills */
  /* Swift fade in/out, toggled by `.is-hidden` (JS) when that end of the strip
     is reached / there's nothing more to scroll. `will-change` keeps it on a
     stable composited layer so the fade doesn't re-snap its position. */
  transition: opacity 0.16s ease, background 0.12s ease;
  will-change: opacity;
}
/* Inline arrow icon (was the Rosetta F172/F173 glyph): ~13px tall, currentColor
   inherits the cap's --nk-text-2 so it keeps the same look. */
.nuke-container .nuke-page-topbar .nuke-pill-scroll-cap svg {
  display: block;
  height: 13px;
  width: auto;
}
/* Round scroll buttons sit flush on each strip edge (no inset). */
.nuke-container .nuke-page-topbar .nuke-pill-scroll-cap--right {
  right: calc(50% + var(--trio-shift) - var(--chat-w) / 2 - var(--col-gap) - var(--right-col-w) + var(--right-inset));
}
.nuke-container .nuke-page-topbar .nuke-pill-scroll-cap--left {
  left: calc(50% - var(--trio-shift) - var(--chat-w) / 2);
}
/* Reached that end (or no overflow) → fade the whole cap group out (opacity
   covers the white box, the arrow, AND the `::before` fade). */
.nuke-container .nuke-page-topbar .nuke-pill-scroll-cap.is-hidden {
  opacity: 0;
  pointer-events: none;
}
/* Top header bar, fixed at the very top. 54px tall = the 38px search bar +
   8px padding above and below. No background — only the search bar inside it
   is visible. */
/* Full-width page header band pinned to the very top of the page, above every
   panel. --page-header-h is the single knob that pushes the nav / stage / search /
   right column down (each adds it to its top offset). Empty scaffold for now —
   content + final styling wired next. */
.nuke-container { --page-header-h: 56px; }
.nuke-container .nuke-page-header {
  position: fixed;
  top: 0;
  /* Full-width: the header BACKGROUND spans the whole viewport; the logo + search
     inside stay over the trio (they position off viewport 50%). */
  left: 0;
  right: 0;
  height: var(--page-header-h);
  /* Header fill = a DARK, SATURATED take on the logged-in user's profile
     colour (low L = dark, high C = saturated); updates live with whoever
     is signed in. */
  /* Colourless → the grayscale twin at the same dark lightness (no hue) — how the profile-coloured fill would
     look with a "colourless" profile colour — so the header still reads as a dark band. */
  --page-header-fill: oklch(from var(--current-user-color) 0.42 0.12 h);
  background: var(--page-header-fill, oklch(0.42 0 0));
  /* BELOW the dim scrim (z-28) so the search-focus / profile-hovercard page-dim
     darkens the header too; still ABOVE the columns (z-25) so its shadow shows.
     The search itself stays bright at z-45. */
  z-index: 27;
  box-sizing: border-box;
  /* 2px bottom border = a slightly DARKER take on the header fill (L × 0.88), tracking the profile colour.
     Drawn as an INSET shadow (not a real border) so it never affects the header contents' placement. */
  box-shadow:
    inset 0 -2px 0 0 oklch(from var(--page-header-fill, oklch(0.42 0 0)) calc(l * 0.88) c h),
    0 2px 6px rgba(0, 0, 0, 0.14);   /* soft drop shadow cast onto the content below */
}
/* Dark-mode header fill — mirrors the light rule but darker, so the header
   stays a distinct band on the dimmed page (tune L = darkness, C = saturation). */
.theme-lightsout .nuke-container .nuke-page-header {
  --page-header-fill: oklch(from var(--current-user-color) 0.27 0.08 h);
  background: var(--page-header-fill, oklch(0.27 0 0));
}
/* SMOOTH THEME FADE — when the page theme colour changes (e.g. visiting a profile re-themes the whole
   page to THAT user's colour), fade the structural chrome that carries it alongside the page background:
   the top header (--page-header-fill + --nk-border-themed divider), the left nav panel + chat headers
   (--nk-border-themed fill), the profile header card (self-tinted), the RIGHT-panel widget headers
   (--sbh-fill profile fill, like the page header) and every plain-box's themed border. These chrome
   containers have no hover COLOUR states, so transitioning bg/border/shadow here never slows an
   interactive hover. The page background (body.page-nuke) + subview overlay carry their own transitions. */
.nuke-container .nuke-page-header,
.nuke-container .nuke-nav,
.nuke-container .nuke-chat-header,
.nuke-container .nuke-chat-header-2,
.nuke-container .nuke-side-box-header,
.nuke-container .nuke-plain-box {
  transition: background-color 0.4s ease, border-color 0.4s ease, box-shadow 0.4s ease;
}
/* Brand logo relocated from the left nav into the page header. Kept over the nav
   column (nav-left + the column's 24px inset — same X as the old nav-list logo) and
   vertically centred in the header band. */
.nuke-container .nuke-page-header .nuke-nav-logo-item {
  position: absolute;
  /* CENTRE chat-panel left — the same X as the subview title (Post / Notifications), so opening a subview
     replaces the wordmark with the header TEXT in place (the back button lives out in the nav column). */
  left: calc(50% - var(--trio-shift) - var(--chat-w) / 2);
  top: 0;
  bottom: 0;
}
/* Header logo: white glyph + no hover wash (nav-item hover bg suppressed here). */
.nuke-container .nuke-page-header .nuke-nav-logo-item .nuke-nav-icon {
  color: #fff;
}
/* Brand wordmark — "Nuke Network" in Marlboro, white. Structured to MIRROR the subview title EXACTLY so
   the two land at the identical vertical position: the logo-text is a 60px flex-centred box = the
   `.nuke-post-modal-bar-top` the title lives in, and 8px left padding = the bar-top's padding. */
.nuke-container .nuke-page-header .nuke-nav-logo-item .nuke-nav-logo-text {
  height: 60px;              /* = .nuke-post-modal-bar-top */
  display: flex;
  align-items: center;
  padding-left: 8px;         /* = the bar-top's 8px, so the text's X matches the title */
  font-family: var(--font-marlboro);
  font-size: calc(32px - var(--font-shrink, 0px));
  font-weight: 400;
  line-height: 1;
  color: #fff;
  white-space: nowrap;
  /* Same nudge as the title — Marlboro's caps sit high in the em box. */
  transform: translateY(0.09em);
}
/* The item itself mirrors `.nuke-page-subview-bar`: a full-height column that vertically centres its 60px
   text box (the same way the bar centres its bar-top). */
.nuke-container .nuke-page-header .nuke-nav-item.nuke-nav-logo-item {
  padding: 0;
  display: flex;
  flex-direction: column;
  justify-content: center;
}
.nuke-container .nuke-page-header .nuke-nav-logo-item:hover {
  background-color: transparent;
}
.nuke-container .nuke-subheader {
  position: fixed;
  top: 0; /* lifted UP into the page-header band — the search now lives in the header */
  /* Over the RIGHT (profile) column ONLY — a header for that column, X-style.
     The chat + Shop columns have no page header above them, so they reach the
     very top of the page. */
  left: calc(50% - var(--trio-shift) + var(--chat-w) / 2 + var(--col-gap));
  right: auto;
  width: var(--right-col-w);
  /* Fill the page-header band so the search vertically centres inside it. */
  height: var(--page-header-h);
  background: transparent;
  /* Between the two scrim states: ABOVE the search-focus scrim (z-28) so the search
     stays bright when YOU focus it, but BELOW the hovercard scrim (z-45) so a profile
     hovercard dims the search along with the rest of the page. Still above the header
     fill (z-27) so it renders on the header band. */
  z-index: 30;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 0;
}
/* Search row — same width as the right (profile) column (`--col-w`), aligned
   over it. */
.nuke-container .nuke-subheader-search {
  width: var(--right-col-w);
  flex: 0 0 auto;
  /* Inset the collapsed search bar from the column edges (border-box keeps the
     column at --right-col-w). */
  padding-left: var(--right-inset);
  padding-right: var(--right-inset);
}

/* Spotlight scrim (prod /home parity): a full-page dim shown while the search
   is hovered or focused. Sits ABOVE the columns + chat stage (z-index 25) but
   BELOW the search subheader (30) and its focused dropdown (40), so everything
   dims except the search itself — no z-index lifting needed. */
.nuke-container .nuke-hover-spotlight {
  position: fixed;
  inset: 0;
  background: rgba(0, 0, 0, 0.6);
  opacity: 0;
  pointer-events: none;
  /* z-index revert is DELAYED 0.18s (past the opacity fade) so on close the scrim keeps
     covering the search/top bar until they've fully un-dimmed — see the hovercard rule. */
  transition: opacity 0.18s ease, z-index 0s linear 0.18s;
  z-index: 28;
}
.nuke-container:has(.nuke-subheader-search .user-search-bar:hover) .nuke-hover-spotlight,
.nuke-container:has(.nuke-subheader-search:focus-within) .nuke-hover-spotlight {
  opacity: 1;
}
/* Hovercard open: dim the page like the search bar does — but DELAY the fade-in (0.18s) so
   it only darkens AFTER the card has popped in over the avatar. The feed is its own stacking
   context (`.nuke-chat-container { isolation: isolate }`), so the scrim would dim the lifting
   pfp too — but by 0.18s the card's opaque avatar already covers it, so the dim lands on the
   background only and the pfp never visibly darkens. z-index flips immediately (the scrim is
   transparent until the delayed opacity fades in). On CLOSE the base rule's transition (no
   delay) un-dims promptly. */
.nuke-container:has(.nuke-hovercard.is-visible) .nuke-hover-spotlight {
  opacity: 1;
  z-index: 45;
  /* Fades in WITH the lift/pop (no delay), on an EASE-IN curve so it stays low through the
     first ~0.11s — while the message pfp is briefly uncovered — then ramps up quickly as the
     card's opaque avatar takes over. Concurrent + fluid. On CLOSE the base rule's quick
     ease-out un-dims. */
  transition: opacity 0.2s cubic-bezier(0.45, 0, 0.9, 1), z-index 0s linear 0s;
}

/* Kill the PROD header's leftover spotlight overlay on this page. `#hover-spotlight`
   (styles.css, z-90) is a sibling OUTSIDE `.nuke-container`, but it's still switched on
   by initHoverSpotlight() — which toggles `body.spotlight-search` whenever this page's
   search (a shared `.user-search-wrapper`) is hovered/focused. At z-90 it paints OVER
   the search subheader (z-30): the search can't escape its z-30 stacking context to
   reach prod's z-95 wrapper-lift, so the whole bar + dropdown gets dimmed along with
   the page. This page has its OWN scrim (`.nuke-hover-spotlight`, z-28, correctly
   below the subheader), so the prod one is pure interference — suppress it while /nuke
   is visible. The nuke scrim still dims the page; the search stays bright. (Not
   `.nuke-container`-scoped because the target lives outside that subtree.) */
body:has(#nuke-container:not(.hidden)) #hover-spotlight {
  display: none;
}

/* Recent-actions ticker inside the page header. Horizontal strip of
   `.dock-pill` bubbles (pastel colors from the pill overrides above),
   newest-first, scrolls/drags sideways. Fills the header width; the
   scrollbar is hidden so it reads as a clean ticker. Pills + their
   render/diff logic are shared with the prod chat header (chat.ts
   `populateHeaderPills`). */
.nuke-container .nuke-pill-header {
  flex: 1 1 auto;
  min-width: 0;
  height: 100%;
  display: flex;
  /* Pills sit at the BOTTOM of the header band (the ticker fills the header's
     full height), so the extra header height opens a gap above them — away
     from the profile header. */
  align-items: flex-end;
  gap: 6px;
  /* The header is now one column wide (over the left/Shop column), so the
     pills fill it edge-to-edge and scroll within it. */
  margin-left: 0;
  /* Trailing gutter so the last pill isn't jammed against the edge; an 8px
     leading gutter sets the first pill's distance from the left edge. Lives as
     container padding (not a header inset) so it scrolls WITH the content. */
  padding: 0 12px 0 8px;
  overflow-x: auto;
  overflow-y: hidden;
  scrollbar-width: none;
  user-select: none;
  /* Own compositor layer so scrolling (native AND the JS drag) translates the
     layer on the GPU rather than repainting the pills + their pfp gloss every
     frame on the main thread, which the page-wide zoom otherwise forces. This
     is what brings the drag up to native-trackpad smoothness. */
  transform: translateZ(0);
}
/* Custom grab cursor (site's Notwaita hand) over the whole strip at
   rest, including the pills — the base `.dock-pill` sets
   `cursor: default`, so the pills are listed explicitly to override it.
   Same asset + hotspot as `.cursor-grab` in styles.css; `!important`
   matches the cursor system's convention there. Gated on `:has(.dock-pill)`
   so an EMPTY header (no pills = nothing to drag) shows the normal cursor,
   not a grab hand. */
.nuke-container .nuke-pill-header:has(.dock-pill),
.nuke-container .nuke-pill-header:has(.dock-pill) .dock-pill {
  cursor: grab !important;
}
/* Username links inside a pill are the only clickable part (they open
   the user's profile), so they get the site's custom pointer/click
   cursor (hand2) instead of the strip's grab. Targets the link + its
   inner name/pfp so the whole clickable area reads as clickable. Same
   asset + hotspot as the pointer rule in styles.css. */
.nuke-container .nuke-pill-header .pill-user-link,
.nuke-container .nuke-pill-header .pill-user-link * {
  cursor: pointer !important;
}
/* Active-drag state: no closed-fist "grabbing" asset exists, so use the
   site's `move` cursor (4-arrow) as the dragging affordance. Same asset
   + hotspot as `.cursor-move` in styles.css. The `.dragging` variants for
   the username link carry extra specificity so a drag-in-progress reads
   as move everywhere, even over a clickable name. */
.nuke-container .nuke-pill-header.dragging,
.nuke-container .nuke-pill-header.dragging .dock-pill,
.nuke-container .nuke-pill-header.dragging .pill-user-link,
.nuke-container .nuke-pill-header.dragging .pill-user-link * {
  cursor: move !important;
}
.nuke-container .nuke-pill-header::-webkit-scrollbar {
  display: none;
}
/* Pill pfps + usernames styled like the chat's — 20px pfp + bold name. The
   pill grows a touch so the 20px pfp fits (base pill is 20px tall). */
.nuke-container .nuke-pill-header .pill-pfp {
  width: 24px;
  height: 24px;
  transform: none;
  /* Slightly-rounded square — tracks the shared --pfp-radius (the inner img is
     clipped to this). */
  border-radius: var(--pfp-radius);
  /* Flat — no drop shadow on the pill avatars. */
  box-shadow: none;
  /* 1px border (same thickness as the `.dock-pill` border) painted in the
     EXACT same color the parent pill uses for its own border — both pull from
     the shared `--pill-bd` variable so they can never drift apart. */
  border: 1px solid var(--pill-bd, var(--nk-border-2));
  /* Keep the 24px footprint with the border inside it. */
  box-sizing: border-box;
}
/* Drop the PFP design-system's 3D rim (lit-top / dark-bottom inset edge) on the
   ticker pfp so its only visible edge is the flat `--pill-bd` border above —
   matching the pill it sits in instead of the rim's grey/dark bevel. */
.nuke-container .nuke-pill-header .pill-pfp::after {
  display: none;
}
/* Flex the whole pill row (like the chat chain header) so the name AND the
   words around it center on the same line. `gap` restores the single space
   that flex collapses at each flex-item boundary (the template's spaces around
   every pill-user-link). */
.nuke-container .nuke-pill-header .pill-content {
  display: flex;
  align-items: center;
  gap: 0.25em;
}
.nuke-container .nuke-pill-header .pill-user-link {
  display: inline-flex;
  align-items: center;
}
.nuke-container .nuke-pill-header .dock-pill {
  /* Equal padding on all sides (4px) so the inline pfp sits the same distance
     from the pill's left edge as from its top/bottom. Content-sized. */
  padding: 4px;
  /* Uniform height for every pill: the pfp pill's full box — 24px pfp + 4px×2
     padding + 1px×2 border = 34px. `border-box` makes `min-height` count the
     whole box, so text-only pills grow to 34px instead of sitting shorter;
     `align-items: center` (base) keeps the short text centered. */
  box-sizing: border-box;
  min-height: 34px;
  /* Match the player-list banner's corner radius (6px) instead of the base 9px. */
  border-radius: 6px;
  /* Flat — no drop shadow on the pills. */
  box-shadow: none;
}
.nuke-container .nuke-pill-header .pill-name {
  font-weight: 600;
}

/* --- Profile card banner (profile tab) ------------------------- */
/* Calling-card banner — sits on the right side of the header,
   behind the PFP and text via z-index: -1 (header creates its own
   stacking context via `isolate`). 224×56 matches the prod 4:1
   banner aspect ratio scaled to the card's 56px height. Mask fades
   the left side so the text/icons stay legible; always visible (no
   hover gate, unlike the contact-row banners). */
.nuke-container .nuke-profile-name {
  font-weight: 600;
  font-size: calc(14px - var(--font-shrink, 0px));
  color: var(--nk-text);
}
.nuke-container .nuke-profile-status {
  color: #808080; /* same as `.msn-text-grey` */
}
.nuke-container .nuke-stage {
  /* MIDDLE column of the centered trio — the chat, now a fixed centered column
     (was a relative element squeezed to the right via padding). Same top/bottom
     band and z-layer as the side columns. */
  position: fixed;
  /* Below the page header (was flush to the page top). */
  top: var(--page-header-h);
  bottom: 0;
  left: calc(50% - var(--trio-shift));
  transform: translateX(-50%);
  width: var(--chat-w);
  min-height: 0;
  display: flex;
  flex-direction: column;
  gap: 8px;
  z-index: 25;
}

.nuke-container .nuke-side-right {
  /* Anchored directly to the viewport's left/right edge (same approach as
     `.nuke-page-header` / `.nuke-page-footer`). No layout-chain dependency, so no
     resize-detachment frame. The search bar is fixed at top:52.5px, height 45px →
     its bottom edge is 97.5px; top = 97.5 + 16 = 113.5px puts the first container
     (shop) 16px below it, and the `gap` below spaces shop → missions by the same
     16px — so search, shop and missions are all evenly 16px apart. */
  position: fixed;
  top: var(--page-header-h); /* right below the header so the panel bg starts there; 16px box gap kept via padding-top */
  /* Spans the full height down to the page bottom (no empty footer band).
     The right column overrides this to reserve the 8px XP bar. */
  bottom: 0;
  display: flex;
  flex-direction: column;
  gap: 16px;
  z-index: 25;
  /* Isolate layout / style so internal updates in one panel
     (e.g. chat scroll position, message addition) don't invalidate
     rendering work on the others or the nuke-stage. Paint containment
     is intentionally omitted because it clips child box-shadows to
     the panel's bounds — children (window-frames + notebook) need
     their drop-shadows to extend outside the panel. */
  contain: layout style;
}

/* --- JACKPOT marquee banner (top of the right column, under the search) ----- */
.nuke-container .nuke-jackpot {
  /* "JACKPOT" marquee: bulb lights around a rectangular outline — NO filled
     plaque/background, and NO clipping (so the corner bulbs aren't cut off) —
     plus a bright cluster that travels the perimeter, gold text in the middle.
     Top of the right column, under the search bar. */
  --cell: 15px;          /* bulb spacing */
  position: relative;
  flex: 0 0 auto;
  height: 64px;
  /* Hidden for now (markup + styles kept) — change to `flex` to restore it. */
  display: none;
  align-items: center;
  justify-content: center;
}
/* Bulbs evenly spaced via `space` — it pins one bulb to each corner so adjacent
   edges share the corner (no overlap/crowding) and stays even at any width. */
.nuke-container .nuke-jackpot::before {
  content: "";
  position: absolute;
  inset: 0;
  border-radius: inherit;
  pointer-events: none;
  --jp-dot: radial-gradient(circle at center,
    #ffe6a8 0 30%, #dd9540 30% 44%, rgba(220, 140, 60, 0) 60%);
  background:
    var(--jp-dot) left top    / var(--cell) var(--cell) space no-repeat,
    var(--jp-dot) left bottom / var(--cell) var(--cell) space no-repeat,
    var(--jp-dot) left top    / var(--cell) var(--cell) no-repeat space,
    var(--jp-dot) right top   / var(--cell) var(--cell) no-repeat space;
}
/* The "spin": a bright cluster of ~3 bulbs that travels around the PERIMETER at
   constant speed (not an angular sweep). `.nuke-jackpot-lights` is a static layer
   masked to the bulbs + screen-blended; inside it the `.nuke-jackpot-spark` comet
   rides the rounded-rect perimeter via offset-path, lighting whichever bulbs it
   covers. Path inset by cell/2 so it runs along the bulb row. */
.nuke-container .nuke-jackpot-lights {
  position: absolute;
  inset: 0;
  border-radius: inherit;
  pointer-events: none;
  mix-blend-mode: screen;
  --jp-mask: radial-gradient(circle at center, #000 0 44%, rgba(0, 0, 0, 0) 60%);
  -webkit-mask:
    var(--jp-mask) left top    / var(--cell) var(--cell) space no-repeat,
    var(--jp-mask) left bottom / var(--cell) var(--cell) space no-repeat,
    var(--jp-mask) left top    / var(--cell) var(--cell) no-repeat space,
    var(--jp-mask) right top   / var(--cell) var(--cell) no-repeat space;
  mask:
    var(--jp-mask) left top    / var(--cell) var(--cell) space no-repeat,
    var(--jp-mask) left bottom / var(--cell) var(--cell) space no-repeat,
    var(--jp-mask) left top    / var(--cell) var(--cell) no-repeat space,
    var(--jp-mask) right top   / var(--cell) var(--cell) no-repeat space;
}
.nuke-container .nuke-jackpot-spark {
  position: absolute;
  left: 0;
  top: 0;
  width: calc(var(--cell) * 2.8);
  height: calc(var(--cell) * 2.8);
  background: radial-gradient(circle, #fff7df 0 24%, rgba(255, 247, 223, 0) 60%);
  /* Square-cornered rectangle path inset by cell/2 so it runs along the bulb row
     and passes through the corner bulbs. */
  offset-path: inset(calc(var(--cell) / 2));
  offset-anchor: center;
  offset-distance: 0%;
  animation: nuke-jackpot-travel 9s linear infinite;
}
@keyframes nuke-jackpot-travel { to { offset-distance: 100%; } }
@media (prefers-reduced-motion: reduce) {
  .nuke-container .nuke-jackpot-spark { animation: none; }
}
/* Gold, beveled "JACKPOT" lettering. */

/* --- Decorative radio (below the notebook; static — no playback wired up) ---- */
/* Floating radio — COLLAPSED: a red rounded-square play FAB pinned to the screen's
   bottom-right (Grok-style). Clicking play expands it (.is-playing) into the full
   radio row. Fixed + a direct child of .nuke-container so it floats at the true
   screen corner (escapes the side panel's layout containment). */
/* Live video companion — sits flush ON TOP of the expanded radio so the two read as
   ONE container: rounded TOP corners, square BOTTOM (meets the radio's squared top),
   no gap. 230px (the expanded radio width) at a 16:9 ratio. Shown only while playing. */

/* ── Radio bar ────────────────────────────────────────────────────────────────
   The station control, moved from the floating FAB into the feed (docked under the profile
   header). Reads like a native panel row: transparent with only a bottom divider (the same
   colour as the post divider), the play button sitting on the post pfp column, then the
   now-playing title. The old FAB + its video companion are hidden. */
.nuke-container .nuke-radio-bar {
  flex: 1;                                          /* MOVED into the feed-tab header: take the slack next to the Feed btn */
  min-width: 0;
  display: flex;
  flex-direction: row;                             /* LCD + controls INLINE (was a stacked column) */
  align-items: center;
  gap: 8px;
  /* No side inset: the ::after dividers are gone, and the LCD (.nuke-radio-bar-info) now sits FLUSH against
     the header's gray panel frame so the edge reads gray, white, colour — continuous, like the stream. */
  padding: 0;
  position: relative;                              /* anchors the bar-spectrum canvas behind the text */
}
/* Audio bar-spectrum canvas — absolutely filling the radio bar, BEHIND the text/controls. */
.nuke-container .nuke-radio-viz-bar {
  position: absolute;
  inset: 0;
  z-index: 0;
  width: 100%;
  height: 100%;
  pointer-events: none;
}
/* Controls row under the LCD: play, skip, then the volume slider filling the rest. */
.nuke-container .nuke-radio-bar-controls {
  position: relative;
  z-index: 1;
  flex: 0 0 auto;
  display: flex;
  align-items: center;
  gap: 10px;
}
.nuke-container .nuke-radio-bar-btn {
  flex: 0 0 auto;
  width: 30px;
  height: 30px;
  border: none;
  border-radius: 50%;
  background: #e0566c;
  color: #fff;
  display: flex;
  align-items: center;
  justify-content: center;
  cursor: pointer;
}
.nuke-container .nuke-radio-bar-btn svg { width: 18px; height: 18px; }
/* Video toggle — secondary circular button next to Play; opens the live-stream video panel below the bar. */
.nuke-container .nuke-radio-bar-video {
  flex: 0 0 auto;
  width: 26px;
  height: 26px;
  border: none;
  border-radius: 50%;
  background: var(--nk-hover-1);
  color: var(--nk-text-2);
  display: flex;
  align-items: center;
  justify-content: center;
  cursor: pointer;
  transition: background 0.12s ease, color 0.12s ease;
}
.nuke-container .nuke-radio-bar-video svg { width: 16px; height: 16px; }
.nuke-container .nuke-radio-bar-video:hover { background: var(--nk-border-soft); color: var(--nk-text); }
.nuke-container .nuke-radio-bar-video:active { transform: scale(0.94); }
/* Active (video panel open) → LCD green, tying the button to the live stream. */
.nuke-container .nuke-radio-bar-video.is-active { background: #0a1a10; color: #5fd98a; }
/* Live-stream video panel below the radio bar (toggled open by the video button). */
.nuke-container .nuke-radio-video-panel {
  display: none;
  /* Full-bleed video — no side, top, or bottom gaps (matches the edge-to-edge feed). */
  padding: 0;
  /* Bottom divider = the pinned header's lower edge against the scrolling feed. NO side frame here — the
     stream now lives INSIDE .nuke-chat-header, which already draws the gray side frame; the CRT screen's
     own white/colour bevel sits just inside it (so the edge reads gray, white, colour, like the radio). */
  border-bottom: var(--nk-panel-border-width) solid var(--nk-border-soft);
}
.nuke-container .nuke-radio-video-panel.is-open { display: block; }
/* The framed CRT "screen": the message-card frame (NPC grey) lives HERE; the video + the CRT overlays sit
   inside and clip to the SQUARE corners (border-radius: 0). */
.nuke-container .nuke-radio-video-screen {
  position: relative;
  width: 100%;
  aspect-ratio: 16 / 9;
  box-sizing: border-box;
  overflow: hidden;
  isolation: isolate;
  /* The 4px black frame is PADDING (the screen's #000 bg showing through). The CRT overlays (scanlines /
     static / vignette), the visualizer canvas and the difference-blend are all inset by the 4px frame to the
     content box, so the frame + border edges stay CLEAN — the effects never bleed onto them. */
  padding: 4px;
  /* Regular (non-white) edges use the SELF profile colour in BOTH themes: --nk-border-themed is
     profile-derived (oklch from --current-user-color) light + dark; dark's pure-gray --nk-border-soft
     was the wrong source. */
  --stream-edge: var(--nk-border-themed, var(--nk-border));
  border: var(--nk-panel-border);
  border-color: var(--stream-edge);   /* themed right/bottom (top width 0; left set white below) */
  border-width: 2px;   /* 2px coloured edge (was 1px from the token) */
  border-top-width: 0; /* no top border */
  border-left: 1px solid #FFFFFF;   /* white highlight — matches the post left border (softened to 0.22 in dark via shared override) */
  border-radius: 0;
  background: #000;
  box-shadow: inset 1px 0 0 0 var(--stream-edge);   /* 1px profile-coloured inner, just inside the white left */
  /* Gentle live-signal wobble on the whole screen (brightness flicker + a hair of jitter). */
  animation: crt-flicker 0.15s steps(2, end) infinite;
}
.nuke-container .nuke-radio-video-el {
  display: block;
  position: relative;
  z-index: 3;
  mix-blend-mode: difference;   /* stream blends OVER the visualizer behind it */
  width: 100%;
  height: 100%;
  background: #000;
  object-fit: contain;
  animation: crt-jitter 0.15s steps(2, end) infinite;
}
/* Butterchurn (WebGL Milkdrop) trippy visualizer canvas — over the stream, shown by JS only when running.
   If it can't run, nothing shows here. Removable — see stream-butterchurn.ts. */
.nuke-container .nuke-radio-viz-gl {
  position: absolute;
  /* <canvas> is a REPLACED element: inset alone cannot stretch it (falls back to intrinsic 300x150), and
     width/height:100% resolve against the PADDING box (bezel included) → 4px overflow on bottom+right that
     leaked the visualizer into the bezel. Pin top/left at the 4px frame and size to the CONTENT box via calc
     (100% = padding-box, minus the 8px of padding) so all four sides land exactly on the picture. */
  top: 4px;
  left: 4px;
  width: calc(100% - 8px);
  height: calc(100% - 8px);
  z-index: 2;
  pointer-events: none;
  display: none;
  opacity: 0.2;
}
/* Scanlines + a curved-tube vignette, above the video. */
.nuke-container .nuke-radio-video-screen::before {
  content: '';
  position: absolute;
  inset: 4px;   /* scanlines + vignette confined to the picture, off the frame */
  z-index: 5;
  pointer-events: none;
  background:
    radial-gradient(70% 55% at 28% 12%, rgba(255, 255, 255, 0.06), rgba(255, 255, 255, 0) 60%),
    repeating-linear-gradient(0deg, rgba(0, 0, 0, 0) 0, rgba(0, 0, 0, 0.045) 1.5px, rgba(0, 0, 0, 0) 3px),
    radial-gradient(120% 135% at 50% 50%, rgba(0, 0, 0, 0) 50%, rgba(0, 0, 0, 0.62) 100%);
  background-size: 100% 100%, 100% 3px, 100% 100%;
  background-repeat: no-repeat, repeat, no-repeat;
  /* Scanlines creep slowly downward (only the scanline layer's position animates; vignette stays put). */
  animation: crt-scan 2.6s linear infinite, crt-jitter 0.15s steps(2, end) infinite;
}
/* Drifting TV STATIC — fine grayscale feTurbulence snow, SCREEN-blended so it reads as white grain over the
   picture, jumping position every frame. */
.nuke-container .nuke-radio-video-screen::after {
  content: '';
  position: absolute;
  inset: 4px;   /* confined to the picture, off the frame; noise tiles (repeat) so drift never bares an edge */
  z-index: 4;
  pointer-events: none;
  opacity: 0.2;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='140' height='140'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='4' stitchTiles='stitch'/%3E%3CfeColorMatrix type='saturate' values='0'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E");
  background-size: 140px 140px;
  animation: crt-static 0.35s steps(9) infinite, crt-jitter 0.15s steps(2, end) infinite;
}
@keyframes crt-static {
  0%   { background-position: 0 0; }
  11%  { background-position: -34px 16px; }
  22%  { background-position: 40px -22px; }
  33%  { background-position: -18px 36px; }
  44%  { background-position: 28px 12px; }
  55%  { background-position: -38px -26px; }
  66%  { background-position: 16px 32px; }
  77%  { background-position: -24px -12px; }
  88%  { background-position: 32px 24px; }
  100% { background-position: 0 0; }
}
@keyframes crt-flicker {
  0%, 100% { filter: brightness(1); }
  50%      { filter: brightness(1.02); }
}
/* The jitter lives on the CONTENT layers (video + overlays), NOT the screen, so the outer border never moves
   — everything jitters within it. */
@keyframes crt-jitter {
  0%, 100% { transform: translateX(0); }
  50%      { transform: translateX(0.4px); }
}
@keyframes crt-scan {
  from { background-position: 0 0, 0 0, 0 0; }
  to   { background-position: 0 0, 0 3px, 0 0; }
}
/* Skip button — secondary, to the right of the play button. */
.nuke-container .nuke-radio-bar-skip {
  position: relative; /* anchors the cooldown tooltip */
  flex: 0 0 auto;
  width: 26px;
  height: 26px;
  margin-left: -5px; /* tighten the gap to the play button (bar gap is 12px) */
  border: none;
  border-radius: 50%;
  background: var(--nk-hover-1);
  color: var(--nk-text-2);
  display: flex;
  align-items: center;
  justify-content: center;
  cursor: pointer;
  transition: background 0.12s ease, color 0.12s ease;
}
.nuke-container .nuke-radio-bar-skip svg { width: 15px; height: 15px; }
.nuke-container .nuke-radio-bar-skip:hover { background: var(--nk-border-soft); color: var(--nk-text); }
.nuke-container .nuke-radio-bar-skip:active { transform: scale(0.94); }
.nuke-container .nuke-radio-bar-skip:disabled { opacity: 0.5; cursor: default; }
.nuke-container .nuke-radio-bar-skip.is-hidden { display: none; }
/* Cooldown tooltip — flashed above the button (time from data-cd) when a skip is denied. */
.nuke-container .nuke-radio-bar-skip::after {
  content: 'Skip in ' attr(data-cd);
  position: absolute;
  bottom: calc(100% + 5px);
  left: 50%;
  transform: translateX(-50%);
  padding: 3px 7px;
  border-radius: 6px;
  background: #1b1b1b;
  color: #fff;
  font-size: calc(10px - var(--font-shrink, 0px));
  font-weight: 600;
  white-space: nowrap;
  opacity: 0;
  pointer-events: none;
  transition: opacity 0.12s ease;
  z-index: 20;
}
.nuke-container .nuke-radio-bar-skip.show-cd::after { opacity: 1; }
/* Volume slider — gray track + pink thumb (matches the play button), fills the controls row. */
.nuke-container .nuke-radio-bar-vol {
  flex: 0 0 auto;
  width: 80px;
  height: 4px;
  margin: 0;
  -webkit-appearance: none;
  appearance: none;
  background: var(--nk-border);
  border-radius: 999px;
  cursor: pointer;
}
.nuke-container .nuke-radio-bar-vol::-webkit-slider-thumb {
  -webkit-appearance: none;
  appearance: none;
  width: 12px;
  height: 12px;
  border: none;
  border-radius: 50%;
  background: #e0566c;
  cursor: pointer;
}
.nuke-container .nuke-radio-bar-vol::-moz-range-thumb {
  width: 12px;
  height: 12px;
  border: none;
  border-radius: 50%;
  background: #e0566c;
  cursor: pointer;
}
.nuke-container .nuke-radio-bar-icon-pause { display: none; }
.nuke-container .nuke-radio-bar.is-playing .nuke-radio-bar-icon-play { display: none; }
.nuke-container .nuke-radio-bar.is-playing .nuke-radio-bar-icon-pause { display: block; }
/* Minimal "LCD screen": a flat dark panel with plain green text. Two rows — a countdown timer on
   top, then the readout "Song Name -" [pfp] "Uploader" below. The bar around it stays white. */
.nuke-container .nuke-radio-bar-info {
  position: relative;
  z-index: 1;
  overflow: hidden;   /* clip the audio bars to the LCD box */
  flex: 1 1 auto;
  min-width: 0;
  display: flex;
  flex-direction: row;                     /* screen text + controls INLINE inside the one player box */
  align-items: center;
  gap: 8px;
  height: 42px;                            /* fixed so the bar never grows/shrinks with the station text */
  padding: 0 10px;                         /* text inset; controls sit inside the box on the right */
  border-radius: 0;                        /* square edges — full-bleed header bar */
  /* Left: 1px white highlight OUTER + 1px profile INNER (inset shadow). Right: profile-coloured border.
     Top: 1px profile-coloured (same as the right edge). Bottom: the pinned block's divider to the feed —
     present when the stream is CLOSED (radio is the block's bottom), removed when the stream is open. */
  border: none;
  border-top: 2px solid var(--nk-border-themed, var(--nk-border));   /* 2px profile-coloured top */
  border-bottom: 2px solid var(--nk-border-soft);   /* divider to the feed when the radio is alone (stream closed) */
  border-left: 1px solid #FFFFFF;   /* white highlight — matches the post left border (softened to 0.22 in dark via shared override) */
  border-right: 2px solid var(--nk-border-themed, var(--nk-border));   /* 2px profile-coloured right edge (both themes) */
  box-shadow: inset 1px 0 0 0 var(--nk-border-themed, var(--nk-border));   /* 1px profile-coloured INNER, just inside the white left */
  background: #000;   /* pure black */
}
/* Stream open → the stream (below) is the block's bottom edge; drop the radio's own bottom divider so the
   radio + stream read as one continuous block (no line between them). */
.nuke-container .nuke-chat-header:has(.nuke-radio-video-panel.is-open) .nuke-radio-bar-info {
  border-bottom: none;
}

/* The title/uploader row. */
.nuke-container .nuke-radio-bar-line {
  position: relative;
  z-index: 1;
  /* Marquee VIEWPORT: fills the LCD width and clips the track; the ticker scrolls the track when the
     title + uploader overflow (refreshRadioMarquee in index.ts). */
  flex: 1 1 auto;                          /* take the slack so the controls sit at the box's right edge */
  display: flex;
  align-items: center;
  min-width: 0;
  overflow: hidden;
}
.nuke-container .nuke-radio-bar-track {
  /* The moving row — song title, uploader pfp, uploader name — sized to its CONTENT so it can exceed the
     viewport (then the ticker translates it). flex:0 0 auto so it never squeezes to fit. */
  flex: 0 0 auto;
  display: flex;
  align-items: center;
  gap: 6px;
  width: max-content;
  max-width: none;
  white-space: nowrap;
  will-change: transform;
}
/* The LCD line links to the current uploader's profile when their wallet resolves (is-clickable toggled in
   index.ts). Hovering anywhere on it (pfp / title / name) underlines the uploader name as the click cue. */
.nuke-container .nuke-radio-bar-line.is-clickable { cursor: pointer; }
.nuke-container .nuke-radio-bar-line.is-clickable:hover .nuke-radio-bar-uploader { text-decoration: underline; }
.nuke-container .nuke-radio-bar-pfp {
  flex: 0 0 auto;
  width: 18px;
  height: 18px;
  /* Square feed-pfp shape (shared --pfp-radius, scales proportionally), not a circle. */
  border-radius: var(--pfp-radius);
  object-fit: cover;
  background: var(--nk-surface-2);
  /* Grayscale → green LCD duotone so the avatar reads as part of the screen (sepia recolours the
     grayscale, hue-rotate + saturate push it to LCD green). */
  filter: grayscale(1) sepia(1) hue-rotate(72deg) saturate(3.2) brightness(0.95);
}
.nuke-container .nuke-radio-bar-pfp.is-hidden { display: none; }
.nuke-container .nuke-radio-bar-now,
.nuke-container .nuke-radio-bar-uploader {
  /* No truncation/ellipsis — when the row is too wide it ticker-scrolls instead (see .nuke-radio-bar-track). */
  flex: 0 0 auto;
  font-size: calc(13px - var(--font-shrink, 0px));
  font-weight: 400;
  color: #5fd98a;
  text-transform: uppercase;
  white-space: nowrap;
}
.nuke-container .nuke-radio-bar-now:empty { display: none; }
.nuke-container .nuke-radio-bar-uploader.is-hidden { display: none; }
/* The floating FAB + its video companion are replaced by the bar — hide them. */
.nuke-container .nuke-radio,
.nuke-container .nuke-radio-video { display: none !important; }

.nuke-container .nuke-radio-video {
  position: fixed;
  right: 20px;
  bottom: 76px; /* flush on the 56px FAB (bottom:20px + 56px) — no gap */
  z-index: 90;
  width: 248px; /* matches the expanded radio width */
  aspect-ratio: 16 / 9;
  border-radius: 16px 16px 0 0;
  background: #000;
  object-fit: cover;
  box-shadow: 0 6px 20px rgba(0, 0, 0, 0.25);
  display: none; /* instant show/hide — no open/close animation */
}
.nuke-container:has(.nuke-radio.is-playing) .nuke-radio-video { display: block; }

.nuke-container .nuke-radio {
  position: fixed;
  right: 20px;
  bottom: 20px;
  z-index: 90;
  width: 56px;
  height: 56px;
  padding: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 0;
  background: var(--nk-surface);
  border: 1px solid var(--nk-border-soft);
  border-radius: 16px;
  /* Soft + NOT animated — a big animated box-shadow is a per-frame paint hog. */
  box-shadow: 0 1px 4px rgba(0, 0, 0, 0.06);
  cursor: pointer;
  overflow: hidden;
  /* Only width transitions; `will-change` isolates the FAB on its own compositor
     layer so its per-frame repaint doesn't re-rasterise the chat underneath. */
  will-change: transform;
  transition: filter 0.18s ease; /* no width animation — the FAB snaps open/closed */
}
/* Darken in lockstep with the search/hovercard page-dim scrim (the FAB sits above
   the scrim, so brightness(0.4) ≈ the scrim's rgba(0,0,0,.6)). */
.nuke-container:has(.nuke-subheader-search .user-search-bar:hover) .nuke-radio,
.nuke-container:has(.nuke-subheader-search:focus-within) .nuke-radio {
  filter: brightness(0.4);
}
/* Radio sits above the scrim (z-90), so it dims via its own brightness filter — same ease-in
   ramp as the scrim so the whole page darkens together, fluidly, with the lift/pop. */
.nuke-container:has(.nuke-hovercard.is-visible) .nuke-radio {
  filter: brightness(0.4);
  transition: filter 0.2s cubic-bezier(0.45, 0, 0.9, 1);
}
/* Collapsed → hide the station info + equaliser; the FAB is one big play target. */
.nuke-container .nuke-radio .nuke-radio-station,
.nuke-container .nuke-radio .nuke-radio-vol,
.nuke-container .nuke-radio .nuke-radio-eq { display: none; }
/* Volume slider (shown only when expanded) — gray track + red thumb to match the
   radio. Controls the stream volume (the video's audio) via radioWs.setVolume. */
.nuke-container .nuke-radio.is-playing .nuke-radio-vol {
  display: block;
  flex: 0 0 auto;
  width: 60px;
  height: 4px;
  margin: 0;
  -webkit-appearance: none;
  appearance: none;
  background: var(--nk-border);
  border-radius: 999px;
  cursor: pointer;
}
.nuke-container .nuke-radio-vol::-webkit-slider-thumb {
  -webkit-appearance: none;
  appearance: none;
  width: 12px;
  height: 12px;
  border: none;
  border-radius: 50%;
  background: #e0566c;
  cursor: pointer;
}
.nuke-container .nuke-radio-vol::-moz-range-thumb {
  width: 12px;
  height: 12px;
  border: none;
  border-radius: 50%;
  background: #e0566c;
  cursor: pointer;
}
.nuke-container .nuke-radio-btn {
  display: flex;
  align-items: center;
  justify-content: center;
  color: #8a94a6;
}
.nuke-container .nuke-radio-btn svg { width: 18px; height: 18px; }
/* Play/pause button — a 30px red circle in BOTH states (matches the expanded
   pause button), centred in the collapsed white square. */
.nuke-container .nuke-radio-btn.play {
  flex: 0 0 auto;
  cursor: pointer;
  width: 30px;
  height: 30px;
  border-radius: 50%;
  background: #e0566c;
  border: 1px solid rgba(0, 0, 0, 0.12);
  color: #fff;
}
.nuke-container .nuke-radio-btn.play svg { width: 15px; height: 15px; }
/* EXPANDED (playing): the full radio row — white box, station info, equaliser.
   Width animates 56px → 230px (content reveals as it widens; height stays 56). */
.nuke-container .nuke-radio.is-playing {
  width: 248px;
  justify-content: flex-start;
  padding: 0 14px;
  gap: 11px;
  /* Square the TOP corners so the video above meets it flush → one container.
     (bg/border/shadow otherwise inherited so the expand only animates `width`.) */
  border-radius: 0 0 16px 16px;
}
.nuke-container .nuke-radio.is-playing .nuke-radio-station { display: block; }
.nuke-container .nuke-radio.is-playing .nuke-radio-eq { display: flex; }
.nuke-container .nuke-radio.is-playing .nuke-radio-btn.play {
  position: static;
  width: 30px;
  height: 30px;
  border-radius: 50%;
  background: #e0566c;
}
.nuke-container .nuke-radio.is-playing .nuke-radio-btn.play svg { width: 15px; height: 15px; }
.nuke-container .nuke-radio-station {
  flex: 1;
  min-width: 0;
  display: flex;
  flex-direction: column;
  justify-content: center;
}
/* Now-playing song title — single line, ellipsised to the FAB width.
   Hidden when empty (nothing playing). */
.nuke-container .nuke-radio-song {
  margin-top: 1px;
  max-width: 100%;
  overflow: hidden;
  white-space: nowrap;
  text-overflow: ellipsis;
  font-size: calc(11px - var(--font-shrink, 0px));
  font-weight: 600;
  line-height: 1.3;
  color: var(--nk-text-2);
}
.nuke-container .nuke-radio-song:empty { display: none; }
.nuke-container .nuke-radio-eq { display: flex; align-items: flex-end; gap: 3px; height: 18px; }
.nuke-container .nuke-radio-eq span {
  width: 3px;
  border-radius: 1px;
  background: #e0566c;
  transform-origin: bottom;
  transform: scaleY(0.3);   /* resting (paused) — bars only move while playing */
}
.nuke-container .nuke-radio.is-playing .nuke-radio-eq span {
  animation: nuke-radio-eq 0.9s ease-in-out infinite;
}
/* Play / pause icon swap driven by the .is-playing toggle. */
.nuke-container .nuke-radio-icon-pause { display: none; }
.nuke-container .nuke-radio.is-playing .nuke-radio-icon-play { display: none; }
.nuke-container .nuke-radio.is-playing .nuke-radio-icon-pause { display: block; }
.nuke-container .nuke-radio-eq span:nth-child(1) { height: 50%; animation-delay: -0.2s; }
.nuke-container .nuke-radio-eq span:nth-child(2) { height: 90%; animation-delay: -0.5s; }
.nuke-container .nuke-radio-eq span:nth-child(3) { height: 65%; animation-delay: 0s; }
.nuke-container .nuke-radio-eq span:nth-child(4) { height: 40%; animation-delay: -0.35s; }
@keyframes nuke-radio-eq { 0%, 100% { transform: scaleY(0.4); } 50% { transform: scaleY(1); } }
@media (prefers-reduced-motion: reduce) {
  .nuke-container .nuke-radio.is-playing .nuke-radio-eq span { animation: none; }
}

/* ╔══════════════════════════════════════════════════════════════════════════╗
   ║  HIDDEN FEATURES — built + wired, intentionally hidden for now.            ║
   ╠══════════════════════════════════════════════════════════════════════════╣
   ║  Convention (so hidden things stay organized + trivially reversible):      ║
   ║    • Add the `nuke-hidden` class to the element you want to hide.          ║
   ║    • Tag it with a `HIDDEN:` comment (HTML `<!-- HIDDEN: … -->` or TS      ║
   ║      `// HIDDEN: …`) so `grep -rn "HIDDEN:" src` lists EVERYTHING hidden.  ║
   ║    • Leave all the underlying code intact — a hidden trigger just sits     ║
   ║      dormant (a display:none button can't be clicked), so nothing else     ║
   ║      needs touching. To RESTORE: delete the class + its HIDDEN: comment.   ║
   ║                                                                            ║
   ║  Currently hidden (keep this list in sync):                                ║
   ║  • Polls — left-nav button (index.html, data-nav="polls"). Live code       ║
   ║    still present: handler index.ts → openPollsModal(), modal polls.ts.     ║
   ║  • Season Pass — left-nav button (index.html, data-nav="season-pass").     ║
   ║    Live code: index.ts → renderNukeSeasonPass (URL-backed at /seasonpass). ║
   ║  • Shop — right-column box (index.html, .nuke-shop-box). Live code still    ║
   ║    present: shop-panel.ts fills #nuke-shop-grid.                           ║
   ║  • Mission notebook — right-column pad (index.html, .nuke-notebook).        ║
   ║    Live code: notebook.ts renderNukeNotebook + index.ts 30s timer.         ║
   ║  • Light / Dark appearance options — cream Light + blue Dark buttons in     ║
   ║    the settings picker (settings.ts opt('light'/'dark', hidden)). True      ║
   ║    White / True Black are the defaults now (System resolves to them); the   ║
   ║    cream/blue prefs + their CSS token blocks stay in code. Rule below.      ║
   ║  • Post "⋯" more-options button — .nuke-post-more on every post header       ║
   ║    (chat-render.ts renderPost + system posts). Inert (no menu wired yet);   ║
   ║    markup kept, hidden via the CSS rule below. Rule below.                  ║
   ║  • Decorative "lined paper" texture — the faint SVG ruled lines             ║
   ║    (--nk-paper-svg) tiled behind the feed + post modal. Var defs + the      ║
   ║    background rule kept; the CSS rule below suppresses the image.           ║
   ║  • Full page top bar — the recent-actions pill ticker (#nuke-pill-header),  ║
   ║    its scroll caps, AND the brand logo — i.e. the whole 48px .nuke-page-    ║
   ║    topbar. The strip markup + index.ts pill code stay; the columns below    ║
   ║    were re-anchored to top:0 to reclaim the 48px band (Home lives in the    ║
   ║    left nav). Rule below.                                                   ║
   ╚══════════════════════════════════════════════════════════════════════════╝ */
.nuke-container .nuke-hidden { display: none !important; }
/* HIDDEN: the "⋯" more-options button to the right of the post header balance (.nuke-post-more, both
   the feed post + system-post headers). Inert for now — no menu is wired. Kept in the markup so
   wiring it later just needs this rule deleted. Restore: delete this rule + the registry line above. */
.nuke-container .nuke-post-more { display: none !important; }
/* HIDDEN: the decorative "lined paper" texture — the faint SVG ruled lines (--nk-paper-svg) tiled
   behind the feed (#nuke-chat-feed) + subview overlay (#nuke-post-modal-body). The themed var defs +
   the original background rule stay in code; this only suppresses the image. Restore: delete this rule
   + the registry line above. */
.nuke-container #nuke-chat-feed,
.nuke-container #nuke-post-modal-body { background-image: none !important; }
/* HIDDEN: the whole page top bar (.nuke-page-topbar) — the recent-actions pill ticker
   (#nuke-pill-header) + scroll caps + brand logo. The markup + the index.ts pill code stay live (just
   not shown); the columns below were re-anchored from top:48px → top:0 to reclaim the 48px band (Home
   still lives in the left nav). Restore: delete this rule + the registry line above, and put the
   top:48px offsets back on .nuke-nav / .nuke-stage / .nuke-subheader / .nuke-side-right. */
.nuke-container .nuke-page-topbar { display: none !important; }
/* HIDDEN: Light (cream) + Dark (blue) appearance options in the settings picker. The settings modal
   mounts on <body> (outside .nuke-container), so the scoped .nuke-hidden above can't reach it — this
   targeted rule hides the two option buttons. True White / True Black are the defaults now (System
   resolves to them). Their prefs + token blocks stay in code (a legacy stored 'light'/'dark' still
   renders). Restore: drop `nuke-hidden` from the two opt() calls in settings.ts. */
.nuke-theme-opt.nuke-hidden { display: none !important; }

/* --- x.com-style left nav rail -------------------------------------------- */
/* Replaces the old `.nuke-side-left` profile column. Fixed, full-height, and
   anchored so its RIGHT edge meets the same seam the profile column did
   (`50% - chat-w/2 - gap`), then grows leftward by `--nav-col-w`. No top chrome
   sits over the left side, so it runs from the very top. */
.nuke-container .nuke-nav {
  position: fixed;
  top: var(--page-header-h); /* was 0 (reclaimed topbar); now below the page header */
  bottom: 0;
  /* Left panel BACKGROUND runs to the viewport LEFT edge; content padded back to
     the nav column (padding-left = nav-left + the 24px inset). */
  left: 0;
  width: calc(50% - var(--trio-shift) - var(--chat-w) / 2 - var(--col-gap));
  box-sizing: border-box;
  padding-left: calc(50% - var(--trio-shift) - var(--chat-w) / 2 - var(--col-gap) - var(--nav-col-w) + 24px);
  padding-right: 24px;
  padding-top: 16px; /* nudge the nav content down off the header (bg fills the padding) */
  display: flex;
  flex-direction: column;
  gap: 12px;
  z-index: 25;
  /* Solid background = the logged-in / viewed user's BORDER colour (SETTING 3, --nk-border-themed); falls
     back to --nk-border, the SAME neutral the posts use, when there is no colour. */
  background: var(--nk-border-themed, var(--nk-border));
  /* Isolate layout/style like the other fixed columns (no paint containment so
     child shadows aren't clipped). */
  contain: layout style;
}
/* Empty fixed-height box at the top of the nav (above the logo), top-aligned with
   the chat panel. Border/radius/background come from `.nuke-plain-box` (same as
   the shop container); this just fixes its height and stops it from shrinking. */
.nuke-container .nuke-nav-top-box {
  flex: 0 0 auto;
  /* Adaptive height — grows to fit the profile card (was a fixed 200px). */
  height: auto;
  position: relative;
  /* Pinned to the BOTTOM of the left nav column (x.com-style account block).
     `order: 1` drops it below the nav list (default order 0); `margin-top: auto`
     pushes it to the very bottom of the flex column. */
  order: 1;
  margin-top: auto;
  /* Small gap below so the pill sits near the very bottom (x.com account block). */
  margin-bottom: 6px;
}
/* Profile card filling the box — reuses the hovercard's inner styling, but
   neutralizes the popup-only chrome: always visible + static (not a fixed,
   animated, shadowed popup), and no border/shadow/bg of its own since the
   surrounding `.nuke-plain-box` already provides the white card chrome. */
.nuke-container .nuke-nav-top-box .nuke-hovercard.nuke-profile-card {
  position: static;
  width: 100%;
  opacity: 1;
  pointer-events: auto;
  transform: none;
  transition: none;
  border: none;
  box-shadow: none;
  background: none;
  cursor: default;
}

/* ── Compact X.com-style account widget (left nav, bottom) ────────────────────
   Slim the left profile card down to a single clickable pill: avatar + name/
   @handle + a ··· glyph (X's bottom-left account block). The banner, cred/balance
   row, stat breakdown, and stake/unstake actions are hidden here (kept in the DOM
   + still populated — the chat-header card carries balance/stake). EVERY rule is
   scoped to `.nuke-nav-top-box` so the shared message hover popup (.nuke-hovercard)
   keeps its full banner + stats design. */
/* Specificity note: the `.nuke-profile-card` is included so these beat the base
   `.nuke-profile-card .nuke-profile-breakdown` (grid) / `.nuke-staking.nuke-profile-
   actions` rules, which otherwise win on source order and re-show the stats. The
   `.nuke-staking` on the last selector matches the actions rule's specificity + 1. */
.nuke-container .nuke-nav-top-box .nuke-profile-card .nuke-profile-card-banner,
.nuke-container .nuke-nav-top-box .nuke-profile-card .nuke-hovercard-idrow,
.nuke-container .nuke-nav-top-box .nuke-profile-card .nuke-profile-breakdown,
.nuke-container .nuke-nav-top-box .nuke-profile-card .nuke-staking.nuke-profile-actions {
  display: none;
}
/* Drop the card chrome — the pill itself is the whole widget. */
.nuke-container .nuke-nav-top-box.nuke-plain-box {
  background: none;
  border: none;
  border-radius: 0;
  overflow: visible;
}
/* Head row → the clickable account pill. */
.nuke-container .nuke-nav-top-box .nuke-profile-card-head {
  align-items: center; /* was flex-end, for the old banner overlap */
  margin-top: 0;       /* was -20px to pull the pfp up over the banner */
  gap: 10px;
  padding: 10px 12px;
  border-radius: 9999px;
  transition: background-color 0.15s ease;
}
.nuke-container .nuke-nav-top-box .nuke-profile-card-head:hover {
  /* Theme-aware version of x.com's nav-hover wash (10% ink). */
  background-color: color-mix(in srgb, var(--nk-hover-tint) 10%, transparent);
}
/* The wash is the affordance — drop the name underline-on-hover here. */
.nuke-container .nuke-nav-top-box .nuke-profile-card-head:hover .nuke-hovercard-name {
  text-decoration: none;
}
/* Avatar → 44px (matches the square nav icon-buttons), no white banner ring. All screen sizes. */
.nuke-container .nuke-nav-top-box .nuke-hovercard.nuke-profile-card .nuke-hovercard-pfp {
  width: 44px;
  height: 44px;
  box-shadow: none;
}
/* Name/@handle column fills the middle and truncates; ··· sits at the far right. */
.nuke-container .nuke-nav-top-box .nuke-hovercard-id { flex: 1; }
.nuke-container .nuke-nav-top-box .nuke-hovercard-namerow { font-size: calc(15px - var(--font-shrink, 0px)); }
.nuke-container .nuke-nav-top-box .nuke-hovercard-name { font-size: calc(15px - var(--font-shrink, 0px)); }
/* X's @handle is regular gray — drop the app-wide italic (rule near 5072) here. */
.nuke-container .nuke-nav-top-box .nuke-hovercard-handle { font-size: calc(15px - var(--font-shrink, 0px)); font-style: normal; }
/* The ··· glyph (decorative for now — clicking anywhere on the pill opens your
   profile; a real account menu can be wired to this later). */
.nuke-container .nuke-nav-top-box .nuke-profile-card-more {
  flex: 0 0 auto;
  margin-left: auto;
  display: flex;
  align-items: center;
  justify-content: center;
  width: 22px;
  height: 22px;
  color: var(--nk-text-2);
  font-size: calc(18px - var(--font-shrink, 0px));
  line-height: 1;
  cursor: pointer;
  border-radius: 50%;
  transition: background 0.12s ease, color 0.12s ease;
}
.nuke-container .nuke-nav-top-box .nuke-profile-card-more:hover {
  background: var(--nk-hover-1);
  color: var(--nk-text);
}

/* Account menu popover (··· on the profile card → Disconnect wallet). Mounts on body,
   so it inherits the --nk-* tokens from body.page-nuke and escapes the .nuke-container
   border reset — a real 2px card border. */
.nuke-account-menu {
  position: fixed;
  z-index: 200;
  min-width: 220px;
  padding: 6px;
  background: var(--nk-surface);
  border: 2px solid var(--nk-border);
  border-radius: 14px;
  box-shadow: 0 14px 34px rgba(0, 0, 0, 0.20), 0 2px 8px rgba(0, 0, 0, 0.08);
  font-family: var(--font-conduit, 'Sunghyun Sans', -apple-system, sans-serif);
}
.nuke-account-menu-item {
  display: flex;
  align-items: center;
  gap: 10px;
  width: 100%;
  padding: 12px 14px;
  border: none;
  background: transparent;
  color: var(--nk-text);
  font-family: inherit;
  font-size: calc(14px - var(--font-shrink, 0px));
  font-weight: 700;
  text-align: left;
  border-radius: 10px;
  cursor: pointer;
}
.nuke-account-menu-item:hover { background: var(--nk-hover-1); }
.nuke-account-menu-item i { font-size: calc(18px - var(--font-shrink, 0px)); }
/* Banner strip at the top of the left profile card — mirrors the /~handle
   profile page. Breaks out of the card's 12px padding (negative margins) to
   span the full card width and sit flush at the top; the plain-box's rounded
   `overflow:hidden` clips its top corners. Solid black shows when no banner image. */
.nuke-container .nuke-profile-card-banner,
.nuke-container .nuke-hovercard-banner {
  margin: -12px -12px 0;
  /* Twitter/X banner aspect ratio (1500×500 = 3:1); height follows the card width. */
  aspect-ratio: 3 / 1;
  position: relative;
  flex-shrink: 0;
  background: #000000;
  overflow: hidden;
}
/* Popup only: inset the banner 2px on the TOP + LEFT + RIGHT so the border's 2px inner regular edge (an
   inset shadow just inside every side) shows AROUND the banner instead of being cropped by it. */
.nuke-container .nuke-hovercard-banner {
  margin: -10px -10px 0;
}
.nuke-container .nuke-profile-card-banner-img,
.nuke-container .nuke-hovercard-banner-img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
}
/* Canvas overlay used by banner-loader.ts to force-loop finite-loop animated banners
   (a dedicated class, NOT the banner-img class, so the :not([src]) hide rule skips it). */
.nuke-container .nuke-banner-loop-canvas {
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
}
/* Head row: pfp on the left, username + @handle to its right. The row is pulled
   up so the pfp's top 20px overlaps the banner and 44px (of the 64px pfp) hangs
   below. The name + handle (which total exactly 40px) bottom-align to the pfp's
   bottom, so the handle bottom meets the pfp bottom and the username top lands
   4px below the banner (44 − 40).
   SHARED by the left profile card AND the message hover popup — they are the same
   banner+pfp+head design, so they read from ONE rule and can never diverge (which
   is what caused the popup's "missing ring" before). */
.nuke-container .nuke-profile-card-head,
.nuke-container .nuke-hovercard-head {
  display: flex;
  align-items: flex-end;
  gap: 12px;
  margin-top: -20px;
  /* Clickable → opens your own profile page (wired in initNukeProfileNav). */
  cursor: pointer;
}
/* Subtle affordance: the name underlines on hover, like a profile link. */
.nuke-container .nuke-profile-card-head:hover .nuke-hovercard-name,
.nuke-container .nuke-hovercard-head:hover .nuke-hovercard-name {
  text-decoration: underline;
}
/* Pfp size (64px) + white ring, keyed off `.nuke-hovercard` itself so it matches
   BOTH the popup (`.nuke-hovercard`) AND the left card (`.nuke-hovercard
   .nuke-profile-card`) as ONE rule, independent of the inner head markup.
   The ring lives on the WRAPPER, not the inner `.cc-pfp` image: the `<img>` has a
   browser-default `overflow: clip` that swallows a box-shadow placed on it (the
   shadow exists but never paints). The wrapper is `overflow: visible`, so the ring
   shows — and because the `.cc-pfp { box-shadow: none !important }` kill rule only
   targets the image, a ring on the wrapper needs NO `!important`. The wrapper's
   `border-radius` makes the ring follow the rounded corners. */
.nuke-container .nuke-hovercard .nuke-hovercard-pfp {
  width: 64px;
  height: 64px;
  /* Hard offset drop-shadow, like the message pfp hover shadow. */
  box-shadow: 2px 2px 0 0 rgba(0, 0, 0, 0.2);
  /* Thicker avatar border to suit the larger 64px popup avatar — the .cc-pfp image border reads
     from this var (default 1px on .nuke-post-pfp). */
  --pfp-outer-border-width: 2px;
}
/* Nudge the @handle 1px left so its italic glyphs' left edge lines up under the
   (upright) username above it. */
.nuke-container .nuke-profile-card .nuke-hovercard-handle,
.nuke-container .nuke-hovercard-head .nuke-hovercard-handle {
  margin-left: -1px;
}
/* Detailed balance breakdown under the headline balance (left card only),
   laid out horizontally like X.com's "Following / Followers" row: each stat is a
   bold value immediately followed by a muted label, with the stats spaced across a
   single row (wrapping only if the card is too narrow). Hidden entirely (inline
   style, via renderNukeProfileBox) when the user holds nothing — except in nuclear
   fallout, where it always shows. A hairline divider separates it from the
   cred/balance row. */
.nuke-container .nuke-profile-card .nuke-profile-breakdown {
  margin-top: 10px;
  padding-top: 10px;
  border-top: 1px solid var(--nk-border-soft);
  /* 2×2 grid of "value label" stats (still X.com-style inline units) — balanced,
     rather than a ragged single-row wrap, now that there are four stats
     (wallet / pending / staked / DPR). Left column: Wallet, Staked; right: Pending,
     DPR. row-gap / column-gap below. */
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 6px 12px;
  font-size: calc(12px - var(--font-shrink, 0px));
  line-height: 1.3;
}
/* One stat: bold value + muted label sitting inline, X.com "123 Following"-style. */
.nuke-container .nuke-profile-card .nuke-profile-breakdown-stat {
  display: inline-flex;
  align-items: baseline;
  gap: 4px;
  white-space: nowrap;
}
.nuke-container .nuke-profile-card .nuke-profile-breakdown-value {
  color: var(--nk-text);
  font-weight: 700;
  font-variant-numeric: tabular-nums;
}
.nuke-container .nuke-profile-card .nuke-profile-breakdown-label {
  color: var(--nk-text-2);
}
/* Stake / Unstake row at the bottom of the card. Overrides the shared
   `.nuke-staking` panel flex (column + grow) into a simple 2-up button row. */
.nuke-container .nuke-profile-card .nuke-staking.nuke-profile-actions {
  flex: 0 0 auto;
  flex-direction: row;
  gap: 8px;
  margin-top: 12px;
}
.nuke-container .nuke-profile-action-btn {
  flex: 1;
  height: 34px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  border: none;
  border-radius: 9999px;
  background: #0F1419;
  color: #FFFFFF;
  font-family: inherit;
  font-weight: 700;
  font-size: calc(14px - var(--font-shrink, 0px));
  line-height: 1;
  cursor: pointer;
}
.nuke-container .nuke-profile-unstake-btn {
  background: #F4212E;
}
.nuke-container .nuke-profile-action-btn:disabled {
  opacity: 0.5;
  cursor: default;
}
/* Brand logo — a FIRST-CLASS, icon-only nav item at the top of the menu (moved out of the hidden top
   bar). It IS a `.nuke-nav-item`, so it inherits the item's pill / hover wash / padding / left-alignment
   and lines up with the labeled items automatically: the hover left edge is flush with them and the icon
   lands at the same 12px inset. Only two overrides: SYMMETRIC padding (so the icon-only hover reads as a
   CIRCLE, not a stubby pill), and a larger 36px mark. The list's own 4px gap spaces it from Home. */
/* NB: uses `.nuke-nav-item.nuke-nav-logo-item` (both classes) so these beat the base `.nuke-nav-item`
   rules on specificity — otherwise the base padding (12px 24px 12px 12px), defined later in the file,
   wins and the box goes 72×60 (wider than tall, icon shoved left). */
.nuke-container .nuke-nav-item.nuke-nav-logo-item {
  padding: 12px;
  margin-bottom: 2px;
}
/* Same 26px icon as the menu items, boxed as a fixed SQUARE with the glyph centred — so the item (icon +
   12px padding all round) is a 50px square → a true circle the same height as the menu pills, glyph
   centred. (The square avoids an oval that would otherwise follow the glyph's advance width.) */
.nuke-container .nuke-nav-item.nuke-nav-logo-item .nuke-nav-icon {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 26px;
  height: 26px;
  font-size: calc(26px - var(--font-shrink, 0px));
}
.nuke-container .nuke-nav-item.nuke-nav-logo-item:active { transform: scale(0.94); }
.nuke-container .nuke-nav-list {
  display: flex;
  flex-direction: column;
  /* The list fills the nav rail (its default in the .nuke-nav flex column) and align-items: stretch
     sizes every item to that full width — so the nav-card buttons come out equal-width AND as wide as
     the "Post" CTA below them (.nuke-nav-post-cta is width:100% of the same rail). */
  align-items: stretch;
  gap: 16px;   /* spacing between the text nav items */
  padding: 0;  /* no top padding — the .nuke-nav 16px padding-top alone matches the right panel's gap */
}
/* Relocated Sign In CTA — the x.com "Post" button slot. Sits directly below the nav
   items (order 0, above the order:1 profile card pinned at the bottom); shown only
   while signed out via the container's data-signedout flag (set in main.ts). */
.nuke-container .nuke-nav-signin {
  display: none;
  align-items: center;
  justify-content: center;
  order: 0;
  width: 100%;
  box-sizing: border-box;
  height: 48px;
  /* Sits directly below the nav items (the "sort list") — NOT pinned to the panel
     bottom. The nav's 12px row gap plus this 4px gives the spacing above it. */
  margin-top: 4px;
  border: none;
  border-radius: 9999px;
  background: var(--nk-accent);
  color: #FFFFFF;
  font-family: inherit;
  font-weight: 800;
  font-size: calc(16px - var(--font-shrink, 0px));
  line-height: 1;
  cursor: pointer;
  transition: background-color 0.15s ease;
}
.nuke-container .nuke-nav-signin:hover { background: #1A8CD8; }
.nuke-container[data-signedout="true"] .nuke-nav-signin { display: flex; }
/* Post CTA — the same full-width pill CTA as the Sign In slot, but the buy-button GREEN. Sits in the nav
   list directly below the "More" item. */
.nuke-container .nuke-nav-post-cta {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 100%;
  box-sizing: border-box;
  height: 42px;   /* match the nav-card button height so the drop shadow runs the same length */
  margin-top: 4px;
  border-radius: 0;   /* fully squared, like the nav-card buttons */
  /* Fill = the page-header profile colour (oklch L0.42 C0.12) rotated −100° in hue (blue→green).
     Isolate pattern: when --current-user-color is unset the raw value is invalid → the #0E8A53
     fallback (the original green) applies. */
  --post-cta-fill: oklch(from var(--current-user-color) 0.42 0.12 calc(h - 100));
  --btn-bg: var(--post-cta-fill, #0E8A53);
  background-color: var(--btn-bg);
  /* Sign-in button DESIGN (colour kept): top-highlight + bottom-shade gradients over the fill. */
  background-image:
    linear-gradient(to bottom, rgba(255, 255, 255, 0.10) 0%, transparent 50%),
    linear-gradient(to top, rgba(0, 0, 0, 0.10) 0%, transparent 12px);
  /* 2px 3D bevel derived from the fill: lighter top/left, darker bottom/right of the SAME hue. */
  border: 4px solid;
  border-color:
    oklch(from var(--btn-bg) calc(l * 1.28) c h)
    oklch(from var(--btn-bg) calc(l * 0.72) c h)
    oklch(from var(--btn-bg) calc(l * 0.72) c h)
    oklch(from var(--btn-bg) calc(l * 1.28) c h);
  box-shadow: 4px 4px 0 0 rgba(0, 0, 0, 0.2);
  color: #FFFFFF;
  font-family: inherit;
  font-weight: 800;
  font-size: calc(16px - var(--font-shrink, 0px));
  line-height: 1;
  cursor: pointer;
  /* Same motion as the nav-card buttons (transform + box-shadow), plus filter/bg for the brightness
     feedback + the cooldown colour swap. */
  transition: transform 0.12s ease, box-shadow 0.12s ease, filter 0.15s ease, background-color 0.15s ease;
}
/* Match the nav-card buttons' hover/press scale, keeping the brightness lift on the coloured fill. */
.nuke-container .nuke-nav-post-cta:hover { transform: scale(1.03); filter: brightness(1.06); }
.nuke-container .nuke-nav-post-cta:active { transform: scale(0.97); filter: brightness(0.92); box-shadow: 1px 1px 0 0 rgba(0, 0, 0, 0.2); }
/* Posting cooldown: renderPostCooldown swaps the label to the remaining time — mute the CTA to the same
   plain gray as the Edit-profile / profile Post cooldown. The gray READS from the border (both fills are
   near-white), so the CTA — which has `border:none` — needs the --nk-border-plain border added here too. */
.nuke-container[data-post-cooldown] .nuke-nav-post-cta {
  background: var(--nk-hover-1);
  border: 1px solid var(--nk-border-plain);
  color: var(--nk-text-2);
  font-weight: 700;
  cursor: default;
  /* Keeps the base's full nav-card box-shadow (inset edges + 4px drop) so the timed-out button's shadow
     matches the nav buttons in every state — no 2px override. */
}
/* In cooldown → NO hover/press feedback (kill the scale + brightness), same as the self-profile Post button. */
.nuke-container[data-post-cooldown] .nuke-nav-post-cta:hover,
.nuke-container[data-post-cooldown] .nuke-nav-post-cta:focus,
.nuke-container[data-post-cooldown] .nuke-nav-post-cta:active {
  transform: none;
  filter: none;
}
/* Posting-cooldown on the self-profile "Post" button — plain gray like the Edit-profile button (kept in
   sync with the nav CTA; the pencil→clock swap is handled with the icon rules). */
.nuke-container[data-post-cooldown] .nuke-profile-post-btn[data-nav="post"] {
  background: var(--nk-hover-1);
  color: var(--nk-text-2);
  border-color: var(--nk-border-plain);
  cursor: default;
}
/* In cooldown → NO hover/press feedback (the button isn't `disabled`, so kill the base scale/brightness). */
.nuke-container[data-post-cooldown] .nuke-profile-post-btn[data-nav="post"]:hover,
.nuke-container[data-post-cooldown] .nuke-profile-post-btn[data-nav="post"]:focus,
.nuke-container[data-post-cooldown] .nuke-profile-post-btn[data-nav="post"]:active {
  transform: none;
  filter: none;
}
/* Signed-out users have no profile — hide the identity header card in the chat panel
   and the profile container in the left nav entirely (the feed + Sign In CTA remain). */
.nuke-container[data-signedout="true"] .nuke-chat-header-2 { display: none; }
.nuke-container[data-signedout="true"] .nuke-nav-top-box { display: none; }
/* Signed-out page is intentionally a stripped-down teaser to push sign-ups: cut the
   nav menu (the Sign In CTA in the same rail stays), the shop, and the mission
   notebook. The feed + search + Sign In remain as the limited interface. */
.nuke-container[data-signedout="true"] .nuke-nav-list,
.nuke-container[data-signedout="true"] .nuke-shop-box,
.nuke-container[data-signedout="true"] .nuke-notebook {
  display: none;
}
/* Each item is a pill that hugs its content (icon + label); the rounded hover
   wash wraps only the item, x.com-style — not the full column width. Labels are
   regular weight at 20px with a 26px icon and a 20px gap between them — x.com's
   actual sidebar metrics; the active route goes bold with a filled icon. */
.nuke-container .nuke-nav-item {
  position: relative; /* anchors the unread dot (.has-unread::after) over the icon */
  display: flex;
  align-items: center;
  gap: 20px;
  width: fit-content;
  max-width: 100%;
  padding: 12px 24px 12px 12px;
  border: none;
  background: none;
  border-radius: 9999px;
  color: var(--nk-text);
  font-family: inherit;
  font-size: calc(20px - var(--font-shrink, 0px));
  font-weight: 400;
  line-height: 1;
  text-align: left;
  cursor: pointer;
  transition: background-color 0.15s ease;
}
/* Unread indicator: a small accent dot at the icon's top-right, shown when the
   tab has activity the user hasn't seen (Home = new messages while away from the
   feed; Notifications = unviewed notifs). Toggled by `.has-unread` (set in
   index.ts → updateNavUnreadDots). The surface-colored ring lifts it off the icon. */
.nuke-container .nuke-nav-item.has-unread::after {
  content: '';
  position: absolute;
  top: 9px;
  left: 29px;
  width: 8px;
  height: 8px;
  border-radius: 50%;
  /* Same colour as the notif-container unread dots + the header back button (bright inverted-hue take on
     your profile colour). Colourless → accent fallback. */
  --dot-bg-themed: oklch(from var(--current-user-color) 0.82 0.16 calc(h + 180));
  background: var(--dot-bg-themed, var(--nk-accent));
  pointer-events: none;
}
.nuke-container .nuke-nav-item:hover {
  /* x.com's exact nav-hover wash (10% ink on white). */
  background-color: rgba(15, 20, 25, 0.1);
}
.nuke-container .nuke-nav-icon {
  flex-shrink: 0;
  font-size: calc(26px - var(--font-shrink, 0px));
  line-height: 1;
  /* SETTING 6 (heading) → nav icon in YOUR hue (nav is not in a post → container default). */
  color: var(--icon-themed, var(--nk-text));
}
/* Nav-item SVG icons: shown + sized (the brand logo item keeps its own font icon). stroke=currentColor
   picks up the base .nuke-nav-icon colour (SETTING 6, your hue). */
.nuke-container .nuke-nav-list .nuke-nav-item:not(.nuke-nav-logo-item) .nuke-nav-icon {
  display: block;
  width: 22px;
  height: 22px;
}
.nuke-container .nuke-nav-label {
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
/* The Search nav button exists ONLY in the collapsed state (≤1018px), where the right-panel search bar is
   removed — hidden by default, revealed in that breakpoint. */
.nuke-container .nuke-nav-search-item { display: none; }
/* Active route — bold label. Excludes the brand wordmark (it carries data-nav="home" so it also gets
   .is-active on the feed, but must never show the active weight). */
.nuke-container .nuke-nav-item.is-active:not(.nuke-nav-logo-item) {
  font-weight: 700;
}
/* Active icon goes bold too, mirroring the label's bold weight (Lucide stroke 2 → 2.75). Higher-specificity
   than the base icon rule so it wins; the SVGs' inline stroke-width="2" is beaten by any CSS stroke-width. */
.nuke-container .nuke-nav-list .nuke-nav-item.is-active:not(.nuke-nav-logo-item) .nuke-nav-icon {
  stroke-width: 2.75;
}
/* Nav items styled like the right-panel Contribute container: a squared, beveled themed card — SETTING-1
   fill in the user's OWN hue (NOT inverted) + brilliant texture + edge-shading, white top/left + darker right/bottom bevel,
   inset themed edges, and a hard 4px drop shadow. Keeps the subtle scale on hover / press for feedback. */
.nuke-container .nuke-nav-list .nuke-nav-item:not(.nuke-nav-logo-item) {
  box-sizing: border-box;
  height: auto;
  width: auto; /* let the list's align-items: stretch equalise every button's width */
  padding: 8px 14px;
  gap: 9px;
  font-size: calc(20px - var(--font-shrink, 0px));
  color: var(--nk-text);
  border-radius: 0;
  --nav-card-bg: oklch(from var(--current-user-color) var(--set-1-l) var(--set-1-c) h);
  background-color: var(--nav-card-bg, var(--nk-surface));
  background-image:
    url('/img/ui/brilliant.png'),
    linear-gradient(to bottom, rgba(var(--nk-surface-rgb), 0.7) 0%, transparent 5px),
    linear-gradient(to bottom, rgba(0, 0, 0, 0.04) 0%, transparent 5px),
    linear-gradient(to top, rgba(0, 0, 0, 0.04) 0%, transparent 16px),
    linear-gradient(105deg, rgba(0, 0, 0, 0.04) 0%, transparent 10px),
    linear-gradient(255deg, rgba(0, 0, 0, 0.04) 0%, transparent 10px);
  --hover-edge-raw: oklch(from var(--current-user-color) var(--set-3-l) var(--set-3-c) h);
  --hover-edge: var(--hover-edge-raw, var(--nk-border-plain));
  --hover-edge-dark: oklch(from var(--hover-edge) calc(l * 0.8) c h);
  border: 2px solid #FFFFFF;
  border-right: 2px solid var(--hover-edge-dark);
  border-bottom: 2px solid var(--hover-edge-dark);
  box-shadow:
    inset 2px 0 0 0 var(--hover-edge),
    inset -2px 0 0 0 var(--hover-edge),
    inset 0 2px 0 0 var(--hover-edge),
    inset 0 -2px 0 0 var(--hover-edge),
    4px 4px 0 0 rgba(0, 0, 0, 0.2);
  transition: transform 0.12s ease, box-shadow 0.12s ease;
}
.nuke-container .nuke-nav-list .nuke-nav-item:not(.nuke-nav-item-static):not(.nuke-nav-logo-item):not(.is-active):hover {
  transform: scale(1.03);
}
.nuke-container .nuke-nav-list .nuke-nav-item:not(.nuke-nav-item-static):not(.nuke-nav-logo-item):not(.is-active):active {
  transform: scale(0.97);
}
/* Active/selected item = pressed flush into the surface: drop shadow 4px → 1px (keeping the inset themed
   bevel edges), exactly like the active profile-page tabs. Its 6-class selector beats the base box-shadow
   rule outright, and the scale rules above are gated off `.is-active`, so the selected item sits still. */
.nuke-container .nuke-nav-list .nuke-nav-item.is-active:not(.nuke-nav-item-static):not(.nuke-nav-logo-item) {
  box-shadow:
    inset 2px 0 0 0 var(--hover-edge),
    inset -2px 0 0 0 var(--hover-edge),
    inset 0 2px 0 0 var(--hover-edge),
    inset 0 -2px 0 0 var(--hover-edge),
    1px 1px 0 0 rgba(0, 0, 0, 0.2);
}

/* Brand logo — a circular icon button at the top, glyph centered. The 50px box +
   4px left margin lines the glyph's center up with the nav items' icon centers
   (placeholder mark; swap for a real logo asset later). */
.nuke-container .nuke-nav-logo {
  display: flex;
  align-items: center;
  justify-content: center;
  flex-shrink: 0;
  width: 50px;
  height: 50px;
  margin: 8px 0 2px 0;
  border: none;
  background: none;
  border-radius: 9999px;
  color: var(--nk-text);
  font-size: calc(30px - var(--font-shrink, 0px));
  line-height: 1;
  cursor: pointer;
}

/* Primary "Post" button — x.com's filled, rounded-full accent button. Stretches
   to the rail width (minus margins) since `.nuke-nav` is a stretch flex column. */

/* Account chip — pinned to the BOTTOM of the rail (margin-top:auto eats the free
   space above it). Avatar + name/@handle + a trailing ··· , in a rounded hover
   pill. Populated by renderNukeUserCard via `.nuke-profile-card-header`. */
/* Avatar uses the site's standard pfp chrome — gloss/rim + 28% rounded corners
   via `.cc-pfp-wrapper` + `.nuke-post-pfp` (40px), same as the message and chat
   header avatars — instead of a plain circle. Just guard it from flex-shrinking. */

/* Live game stats in the rail (nuke countdown + global DPR) — styled exactly like
   the nav links (icon + label, same size/weight), just informational: no hover
   wash, no pointer. Populated live by initNukeNav(). */
.nuke-container .nuke-nav-item-static {
  cursor: default;
}
.nuke-container .nuke-nav-item-static:hover {
  background: none;
}
/* Tabular figures so the ticking countdown doesn't wobble its width. */
.nuke-container .nuke-nav-timer-value {
  font-variant-numeric: tabular-nums;
}
.nuke-container .nuke-side-right {
  /* RIGHT column of the trio (Shop + Season Pass + notebook): a gap to the right
     of the wider centered middle (chat) column. The pills + search headers sit
     above it; no XP bar here, so it runs to the very bottom. */
  left: calc(50% - var(--trio-shift) + var(--chat-w) / 2 + var(--col-gap));
  right: 0; /* right panel BACKGROUND runs to the viewport RIGHT edge */
  width: auto;
  bottom: 0;
  /* Same background as the page (body.page-nuke): solid profile-tinted colour + the
     200px top-fade gradient + the brilliant.png texture pattern. */
  background-color: var(--page-bg-themed, var(--nk-surface));
  background-image:
    url('/img/ui/brilliant.png'),
    linear-gradient(to bottom in oklab, var(--page-grad-top) 0, transparent 200px);
  background-repeat: repeat, no-repeat;
  background-position: 0 0, 0 0;
  background-size: auto, auto;
  background-attachment: scroll, scroll; /* panel-relative: the top-fade starts at the panel (header) */
  transition: background-color 0.4s ease, --page-grad-top 0.4s ease;   /* re-theme smoothly with the page */
  /* Top gap (first box 16px below the header; bg still fills the padding) + side inset. Matching 16px
     bottom gap so a scrolled panel ends with the same breathing room as the header→first-box gap. */
  padding-top: 16px;
  padding-bottom: 16px;
  padding-left: var(--right-box-inset);
  padding-right: calc(50% + var(--trio-shift) - var(--chat-w) / 2 - var(--col-gap) - var(--right-col-w) + var(--right-box-inset)); /* keep the boxes in the right column; bg fills the wing */
  /* Short viewports: if the stacked boxes are taller than the fixed column, let the
     whole right panel scroll instead of clipping — scrollbar hidden. */
  overflow-y: auto;
  scrollbar-width: none; /* Firefox */
}
.nuke-container .nuke-side-right::-webkit-scrollbar { display: none; } /* WebKit */
/* Right-column card border width follows the center-panel token (--nk-panel-border-width) so one knob
   moves it with the chat panel + search bar. Scoped to .nuke-side-right so the shared .nuke-plain-box
   chrome used elsewhere keeps its own 1px border. box-sizing:border-box means it eats inward — no shift. */
.nuke-container .nuke-side-right .nuke-plain-box { border-width: var(--nk-panel-border-width); }

/* --- Nuke stage tab content panes ------------------------------ */

.nuke-container .nuke-tab-pane {
  position: absolute;
  inset: 0;
  overflow: auto;
  padding: 12px;
}
.nuke-container .nuke-tab-pane.hidden {
  display: none;
}

/* Profile pane: flex column hosting the centered full profile card. */
.nuke-container .nuke-tab-pane[data-tab-pane="1"] {
  display: flex;
  flex-direction: column;
  padding: 0;
}

/* --- Notebook paper widget (right column, below shop) ---------- */

/* Base notebook box — establishes the positioning context for the absolutely
   positioned paper/canvas/content layers, the container-query context (every
   notebook dimension below is in `cqw`), and the ruled-line pitch variables. */
.nuke-container .nuke-notebook {
  flex: 1;
  min-height: 0;
  position: relative;
  container-type: inline-size;
  /* Ruled-line pitch — single source of truth for the paper's blue lines AND
     the mission text line-height, so every line of text rests on a rule. */
  --rule: 7.0625cqw;
  /* Vertical nudge so text baselines sit just on top of the rules (tuned). */
  --rule-rest: 1.3cqw;
}
/* Notebook in the RIGHT column below the shop — fills the column width like the
   other containers there, at a fixed height. */
.nuke-container .nuke-side-right .nuke-notebook {
  width: 100%;
  flex: 0 0 400px; /* fixed height (was 40% of the column) */
  min-height: 0;
}
.nuke-container .nuke-notebook-inner {
  position: absolute;
  inset: 0;
  filter: drop-shadow(0 2px 4px rgba(0, 0, 0, 0.06)) drop-shadow(0 8px 20px rgba(0, 0, 0, 0.06));
}

.nuke-container .nuke-notebook-canvas {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  cursor: url('/img/cursors/pencil.png') 5 18, crosshair !important;
  touch-action: none;
  filter: url(#fibrous-tear);
  mask-image:
    radial-gradient(circle at 7.8125cqw 20%, transparent 3.4375cqw, black 3.4375cqw),
    radial-gradient(circle at 7.8125cqw 50%, transparent 3.4375cqw, black 3.4375cqw),
    radial-gradient(circle at 7.8125cqw 80%, transparent 3.4375cqw, black 3.4375cqw);
  mask-composite: intersect;
  -webkit-mask-image:
    radial-gradient(circle at 7.8125cqw 20%, transparent 3.4375cqw, black 3.4375cqw),
    radial-gradient(circle at 7.8125cqw 50%, transparent 3.4375cqw, black 3.4375cqw),
    radial-gradient(circle at 7.8125cqw 80%, transparent 3.4375cqw, black 3.4375cqw);
  -webkit-mask-composite: source-in;
}

.nuke-container .nuke-notebook-paper {
  position: absolute;
  inset: 0;
}

/* All the paper visuals — crumple/tear filter, cream fill, noise, ruled lines,
   red margin, binder-hole mask — live on this layer BEHIND the text. Keeping the
   filter off .nuke-notebook-paper itself means it no longer cascades to the
   .nuke-notebook-content child, so the mission text stays crisp/undistorted while
   the paper keeps its crinkle. */
.nuke-container .nuke-notebook-paper::before {
  content: '';
  position: absolute;
  inset: 0;
  /* Match the avatar/reply roundedness — shared --pfp-radius. (Was 3.75cqw.) */
  border-radius: var(--pfp-radius);
  border: 1px solid #EBDFA8;
  /* No fibrous-tear displacement here — it distorted the corners into a torn edge;
     dropping it lets the rounded corners render cleanly, matching the shop container. */
  background-color: #FFFFE1;
  background-image:
    url("data:image/svg+xml,%3Csvg viewBox='0 0 200 200' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.8' numOctaves='4' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)' opacity='0.05'/%3E%3C/svg%3E"),
    linear-gradient(90deg, transparent 18.75cqw, rgba(232, 135, 135, 0.5) 18.75cqw, rgba(232, 135, 135, 0.5) 19.0625cqw, transparent 19.0625cqw),
    repeating-linear-gradient(180deg, transparent, transparent calc(var(--rule) - 1px), rgba(123, 163, 194, 0.3) calc(var(--rule) - 1px), rgba(123, 163, 194, 0.3) var(--rule));
  background-size: 100% 100%, 100% 100%, 100% 100%;
  background-position: 0 0, 0 0, 0 0;
  mask-image:
    radial-gradient(circle at 7.8125cqw 20%, transparent 3.4375cqw, black 3.4375cqw),
    radial-gradient(circle at 7.8125cqw 50%, transparent 3.4375cqw, black 3.4375cqw),
    radial-gradient(circle at 7.8125cqw 80%, transparent 3.4375cqw, black 3.4375cqw);
  mask-composite: intersect;
  -webkit-mask-image:
    radial-gradient(circle at 7.8125cqw 20%, transparent 3.4375cqw, black 3.4375cqw),
    radial-gradient(circle at 7.8125cqw 50%, transparent 3.4375cqw, black 3.4375cqw),
    radial-gradient(circle at 7.8125cqw 80%, transparent 3.4375cqw, black 3.4375cqw);
  -webkit-mask-composite: source-in;
}

.nuke-container .nuke-notebook-content {
  position: relative;
  width: 100%;
  height: 100%;
  /* Top padding = 2 rules so the first line starts on the grid; bottom padding
     clears the snapped footer cell (its top edge = --footer-bottom + one rule). */
  padding: calc(var(--rule) * 2) 6.25cqw calc(var(--footer-bottom, calc(var(--rule) * 1.5)) + var(--rule) + 1cqw) 20cqw;
  display: flex;
  flex-direction: column;
  /* If the list ever outgrows the paper (short viewport, many wrapped rows),
     scroll inside the paper rather than spilling rows/footer onto the window
     below. */
  overflow-y: auto;
  overflow-x: hidden;
  min-height: 0;
  font-family: var(--font-conduit);
  color: #2d3748;
  /* 12px = the page's canonical list/UI text size (.nuke-row-name); the .nuke
     subtree runs at an 11px base under zoom, so the old 16px was an anomaly. */
  font-size: calc(14px - var(--font-shrink, 0px));
  box-sizing: border-box;
}

.nuke-container .nuke-notebook-row {
  display: flex;
  align-items: flex-start;
  gap: 2.5cqw;
  /* No vertical padding/min-height: each row is sized purely by its text, whose
     line-height equals one rule — so single- and multi-line rows both stay on
     the ruled grid and a wrapped description just occupies the next rule. */
  position: relative;
}

.nuke-container .nuke-notebook-check {
  position: absolute;
  /* In the red-margin gutter, clear of the binder holes. Absolute + top-anchored
     to the (flex-start) row means it always sits in the task's top-left corner
     and never drifts when the description wraps to a second line. */
  left: -8cqw;
  /* Track the text's baseline nudge so the box stays centered on its line. */
  top: calc(var(--rule-rest) + 1cqw);
  width: 4.6cqw;
  height: 4.6cqw;
  border: 0.7cqw solid rgba(74, 85, 104, 0.7);
  border-radius: 1.5cqw;
  transform: rotate(-2deg);
  box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.1);
}

.nuke-container .nuke-notebook-check--checked {
  border-color: rgba(25, 39, 66, 0.7);
  transform: rotate(1deg);
}

.nuke-container .nuke-notebook-check--checked::after {
  content: '';
  position: absolute;
  left: 30%;
  top: 5%;
  width: 35%;
  height: 75%;
  border-right: 2px solid rgba(25, 39, 66, 0.85);
  border-bottom: 2px solid rgba(25, 39, 66, 0.85);
  transform: rotate(40deg);
}

.nuke-container .nuke-notebook-text {
  flex: 1 1 auto;
  min-width: 0;
  /* One rule per line of text + a small downward nudge so the baseline rests on
     the printed rule (writing-on-lined-paper). translateY keeps every wrapped
     line shifted by the same amount, so they all stay registered. */
  line-height: var(--rule);
  transform: translateY(var(--rule-rest));
  overflow-wrap: anywhere;
}

.nuke-container .nuke-notebook-text--done {
  text-decoration: line-through;
  text-decoration-color: rgba(239, 68, 68, 0.7);
  text-decoration-thickness: 2px;
}

/* Active-mission grant progress (e.g. "1/3"). flex-shrink:0 + nowrap keep it
   fully visible; align-self:flex-start pins it beside the first text line so it
   stays top-right even when the description wraps. Spacing comes from the row's
   gap, not padding. */
.nuke-container .nuke-notebook-badge {
  margin-left: auto;
  align-self: flex-start;
  font-size: calc(12px - var(--font-shrink, 0px));
  /* Match the text's rule height + baseline nudge so it lines up with line 1. */
  line-height: var(--rule);
  transform: translateY(var(--rule-rest));
  color: #7a8aa0;
  white-space: nowrap;
  flex-shrink: 0;
}
/* Exhausted-group + cap-locked missions read muted / italic. */
.nuke-container .nuke-notebook-item--exhausted .nuke-notebook-text,
.nuke-container .nuke-notebook-item--locked .nuke-notebook-text {
  color: #9aa5b1;
  font-style: italic;
}
/* Quest-streak + reset-countdown footer. It sits ABOVE the drawing canvas (so
   its clear button is clickable and it never scrolls away with the rows),
   pinned to the bottom of the paper. pointer-events:none lets you keep drawing
   through the streak/timer text — only the clear button re-enables clicks. */
.nuke-container .nuke-notebook-footer {
  position: absolute;
  left: 0;
  right: 0;
  /* --footer-bottom is set by snapNukeNotebookFooter() (JS) so this one-rule
     cell's bottom edge lands exactly on a blue rule — the streak then rests on
     the grid like the missions instead of floating between lines. */
  bottom: var(--footer-bottom, calc(var(--rule) * 1.5));
  height: var(--rule);
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: 2cqw;
  /* padding-left:20cqw keeps the streak aligned under the mission-text column. */
  padding: 0 6.25cqw 0 20cqw;
  font-size: calc(12px - var(--font-shrink, 0px));
  pointer-events: none;
}
/* Streak + timer use the mission text's rule rhythm so their baseline rests on
   the cell's bottom rule, matching the rows above. */
.nuke-container .nuke-notebook-streak,
.nuke-container .nuke-notebook-timer {
  line-height: var(--rule);
  transform: translateY(var(--rule-rest));
  white-space: nowrap;
}
.nuke-container .nuke-notebook-streak {
  font-weight: 700;
  color: #c2410c;
}
.nuke-container .nuke-notebook-timer {
  color: #7a8aa0;
  text-align: right;
}

/* Always-present eraser button in the left margin gutter, to the left of the
   streak. Eraser glyph via mask so the TS/HTML stays a plain <button> primitive;
   background-color is the icon ink. pointer-events:auto re-enables clicks that
   the footer disables. Clears the drawing canvas (handler in notebook.ts). */
.nuke-container .nuke-notebook-clear {
  position: absolute;
  left: 12cqw;
  /* Center on the footer's text line, same calc as the mission checkboxes. */
  top: calc(var(--rule-rest) + 1cqw);
  width: 4.6cqw;
  height: 4.6cqw;
  padding: 0;
  border: 0;
  background-color: #7a8aa0;
  -webkit-mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='2.2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='m7 21-4.3-4.3c-1-1-1-2.5 0-3.4l9.6-9.6c1-1 2.5-1 3.4 0l5.6 5.6c1 1 1 2.5 0 3.4L13 21'/%3E%3Cpath d='M22 21H7'/%3E%3Cpath d='m5 11 9 9'/%3E%3C/svg%3E") center / contain no-repeat;
  mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='2.2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='m7 21-4.3-4.3c-1-1-1-2.5 0-3.4l9.6-9.6c1-1 2.5-1 3.4 0l5.6 5.6c1 1 1 2.5 0 3.4L13 21'/%3E%3Cpath d='M22 21H7'/%3E%3Cpath d='m5 11 9 9'/%3E%3C/svg%3E") center / contain no-repeat;
  cursor: pointer;
  pointer-events: auto;
  transition: background-color 0.12s ease;
}
.nuke-container .nuke-notebook-clear:hover {
  background-color: #c2410c;
}

/* --- Window chrome (MSN-style frames + title-bar + buttons) ---- */

.nuke-container .nuke-window-cluster {
  position: relative;
  isolation: isolate;
}
.nuke-container .nuke-window-cluster > .window-frame {
  position: relative;
  z-index: 1;
}
.nuke-container .window-frame {
  background-color: #E6F1FF;
  /* Matches `.chat-bubble-local`'s soft-blue border instead of the
     old bright `#0054E3` so all the chrome reads as the same color
     family. */
  border: 1px solid #C8DDF2;
  /* Matches the PFP design's effective radius (16% of 58px ≈ 9px) so
     the window's corner softness reads alongside the calling-card PFP
     as the same chrome family. `overflow: hidden` clips child
     backgrounds (gradient header, footer strip) to the rounded
     corners — without it, children paint past the rounded edge. */
  border-radius: 9px;
  overflow: hidden;
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
  /* Smoothly re-theme the wallpaper when the page colour changes (visiting a profile / post re-themes the page). */
  transition: background-color 0.4s ease, --page-grad-top 0.4s ease;
}
/* Empty placeholder container where the missions notebook used to be — fills
   the remaining height; keeps the default fully-rounded window-frame look. */
/* Combined left container (`.nuke-side-stack`, a flex column): the scrollable
   shop list spans the full height, with the inventory row pinned to the bottom
   — both in one white container. */
/* Plain white container + 1px gray border — replaces the old blue window-frame
   chrome for the shop & season-pass panels. No tab header, shading, or shadow. */
.nuke-container .nuke-plain-box {
  background: var(--nk-surface);
  border: 1px solid var(--nk-border-soft);
  /* Match the avatar/reply roundedness — shared --pfp-radius. (Was 18px.) */
  border-radius: var(--pfp-radius);
  overflow: hidden;
}
/* Right-column widget containers are squared — no rounded edges (nav keeps its own radius). */
.nuke-container .nuke-side-right .nuke-plain-box { border-radius: 0; }
/* Shop box: a flex column so the scroll wrap fills it. Fixed height (the list
   scrolls inside) instead of growing to take the column's slack. */
.nuke-container .nuke-shop-box {
  display: flex;
  flex-direction: column;
  flex: 0 0 400px;
  min-height: 0;
  /* Containing block for the absolutely-positioned frosted header below. */
  position: relative;
}
/* Generic right-column widget box — the same white-card chrome as the shop
   (.nuke-plain-box gives the border/radius/bg) with a bold title header, but in
   normal flow + sized to its content. Used for the Top Post / Top Player widgets. */
.nuke-container .nuke-side-box {
  display: flex;
  flex-direction: column;
  flex: 0 0 auto;
  /* Retro hard offset drop-shadow (no blur), bottom-right — like a classic button. */
  box-shadow: 4px 4px 0 0 rgba(0, 0, 0, 0.2);
}
/* Hidden while viewing a profile — the Trending box drops out so the friends box stands alone
   (toggled by setFriendsPanel in contacts.ts). */
.nuke-container .nuke-side-box.nuke-side-box-hidden { display: none; }
/* CONTRIBUTE widget — styled like the profile HOVER POPUP (.nuke-hovercard): a squared, recessed themed
   card with a white top/left + darker-themed right/bottom BEVEL, themed inner edges, and a soft drop shadow.
   Themed to the VIEWER (--current-user-color). Header + body blend INTO the card (no dark bar) so the bevel
   frame reads around the content, exactly like the popup. */
.nuke-container .nuke-contribute-card {
  --card-bg-themed: oklch(from var(--current-user-color) var(--set-2-l) var(--set-2-c) h);
  /* Fill = the right-panel page-bg (SETTING 1) + brilliant.png texture, hue INVERTED (calc(h + 180)) so the
     card reads as the complement of the panel. No top-fade; the card's edge-shading gradients stay below. */
  --card-bg-inv: oklch(from var(--current-user-color) var(--set-1-l) var(--set-1-c) calc(h + 180));
  background-color: var(--card-bg-inv, var(--nk-surface));
  background-image:
    url('/img/ui/brilliant.png'),
    linear-gradient(to bottom, rgba(var(--nk-surface-rgb), 0.7) 0%, transparent 5px),
    linear-gradient(to bottom, rgba(0, 0, 0, 0.04) 0%, transparent 5px),
    linear-gradient(to top, rgba(0, 0, 0, 0.04) 0%, transparent 16px),
    linear-gradient(105deg, rgba(0, 0, 0, 0.04) 0%, transparent 10px),
    linear-gradient(255deg, rgba(0, 0, 0, 0.04) 0%, transparent 10px);
  --hover-edge-raw: oklch(from var(--current-user-color) var(--set-3-l) var(--set-3-c) calc(h + 180));
  --hover-edge: var(--hover-edge-raw, var(--nk-border-plain));
  --hover-edge-dark: oklch(from var(--hover-edge) calc(l * 0.8) c h);
  border: 2px solid #FFFFFF;
  border-right: 2px solid var(--hover-edge-dark);
  border-bottom: 2px solid var(--hover-edge-dark);
  box-shadow:
    inset 2px 0 0 0 var(--hover-edge),
    inset -2px 0 0 0 var(--hover-edge),
    inset 0 2px 0 0 var(--hover-edge),
    inset 0 -2px 0 0 var(--hover-edge),
    4px 4px 0 0 rgba(0, 0, 0, 0.2);
  padding: 12px;
}
/* Header blends into the themed card — no dark bar / border / fixed height; just a bold label at the top. */
.nuke-container .nuke-contribute-card .nuke-side-box-header {
  height: auto;
  padding: 0 0 8px 0;
  background: none;
  border-bottom: none;
  color: var(--nk-text);
  font-size: calc(18px - var(--font-shrink, 0px));
}
/* Body sits flush inside the card's own 12px padding. */
.nuke-container .nuke-contribute-card .nuke-side-box-body {
  padding: 0;
  min-height: 0;
  background: none;
}
.nuke-container .nuke-side-box-header {
  height: 38px;   /* = the search bar height (.nuke-search-row) */
  flex: 0 0 auto;
  display: flex;
  align-items: center;
  padding: 0 12px;
  font-size: calc(16px - var(--font-shrink, 0px));
  font-weight: 700;
  /* Colored header bar. Defaults to the brand accent; override per box with
     --side-box-header-bg / --side-box-header-text (set inline on the box). The
     card's overflow:hidden + 16px radius rounds the header's top corners. */
  color: var(--side-box-header-text, #fff);
  /* Match the page header (search bar): dark saturated profile fill (0.42/0.12) + themed bottom border;
     grayscale twin when colourless. */
  --sbh-fill: oklch(from var(--current-user-color) 0.42 0.12 h);
  background: var(--sbh-fill, oklch(0.42 0 0));
  border-bottom: 1px solid var(--nk-border-themed, var(--nk-border));
}
/* Dark mode mirrors the page header's darker fill (0.27/0.08). */
.theme-lightsout .nuke-container .nuke-side-box-header {
  --sbh-fill: oklch(from var(--current-user-color) 0.27 0.08 h);
  background: var(--sbh-fill, oklch(0.27 0 0));
}
/* Inverted header bar (Trending + Top Users — NOT the Users-Friends mode, which keeps the default bar):
   same dark fill + white text, but the profile HUE is inverted (h + 180), like the header back button.
   The bottom divider matches the SITE TOP HEADER's — a darker take on the fill (L × 0.88), drawn as an
   inset shadow rather than a real border — but 1px (not 2px) and inheriting the inverted --sbh-fill hue.
   Colourless → the neutral gray fallback (no hue to flip). */
.nuke-container .nuke-side-box-header.nuke-side-box-header-inverted {
  --sbh-fill: oklch(from var(--current-user-color) 0.42 0.12 calc(h + 180));
  background: var(--sbh-fill, oklch(0.42 0 0));
  border-bottom: none;
  box-shadow: inset 0 -1px 0 0 oklch(from var(--sbh-fill, oklch(0.42 0 0)) calc(l * 0.88) c h);
}
/* Trending header: the "Post" button colour (page-header fill rotated −100° in hue) instead of the
   inverted hue, with a matching 1px bottom border = that fill ×0.88 (the Post button's own border),
   drawn as an inset shadow. Colourless → the Post button's #0E8A53 green fallback. */
.nuke-container .nuke-side-box-header.nuke-side-box-header-post {
  --sbh-fill: oklch(from var(--current-user-color) 0.42 0.12 calc(h - 100));
  background: var(--sbh-fill, #0E8A53);
  border-bottom: none;
  box-shadow: inset 0 -1px 0 0 oklch(from var(--sbh-fill, #0E8A53) calc(l * 0.88) c h);
}
.nuke-container .nuke-side-box-body {
  padding: 12px;
  min-height: 88px;
}
/* Top Users / Users Friends side box: cap the list at 10 rows (each 54px + 8px gap) plus the box's
   12px top+bottom padding, then scroll. Top Users is always <=10 so it never scrolls; a user's
   friends list scrolls past 10. */
.nuke-container #nuke-top-players {
  box-sizing: border-box;
  max-height: calc(2 * 12px + 10 * (54px + 8px));
  overflow-y: auto;
}
/* Trending widget: the cycled post keeps its own card borders + inner padding; a small
   container padding insets it from the box edges. */
.nuke-container #nuke-trending {
  padding: 8px;
  overflow: hidden;
}
/* Tighter inner padding than a feed post (12px 16px) to free up space in the narrow widget. */
.nuke-container #nuke-trending .nuke-post {
  padding: 8px 10px;
}
/* Handle-then-username truncation comes from the shared post-header one-line protection
   (see `.nuke-post-head` above). The date, however, is ALWAYS hidden in the narrow widget
   (a feed post keeps it whenever there's room; here there rarely is). */
.nuke-container #nuke-trending .nuke-post-time,
.nuke-container #nuke-trending .nuke-post-dot {
  display: none;
}
.nuke-container .nuke-trending-empty {
  padding: 22px 12px;
  color: var(--nk-text-2);
  font-size: calc(13px - var(--font-shrink, 0px));
  text-align: center;
}
/* Twitter-style widget header (the "Today's News"-tab look): a bold title row
   pinned at the top of the box. Same frosted technique as the chat nav header —
   absolutely positioned OVER the scroll area with a translucent bg + backdrop
   blur, so the shop items blur as they scroll up behind it. The grid below gets
   matching top padding so the first row clears the header. */
.nuke-container .nuke-shop-header {
  position: absolute;
  top: 0;
  left: 0;
  right: 0;
  z-index: 5;
  height: 44px;
  display: flex;
  align-items: center;
  padding: 0 12px;
  font-size: calc(22px - var(--font-shrink, 0px));
  font-weight: 800;
  color: var(--nk-text);
  background: rgba(var(--nk-surface-rgb), 0.88);
  -webkit-backdrop-filter: blur(8px);
  backdrop-filter: blur(8px);
}
/* The header's bottom edge: a solid-white band fading UP into transparency, so the
   frosted/blurred header doesn't butt straight into the solid-white scroll mask
   below it — instead it eases into the mask's matching solid-white top. */
.nuke-container .nuke-shop-header::after {
  content: '';
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  height: 30px;
  /* Below the header's "Shop" text but above its background — the header sets a
     stacking context (z-index:5), so a negative z here sits above the bg fill yet
     under the inline title text, keeping the gradient off the letters. */
  z-index: -1;
  pointer-events: none;
  background: linear-gradient(to top, rgb(var(--nk-surface-rgb)) 0%, rgb(var(--nk-surface-rgb)) 15%, rgba(var(--nk-surface-rgb), 0) 100%);
}
/* Shop wrap takes all the slack above the inventory; the grid scrolls inside it
   (absolutely positioned, native bar hidden) with a JS-driven overlay thumb. */
.nuke-container .nuke-side-stack .nuke-shop-wrap {
  flex: 1 1 auto;
  min-height: 0;
  position: relative;
}
/* Top fade: a white→transparent gradient pinned just under the 44px frosted
   header so rows dissolve into it as they scroll up — no hard cut at the header
   edge. Sits above the grid content (z 4) but below the header (z 5). Hidden at
   the very top (nothing scrolled under yet); quickly fades in once scrolled
   (`.is-scrolled`, toggled in shop-panel.ts). */
.nuke-container .nuke-side-stack .nuke-shop-wrap::before {
  content: '';
  position: absolute;
  top: 44px;
  left: 0;
  right: 0;
  height: 32px;
  z-index: 4;
  pointer-events: none;
  background: linear-gradient(to bottom, rgb(var(--nk-surface-rgb)) 0%, rgba(var(--nk-surface-rgb), 0) 100%);
  opacity: 0;
  transition: opacity 0.12s ease;
}
.nuke-container .nuke-side-stack .nuke-shop-wrap.is-scrolled::before {
  opacity: 1;
}
.nuke-container .nuke-side-stack .nuke-shop-grid {
  position: absolute;
  inset: 0;
  overflow-y: auto;
  /* Native bar hidden — the overlay thumb (`.nuke-shop-thumb`) shows instead. */
  scrollbar-width: none;
}
.nuke-container .nuke-side-stack .nuke-shop-grid::-webkit-scrollbar {
  display: none;
}
/* Overlay scrollbar thumb — same chrome + behavior as the chat's
   `.nuke-chat-scroll-thumb` (driven by attachNukeOverlayScrollbar). */
.nuke-container .nuke-shop-thumb {
  position: absolute;
  /* Clears the 44px frosted header so the thumb doesn't run behind it. */
  top: 48px;
  right: 2px;
  width: 8px;
  /* Above the top fade mask (z 4) so the scrollbar is never veiled by it. The
     thumb starts at 48px — below the header — so sitting above it is harmless. */
  z-index: 5;
  background: #D1D1D1;
  border-radius: 4px;
  pointer-events: none;
  opacity: 0;
  transition: opacity 0.2s ease;
}
.nuke-container .nuke-shop-wrap.is-scrolling .nuke-shop-thumb {
  opacity: 1;
}
/* Vertical item list. The 4-column track grid is KEPT (each square is still a
   `1fr` column, so it's the exact same size as in the old 4×4 layout — and the
   same square as an inventory slot), but every slot is forced into column 1
   (see `.inventory-slot` below) so the items stack straight down the left edge,
   one per row, instead of flowing across all four columns. `align-content:start`
   keeps rows at their natural square height rather than stretching. */
.nuke-container .nuke-shop-grid {
  display: grid;
  grid-template-columns: 1fr;
  align-content: start;
  gap: 8px;
  /* Top padding clears the 44px frosted header so the first row isn't hidden.
     Extra RIGHT padding (16px) keeps the right-aligned price tag clear of the
     overlay scrollbar thumb (right:2px + 8px wide → occupies the rightmost 10px). */
  padding: 52px 16px 8px 8px;
}
/* Each shop entry is a clickable ROW: the square icon box on the left, and the
   item title (top) + description (below) as plain text BESIDE it (outside the
   box), top-aligned. */
.nuke-container .nuke-shop-row {
  grid-column: 1;
  /* The grid track is minmax(auto,1fr); without this the title's `nowrap`
     min-content blows the track past the panel and the price overflows. */
  min-width: 0;
  display: flex;
  flex-direction: row;
  align-items: center;
  gap: 10px;
  cursor: pointer;
}
/* The square icon box (the "item container") — same size as an inventory slot. */
.nuke-container .nuke-shop-grid .inventory-slot {
  position: relative;
  flex-shrink: 0;
  width: 64px;
  height: 64px;
  padding: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  background: #F2F2F2;
  border: 1px solid #E5E5E5;
  /* Match the avatar/reply roundedness — shared --pfp-radius. (Was 8px.) */
  border-radius: var(--pfp-radius);
  transition: background 0.12s ease, border-color 0.12s ease;
}
.nuke-container .nuke-shop-row:hover .inventory-slot {
  background: #EAF2FB;
  border-color: #C8DDF2;
}
.nuke-container .nuke-shop-grid .inventory-slot.empty {
  cursor: default;
}
.nuke-container .nuke-shop-grid .inventory-slot.empty:hover {
  background: #F2F2F2;
  border-color: #E5E5E5;
}
/* `filter:none` cancels styles.css's `.inventory-slot.equipped .slot-icon`
   invert, which would mangle the emoji on the light background. */
.nuke-container .nuke-shop-grid .slot-icon {
  font-size: calc(26px - var(--font-shrink, 0px));
  line-height: 1;
  filter: none;
  flex-shrink: 0;
}
/* Item text column to the right of the icon — a head row (title + price) on top,
   description below. Flex-fills the row so the price tag rides the right edge. */
.nuke-container .nuke-shop-item-text {
  flex: 1 1 auto;
  min-width: 0;
  display: flex;
  flex-direction: column;
  gap: 3px;
}
/* Head row: title hugs the left and truncates; price tag pinned to the right. */
.nuke-container .nuke-shop-item-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 8px;
}
.nuke-container .nuke-shop-item-name {
  flex: 1 1 auto;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  font-size: calc(15px - var(--font-shrink, 0px));
  font-weight: 700;
  color: var(--nk-text);
  line-height: 1.25;
}
.nuke-container .nuke-shop-item-desc {
  font-size: calc(12px - var(--font-shrink, 0px));
  color: var(--nk-text-2);
  line-height: 1.35;
}
/* Price tag — a money-green capsule hugging its "$X.XX", right-aligned in the head
   row and vertically centered with the title. */
.nuke-container .nuke-shop-price {
  flex-shrink: 0;
  padding: 1px 8px;
  border: 1px solid #B7E4CE;
  border-radius: 9999px;
  background: #E9F8F1;
  color: #0E8A53;
  font-family: var(--font-conduit);
  font-weight: 700;
  font-size: calc(12px - var(--font-shrink, 0px));
  line-height: 1.4;
}
/* Quick-buy pill — same shape/size as the price tag but FILLED green so it reads as the
   buy action. Click purchases immediately, skipping the preview modal. */
.nuke-container .nuke-shop-buy {
  flex-shrink: 0;
  padding: 1px 10px;
  border: 1px solid #0E8A53;
  border-radius: 9999px;
  background: #0E8A53;
  color: #FFFFFF;
  font-family: var(--font-conduit);
  font-weight: 700;
  font-size: calc(12px - var(--font-shrink, 0px));
  line-height: 1.4;
  cursor: pointer;
  transition: filter 0.15s ease;
}
.nuke-container .nuke-shop-buy:hover { filter: brightness(1.08); }
.nuke-container .nuke-shop-buy:active { filter: brightness(0.94); }
/* Transient action toast (e.g. shop buy feedback) — dark pill, bottom-center, fades in. */
.nuke-container .nuke-toast {
  position: fixed;
  left: 50%;
  bottom: 28px;
  transform: translateX(-50%) translateY(8px);
  z-index: 1300;
  max-width: 360px;
  padding: 10px 18px;
  border-radius: 9999px;
  background: #0F1419;
  color: #FFFFFF;
  font-size: calc(14px - var(--font-shrink, 0px));
  font-weight: 700;
  text-align: center;
  box-shadow: 0 8px 28px rgba(15, 20, 25, 0.25);
  opacity: 0;
  pointer-events: none;
  transition: opacity 0.2s ease, transform 0.2s ease;
}
.nuke-container .nuke-toast.is-visible {
  opacity: 1;
  transform: translateX(-50%) translateY(0);
}
.nuke-container .nuke-toast.is-error { background: #F4212E; }
.nuke-container .nuke-shop-grid .slot-charges {
  position: absolute;
  bottom: 2px;
  right: 3px;
  font-size: calc(11px - var(--font-shrink, 0px));
  font-weight: 700;
  color: #555;
  background: rgba(var(--nk-surface-rgb), 0.85);
  border-radius: 4px;
  padding: 0 2px;
  line-height: 1.3;
}
/* Toolbar strips inside a window-frame (contact list search row, main
   stage tabs) use the window-frame's blue border color so the
   underline reads as part of the same chrome family. They also get
   the subtle inner feather (white top + sides, dark above the bottom
   border) for a slight inset depth — the standalone top page header
   stays flat. */

.nuke-container .inset-box {
  background: var(--nk-surface);
  border-left: 1px solid #A0A0A0;
  border-top: 1px solid #A0A0A0;
  border-right: 1px solid #FFFFFF;
  border-bottom: 1px solid #FFFFFF;
}
/* --- Classic XP-style buttons (Block / Video / etc.) ----------- */

.nuke-container .classic-button {
  background: linear-gradient(to bottom, #F4F8FC 0%, #D8E6F5 100%);
  border: 1px solid #83A6D4;
  border-radius: 3px;
  color: var(--nk-text);
  padding: 2px 8px;
  text-align: center;
}
.nuke-container .classic-button:hover {
  background: linear-gradient(to bottom, rgb(var(--nk-surface-rgb)) 0%, var(--nk-surface-2) 100%);
  border-color: #316AC5;
}
.nuke-container .classic-button:active {
  background: linear-gradient(to bottom, #D8E6F5 0%, #F4F8FC 100%);
  box-shadow: inset 1px 1px 2px rgba(0, 0, 0, 0.2);
}
/* Toggle/active state — used for the chat view tab buttons. */
.nuke-container .classic-button.is-active {
  background: linear-gradient(to bottom, #D8E6F5 0%, #F4F8FC 100%);
  box-shadow: inset 1px 1px 2px rgba(0, 0, 0, 0.2);
  border-color: #316AC5;
}
/* Chat-toolbar buttons (e.g. "Global Chat") borrow the search-bar's
   6px corner radius so they read as part of the same chrome family.
   Zeroed vertical padding so the button's text fills the visible
   gradient more snugly — without it, the 2px top/bottom from
   `.classic-button` leaves a thicker empty band around the text
   than the search bar shows around its input. */
.nuke-container .nuke-search-row .classic-button {
  border-radius: 6px;
  padding-top: 0;
  padding-bottom: 0;
  /* Match `.nuke-row-name` (contact-list username) so the toolbar
     reads as the same typographic scale. Explicit because Tailwind
     preflight is disabled — without it, `<button>` falls back to
     the user-agent default (~13.3px) rather than inheriting from
     `.nuke-container`. */
  font-size: calc(14px - var(--font-shrink, 0px));
  line-height: 16px;
}

/* --- Status dots + theme accents ------------------------------- */

.nuke-container .status-dot {
  /* Fixed-px circle (so it's ALWAYS a perfect circle — % sizing made it an
     ellipse), centered exactly on the pfp's bottom-right corner via
     translate(50%,50%) so it sits at that corner. --dot-size is set per context.
     `.nuke-container` beats the markup's Tailwind `bottom-0 right-0`. */
  /* width == height, so it's always square → a true circle. */
  width: var(--dot-size, 8px);
  height: var(--dot-size, 8px);
  bottom: 0;
  right: 0;
  transform: translate(50%, 50%);
  /* Was `rgba(0, 0, 0, 0.3)` — softened so the border reads as a
     subtle tint of the dot's inner gradient instead of a hard
     dark outline. */
  border: none;
  border-radius: 50%;
  box-shadow: inset 1px 1px 2px rgba(var(--nk-surface-rgb), 0.5);
  /* (Dot rotates via the wrapper's --cc-tilt now — no own transform needed.) */
}
/* Solid, unshaded online dot — no gradient, no inset gloss (box-shadow none
   cancels the highlight inherited from `.status-dot`). Color is the midpoint of
   the old #7DDA3F→#47A111 gradient so the tone is unchanged, just flat. */
.nuke-container .status-online  { background: #62BE28; box-shadow: none; }
.nuke-container .status-away    { background: linear-gradient(to bottom right, #FFD24D, #EAA600); }
.nuke-container .status-busy    { background: linear-gradient(to bottom right, #FF6B6B, #D31212); }
/* Solid, unshaded offline dot — flat gray, gloss cancelled. Color is the
   midpoint of the old #D0D0D0→#9A9A9A gradient. */
.nuke-container .status-offline { background: #B5B5B5; box-shadow: none; }
/* HIDDEN: presence status DOT removed SITEWIDE — the online/offline info now shows as TEXT on the profile
   page (.nuke-profile-page-status). All the dot markup + JS presence toggles are kept intact; unhide by
   deleting this one rule. (Colours above are reused by the profile status text.) */
.nuke-container .status-dot { display: none !important; }

/* Per-context status-dot sizing — --dot-size (dot diameter, ~20% of the pfp, kept in px so the dot is always
   a perfect circle). Centered on the pfp corner by the base .status-dot rule. */
.nuke-container .nuke-post-pfp,
.nuke-container .nuke-row-pfp { --dot-size: 8px; }
.nuke-container .nuke-hovercard-pfp { --dot-size: 11px; }
.nuke-container .nuke-post-quote-pfp { --dot-size: 5px; }

/* --- Chat bubbles (local / remote) ----------------------------- */

.nuke-container .chat-bubble-local {
  background-color: #F0F6FF;
  border: 1px solid #C8DDF2;
  border-radius: 4px;
}
.nuke-container .chat-bubble-remote {
  background-color: var(--nk-surface);
  border: 1px solid var(--nk-border-2);
  border-radius: 4px;
}
/* Brief blue-border flash when a reply-quote click scrolls to a
   target message. Border-color snaps instantly to blue (transition
   is suppressed inline for that paint), then the class is removed
   and the color eases back via the transition below. */
.nuke-container .chat-bubble-local,
.nuke-container .chat-bubble-remote {
  transition: border-color 0.4s ease-out;
  /* Subtle drop shadow on every bubble — single source of truth.
     Replaces the broken `shadow-sm` Tailwind utility that was being
     appended from the TS render template (preflight off → composed
     `box-shadow` variables resolve to undefined → no visible shadow). */
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
}
.nuke-container .chat-bubble-local.chat-msg-flash-border,
.nuke-container .chat-bubble-remote.chat-msg-flash-border {
  border-color: #2196F3;
}
/* Typography of message text inside the bubble. Was inline
   `style="font-family:...; color:..."` per bubble — now a class
   each. The visual contrast between local + remote stays the
   same: local reads as your own clean conduit text, remote reads
   as the playful purple Comic Sans. */
.nuke-container .nuke-chat-text-self {
  font-family: var(--font-conduit);
  /* SETTING 7 body text — the uploader's hue (defined on .nuke-post); no author colour → --nk-text-msg. */
  color: var(--body-text-themed, var(--nk-text-msg));
}
.nuke-container .nuke-chat-text-remote {
  font-family: var(--font-conduit);
  color: var(--body-text-themed, var(--nk-text-msg));
}

/* --- Chat content extras: links, reply, day separator, image -- */

/* `@mention` + URL links — X.com link blue (#1D9BF0), no underline at rest,
   underline on hover (matches X). URLs are real `<a target="_blank"
   rel="noopener noreferrer">` anchors, so they open in a new tab like X. */
.nuke-container .nuke-chat-link {
  /* URL links use a DARKER version of the link blue (accent hue+chroma, lower OKLCH lightness) so they
     read as calm dark-blue links. This same colour is used in BOTH light and dark mode (no dark override).
     Mentions (also .nuke-chat-link) override this with their own colour below (equal specificity, later
     source order wins). */
  color: oklch(from var(--nk-accent) 0.46 c h);
}
/* @-mention links show the username only — hide the leading avatar (matches
   the `.dock-pill` mention pills). */
.nuke-container .nuke-chat-link .pill-pfp {
  display: none;
}
.nuke-container .nuke-chat-link:hover {
  text-decoration: underline;
}
/* @-mention: bold like the username shown in message headers (`.nuke-post-name`).
   Underline on hover is inherited from `.nuke-chat-link:hover`; clicking it opens
   the user's profile (data-tgid → handler in nuke/index.ts). */
.nuke-container .nuke-chat-mention {
  font-weight: 400;
  font-style: italic;
  /* All @mentions use a DARKER version of the link blue (matches .nuke-chat-link), not the mentioned
     user's profile colour — same colour in light and dark mode (no dark override).
     (--mention-color is still set inline per mention, but no longer drives the colour.) */
  color: oklch(from var(--nk-accent) 0.46 c h);
}
/* `/command` tokens (e.g. `/happy`) — same X link blue as mentions/links, but not
   clickable (no hover underline / pointer): a plain visual highlight. */
.nuke-container .nuke-chat-command {
  /* The SENDER's profile colour (`--nuke-cmd-color`, set inline per post), HUE-ROTATED by
     --nk-hue-shift (same knob + same shade as the sticky notes) so the /word reads as a
     related-but-different tint of their colour, THEN lightness-floored via OKLCH so even a dark
     profile colour reads as a BRIGHT version of itself; falls back to the blue accent when unset.
     Chroma softened to 78% (calc(c * 0.78)) to match the link saturation. */
  color: oklch(from hsl(from var(--nuke-cmd-color, #808080) calc(h + 15) s l) clamp(0.55, l, 0.72) calc(c * 0.78) h);
  font-weight: 400; /* not bold */
  text-decoration: none; /* no underline at rest — only on hover (see below) */
  cursor: pointer; /* clickable — clicking re-posts this /word as your own message */
}
/* Underline ONLY on hover — matches the mention/link affordance (`.nuke-chat-link:hover`). */
.nuke-container .nuke-chat-command:hover {
  text-decoration: underline;
}
/* An EXPIRED /word — its source post is past the 10-min send window (the renderer tags the post
   `.nuke-post-cmd-expired`) — is NOT clickable: the click gate in chat-render does nothing, so drop the
   pointer cursor so it doesn't read as an action. */
.nuke-container .nuke-post-cmd-expired .nuke-chat-command { cursor: default; }
/* Expired /word isn't clickable, so it gets no hover underline either. */
.nuke-container .nuke-post-cmd-expired .nuke-chat-command:hover { text-decoration: none; }
/* Day separator above messages from a new day. Was inline
   `w-full text-center border-b border-[var(--nk-border-2)] leading-[0.1em] my-2`
   wrapping a `bg-white px-2 text-[#999] text-[10px]` label. The
   `border-b` was preflight-broken (silently rendering as
   `border-width: 1px` with `border-style: none`). */
.nuke-container .nuke-chat-day-sep {
  /* No borders, no fill — just the two divider lines + the date label, flush with the posts. */
  display: flex;
  align-items: center;
  gap: 8px;
  box-sizing: border-box;
  margin: 0 -10px;
  position: relative;
  z-index: 1;
  /* 12px side padding so the flanking lines stay inset from the container edges (not full width). */
  padding: 6px 12px;
  background: transparent;   /* no fill — the chat panel shows through */
  border-radius: 0;
}
/* Feed is full-width (0 side padding) now, so its dividers do not break out — they already reach the edges. */
.nuke-container #nuke-chat-feed .nuke-chat-day-sep { margin-left: 0; margin-right: 0; }
/* The two line halves flank the label and flex to fill the rest of the width,
   matching the post border colour. */
.nuke-container .nuke-chat-day-sep::before,
.nuke-container .nuke-chat-day-sep::after {
  content: '';
  display: block;
  flex: 1 1 auto;
  height: 1px;
  background: var(--nk-border-soft);
}
.nuke-container .nuke-chat-day-sep-label {
  /* Text colour = the profile TIMESTAMP colour (SETTING 4, --set4-themed → the viewer's hue; --nk-text-2 when colourless). */
  flex: 0 0 auto;
  color: var(--set4-themed, var(--nk-text-2));
  font-size: calc(12px - var(--font-shrink, 0px));
  white-space: nowrap;
}

/* --- Mention pill PFP spacing ---------------------------------- */
/* When an `@mention` follows a natural word-space (mid-sentence),
   the inline pfp gets 3px left margin so it doesn't sit flush with
   the preceding word. JS toggles `.nuke-mention-pfp-spaced` on the
   `.pill-pfp` based on the character that preceded the mention. */
.nuke-container .nuke-mention-pfp-spaced {
  margin-left: 3px;
}

/* --- Reaction badge typography (count) ------------------------- */
.nuke-container .nuke-reaction-badge {
  font-size: calc(14px - var(--font-shrink, 0px));
  line-height: 14px;
}
.nuke-container .nuke-reaction-count {
  color: var(--nk-text-2);
  font-family: var(--font-conduit);
  font-size: calc(13px - var(--font-shrink, 0px));
  font-weight: 600;
  font-variant-numeric: tabular-nums;
}

/* --- Chat image bubble wrapper (gray placeholder + radius) ----- */
.nuke-container .nuke-chat-img-wrap {
  background: var(--nk-border-2);
  /* Squared corners. */
  border-radius: 0;
  /* No resting shadow — images sit flat and only grow a drop
     shadow on hover (matches the reply boxes). */
}

/* --- Contact row hover + offline name color -------------------- */

/* Row hover background highlight. */
.nuke-container .nuke-row:hover {
  background-color: var(--card-hover, #EAF2FB);
}
/* Hover underline on the displayed name (was Tailwind `group-hover:underline`). */
.nuke-container .nuke-row:hover .nuke-row-name {
  text-decoration: underline;
}
/* Offline rows get a muted name color via the `data-status` attribute
   (was Tailwind `text-[#666]` inlined into the class string). */
.nuke-container .nuke-row[data-status="offline"] .nuke-row-name {
  color: #666;
}

/* --- Chat image shimmer placeholder ---------------------------- */

/* Shine/shimmer sweep across the gray placeholder while a chat image
   loads. The ::before pseudo paints first in source order, so the
   actual <img> child sits on top — when the img has opacity 1 it
   fully covers the shimmer, and when at opacity 0 (still loading) the
   shimmer shows through. */
.nuke-container .nuke-chat-img-wrap::before {
  content: '';
  position: absolute;
  inset: 0;
  background: linear-gradient(
    100deg,
    transparent 30%,
    rgba(var(--nk-surface-rgb), 0.3) 50%,
    transparent 70%
  );
  transform: translateX(-100%);
  animation: nuke-chat-img-shimmer 1.4s infinite ease-in-out;
  pointer-events: none;
}
/* Stop + hide the shimmer once the image has actually loaded — the
   `loaded` class is added by JS in the load handler. Important for
   transparent-background images where the shimmer would otherwise
   show through the loaded pixels. */
.nuke-container .nuke-chat-img-wrap.loaded::before {
  display: none;
}
@keyframes nuke-chat-img-shimmer {
  100% { transform: translateX(100%); }
}

/* ===== X.com-style chat feed ===================================== */
/* Each sender-chain renders as one post: avatar + name/@handle/time
   header, paragraph-spaced segments, quote-tweet replies, X-style
   1/2/3/4 image grids. Divider line per post; white feed. */
.nuke-container .nuke-post {
  /* Column: the avatar + content row on top, then a full-width footer bar that
     rests flush against the post's bottom edge. */
  display: flex;
  flex-direction: column;
  width: 100%;
  box-sizing: border-box;
  padding: 12px 16px;
  /* FLOATING CARD: each message is its own bordered square lifted off the feed, with a
     gap to the next (the outer frame comes from #nuke-chat-feed's padding). Replaces the
     old shared bottom-divider list. Squared corners to match the app's squared chrome.
     Depth comes from a hard-edge bevel — the bottom + right borders are 2px vs the 1px
     top + left — reading as a solid drop-shadow cast down-right (theme-independent, no
     blur). Squared corners. */
  /* Card FILL = the author's profile-colour tint (the same `--nuke-post-hl` mix that used to
     appear only on hover, set inline by renderPost), grey fallback for colourless authors —
     so the feed is tinted per author by default. Centralised in --card-bg so the fill AND its
     border stay in lockstep. The BORDER is a darker variant of that fill (mixed 20% toward
     black), tracking each card's own colour; bottom + right stay 2px for the bevel. */
  /* COLOR SETTING 2 (card / container background): the author's profile HUE run
     through setting 2's OKLCH L/C. Isolated in --card-bg-themed so a colourless
     author (no --nuke-cmd-color → invalid) falls cleanly to the neutral
     --nk-hover-1, leaving every downstream --card-bg consumer valid. */
  --card-bg-themed: oklch(from var(--nuke-cmd-color) var(--set-2-l) var(--set-2-c) h);
  --card-bg: var(--card-bg-themed, var(--nk-hover-1));
  /* COLOR SETTING 3 (border): the author's hue at setting 3 — the card's bottom
     border, its pfp ring, and its reply + cred arrows all read from --card-border.
     Colourless author → invalid → the neutral/user-themed --nk-border. */
  --card-border-themed: oklch(from var(--nuke-cmd-color) var(--set-3-l) var(--set-3-c) h);
  --card-border: var(--card-border-themed, var(--nk-border-plain));
  /* COLOR SETTING 7 (body text): the post BODY text, tinted to the uploader's own hue
     (dark but readable). Consumed via the fallback chain on .nuke-chat-text-*; colourless
     author → invalid → the neutral --nk-text-msg. */
  --body-text-themed: oklch(from var(--nuke-cmd-color) var(--set-7-l) var(--set-7-c) h);
  /* SETTING 4 (muted): IN a post → the poster's hue (overrides the container default). Drives this
     post's timestamp. Its footer SVG icons use --icon-themed (setting 6) below. */
  --set4-themed: oklch(from var(--nuke-cmd-color) var(--set-4-l) var(--set-4-c) h);
  --icon-themed: oklch(from var(--nuke-cmd-color) var(--set-6-l) var(--set-6-c) h);
  /* HOVER + PULSE, toned WAY down to sit in the soft (desaturated) theme palette — OKLCH steps just
     below the resting card (setting 2 = L.930/C.024) instead of a strong raw-colour mix:
       --card-darken — the new-message ARRIVAL flash + scroll-to PULSE (L.880/C.045, clearly darker).
       --card-hover  — the HELD hover tint (L.910/C.032, a hair darker than rest, lighter than the pulse).
     Isolated in *-themed so colourless authors fall to a neutral light grey (no forced hue). */
  --card-darken-themed: oklch(from var(--nuke-cmd-color) 0.880 0.045 h);
  --card-darken: var(--card-darken-themed, color-mix(in oklab, #808080 16%, var(--nk-surface)));
  --card-hover-themed: oklch(from var(--nuke-cmd-color) 0.910 0.032 h);
  --card-hover: var(--card-hover-themed, color-mix(in oklab, #808080 9%, var(--nk-surface)));
  /* Layered background: a VERY subtle white highlight gradient over the top half (a soft sheen,
     fading to nothing by the middle), then the faint dotted-grid TEXTURE (48px tile), over the
     profile-colour fill. The SVG's own opaque base was dropped so --card-bg shows through.
     --card-highlight (the sheen) is defined on .nuke-container (shared) so the chat-header profile card
     can use it too — a .nuke-post-scoped var is invisible there and would invalidate its whole
     background-image, dropping the texture layer with it. */
  background-color: var(--card-bg);
  /* No sheen, no texture — flat --card-bg fill only. */
  background-image: none;
  border: none;
  border-top: 1px solid #FFFFFF;                 /* white 1px top */
  border-bottom: 2px solid var(--card-border);   /* 2px bottom */
  /* SIDE borders now carry the PANEL FRAME (moved off .nuke-chat-container::after / the overlay): the
     REAL side border is the frame (outer left = --nk-border-2, outer right = --nk-border-soft), and the
     card's OWN side borders (white left / setting-3 right) are retained just inside as inset shadows —
     same 1px footprint, no width change. --post-side-bevel lets dark mode soften the left inset. */
  --post-side-bevel: #FFFFFF;
  border-left: 1px solid var(--nk-border-2);
  border-right: 1px solid var(--nk-border-soft);
  border-radius: 0;      /* squared edges */
  margin-bottom: 0;      /* no gap between posts — cards stack flush */
  /* X.com feed type: 15px base, weight 400; the name's explicit 700 stands. Shared with the search
     rows via --nk-card-font so both scale together. */
  font-size: var(--nk-card-font, 15px);
  font-weight: 400;
  position: relative;
  /* ALWAYS-ON depth: the 1px white INNER bevel on the top + left (a lit edge that pairs with the darker
     2px bottom/right borders) PLUS the down-right --card-lift drop shadow. The lift used to appear only on
     hover — it's now PERMANENT, so every message reads as a floating tile at rest; hover just tints. */
  /* Retained inner side borders (white left / setting-3 right), redrawn as inset shadows now that the real
     side borders carry the panel frame. Inset never clips (unlike an outward shadow under contain:paint). */
  box-shadow: inset 1px 0 0 0 var(--post-side-bevel), inset -1px 0 0 0 var(--card-border);
  /* Only the tint changes on hover now (the lift is permanent); the box-shadow transition still eases the
     lift when --card-lift flips with the theme. The scroll-to highlight opts out below. */
  transition: background-color 0.22s ease, box-shadow 0.18s ease;
}
/* Cards are inset from the panel edges (side gutters + a top gutter), IDENTICAL across the feed, /post and
   /profile. Each card's own margin-bottom supplies both the gap between cards and the frame below the last.
   The side padding also gives the cards' down-right lift shadow room before #nuke-chat-scroll's
   `contain:paint` clips it (0 side padding cropped the right shadow flush at the edge). */
.nuke-container #nuke-post-modal-body {
  padding: 0 10px 0;   /* no top gap — content butts against the back-bar */
}
/* /post + /profile: the post CARDS break out of the modal body 10px side padding to full width, so they
   match the edge-to-edge feed. The profile header/banner + day dividers keep the modal inset context. */
.nuke-container #nuke-post-modal-body .nuke-post {
  width: calc(100% + 20px);
  margin-left: -10px;
  margin-right: -10px;
}
/* Feed cards span the FULL chat-panel width — no side gutters (the /post + /profile modal body above
   keeps its 10px inset). Trade-off: the card's down-right --card-lift shadow now sits flush at the
   right edge, so #nuke-chat-scroll contain:paint crops its rightmost sliver. */
.nuke-container #nuke-chat-feed {
  padding: 0;   /* first post flush to the feed top — no gap */
}
/* "Lined paper" texture tiled CONTINUOUSLY across the whole post list — on the feed CONTAINER, not each
   post (per-post the tile reset its phase at every boundary, so the gaps read unevenly). A faint DOTTED
   horizontal rule every 12px from the provided SVG (--nk-paper-svg, themed light + dark), tiled 48×48 so
   every post shares one grid. Also on the subview overlay body so /post + /profile match. Sits behind the
   transparent posts (a hovered post's opaque tint covers its own slice — fine, the rules are ultra-faint). */
.nuke-container #nuke-chat-feed,
.nuke-container #nuke-post-modal-body {
  background-image: var(--nk-paper-svg);
  background-repeat: repeat;
}
/* Avatar gutter + message content, side by side (the old `.nuke-post` row). */
.nuke-container .nuke-post-row {
  display: flex;
  gap: 12px;
}
/* Floating cards each keep a full border — the last one is just another card with its
   margin-bottom framing it off the footer (no special-casing needed). */
.nuke-container .nuke-post-aside { flex: 0 0 auto; }

/* ── x.com-style comment-chain connector ─────────────────────────────
   A vertical thread line through the avatar gutter linking a user's event-pill
   post to their adjacent text post (set by renderPost via `sameThread`). The
   avatar is 40px at the post's 16px left padding, so its centre is x=36px; the
   footer starts at x=68px, so the line never collides with footer content.
   `-above` runs from the post's top edge to just above the avatar; `-below` from
   just below the avatar to the bottom edge — adjacent segments meet at the shared
   post border for one continuous line. */
.nuke-container .nuke-post-thread-above::before,
.nuke-container .nuke-post-thread-below::after {
  content: "";
  position: absolute;
  left: 36px;
  width: 2px;
  margin-left: -1px;
  /* Matches the host card's border colour (--card-border). Each segment is a pseudo of
     its OWN .nuke-post and is cut off at that card's edge, so a chain spanning differently
     coloured cards just shows each half in its own card's colour — the cross-container case
     resolves itself. Falls back to the neutral border if a post has no card colour. */
  background: var(--card-border, var(--nk-border));
  z-index: 0;
}
/* Chained posts stay SEPARATE floating cards (each keeps its border + inter-card gap).
   The connector line lives ONLY inside each card and is CUT OFF at the container edge —
   it never crosses the gap. Offsets are relative to each post's padding box: the upper
   post's `::after` runs down to its border-box bottom (−2px, through the 2px bottom
   border) and stops there; the lower post's `::before` starts flush at its border-box
   top (−1px, through the 1px top border). */
.nuke-container .nuke-post-thread-above::before { top: 0; height: 8px; } /* start at the padding top — just below the white 1px top border, so the chain line never covers it */
.nuke-container .nuke-post-thread-below::after { top: 56px; bottom: -2px; }
/* System / event posts (nuke, stake, season, …) render inline in the feed as
   normal message rows authored by the acting user — square pfp + full name /
   handle / time / balance / cred chrome (see renderActorSystemPost). No-user
   broadcasts use the "nuke" app identity. The `.nuke-post-system` hook class is
   kept on the article for any future system-specific styling. */
/* The top ticker pills now scroll to their inline event post on click. */
.nuke-container #nuke-pill-header .dock-pill {
  cursor: pointer;
}
/* Cred-vote posts (from nuke-profile, systemType credUp/credDown): tint the
   one-line sentence so the up/down read at a glance. The ▲/▼ glyph is baked
   into the content by the chat server. */
.nuke-container .nuke-post-cred-up { color: var(--color-nuke-success); }
.nuke-container .nuke-post-cred-down { color: var(--color-nuke-primary); }
/* Self-posts (your own posts, not votes): text in the AUTHOR's profile colour (color1, carried
   as `--nuke-cmd-color` on the .nuke-post), falling back to the normal text colour when unset. */
.nuke-container .nuke-post-cred-self { color: var(--nuke-cmd-color, var(--nk-text)); }
/* Doodle attachment in the FEED — a square post-it (its yellow bg is baked into the canvas) that
   draws + replays its strokes (doodle.ts). Matches the chat-image chrome: same --pfp-radius + 1px
   border, the same hover-lift, and the same 12px spacing from the text as .nuke-post-media. */
.nuke-container .nuke-post-doodle {
  position: relative;
  margin-top: 8px;
  width: 100%;
  max-width: none; /* fill the full post-body width (like images), no leftover side space */
  /* Pastel of the author's profile colour (from the inherited --nuke-cmd-color). The canvas is
     filled with this same resolved colour (doodle.ts reads getComputedStyle().backgroundColor),
     so strokes sit on it. Falls back to a soft post-it yellow when the author has no colour. */
  /* Note bg = a pastel of the author's profile colour; white/no colour falls back to post-it yellow
     (validProfileHex rejects #ffffff, so --nuke-cmd-color is unset there). */
  /* Tinted like the reply/message card (subtle --card-bg), NOT the strong post-it colour. The gradient
     + 2px border below still derive from --sticky-bg, so their design is unchanged. */
  /* Sticky note colour = the profile colour with its HUE rotated slightly (not the exact colour) —
     a related-but-different shade. --nk-hue-shift (deg, on .nuke-container) is the knob; border + canvas follow. */
  --sticky-bg: hsl(from var(--card-bg) calc(h + 15) s l);
  /* Chrome-less in chat: no background, border, or shadow — just the doodle. The canvas fill reads the
     (now transparent) background, so the strokes float on the post card. */
  background: none;
  border: none;
  border-radius: 0;      /* no rounded edges */
  overflow: hidden;
  box-shadow: none;
  transition: transform 0.09s ease;   /* smooth hover scale */
}
.nuke-container .nuke-post-doodle:hover {
  transform: scale(1.05);   /* hover scale (kept in sync with the JS press rest for .nuke-post-doodle) */
  box-shadow: none;   /* no hover shadow */
}
.nuke-container .nuke-post-doodle:active {
  box-shadow: none;
}
.nuke-container .nuke-post-doodle-canvas {
  display: block;
  width: 100%;
  aspect-ratio: 16 / 9; /* wide landscape — composer + feed MUST match so drawings dont distort */
}
/* Doodle lightbox removed — clicking a doodle now REPLAYS its draw-in in place (see doodle.ts). */
/* Self-post composer attachment controls. The compose modal mounts on <body>, so these are
   UNSCOPED (they inherit the --nk-* theme tokens from body.page-nuke). */
.selfpost-attach-row { display: flex; gap: 8px; flex-wrap: wrap; }
/* Icon-only attach buttons, styled like the post-footer action icons (.nuke-post-action-btn): a 28px
   transparent circle, no border, themed icon colour. No text label (title + aria-label carry the name). */
.selfpost-attach-btn {
  width: 28px;
  height: 28px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  border: none;
  border-radius: 9999px;
  background: transparent;
  color: var(--icon-themed, var(--nk-text-2));
  font-size: calc(18px - var(--font-shrink, 0px));
  line-height: 1;
  cursor: pointer;
}
.selfpost-attach-btn:hover svg { scale: 1.1; } /* footer-style hover — no bg circle */
.selfpost-attach-btn i { font-size: calc(18px - var(--font-shrink, 0px)); }
.selfpost-attach-btn svg { width: 15px; height: 15px; stroke-width: 2.4; } /* size + effective stroke match the footer icons (15px, 10% stroke ratio) */
/* Selected Music / Video mode → the icon lights up in the accent colour (no bg pill now). */
.selfpost-attach-btn.is-active { color: var(--nk-accent, #1D9BF0); }
.selfpost-attach-preview { position: relative; width: 100%; } /* fills the doodle's slot in the right column */
.selfpost-attach-preview img {
  display: block;
  width: 100%;
  aspect-ratio: 16 / 9;   /* same box as the doodle drawable area it replaces */
  object-fit: cover;
  border-radius: 0;       /* no rounded edges */
  border: 1px solid var(--nk-border-soft, #cfd9de);
}
.selfpost-attach-remove {
  position: absolute;
  top: 6px;
  left: 6px;
  width: 26px;
  height: 26px;
  border-radius: 50%;
  border: none;
  background: rgba(0, 0, 0, 0.6);
  color: #fff;
  cursor: pointer;
  font-size: calc(13px - var(--font-shrink, 0px));
  line-height: 1;
}
.selfpost-sticky {
  position: relative;
  /* Centered post-it note. */
  margin: 0 auto;
  width: 100%;
  max-width: none; /* fill the modal width (wide doodle) */
  box-sizing: border-box;
  --sticky-bg: #fef9c3; /* post-it yellow (kept for the pattern swatches) */
  background: transparent; /* transparent doodle — canvas fills the modal bg via initNukeDraw */
  border: none; /* no border */
  border-radius: 2px; /* rounded corners */
  padding: 6px 0; /* no SIDE padding — the canvas spans the full note width */
}
/* Self-post composer: the doodle note goes EDGE TO EDGE with the modal (cancel the cards 20px side padding). */
.compose-selfpost .selfpost-sticky {
  width: auto;
  /* Full width of the right column (aligned with the text input above it). */
  margin-left: 0;
  margin-right: 0;
  padding-top: 0; /* no gap between the text input and the doodle canvas */
}
/* The doodle CANVAS (not the settings) aligns with the text input: left indented past the avatar
   column (pfp 44px + gap 12px + the 10px full-bleed offset), right gets the text inputs 10px padding.
   The canvas keeps 16:9 (aspect-ratio) as it narrows. */
.compose-selfpost .selfpost-canvas-wrap {
  margin-left: 0;
  margin-right: 0;
}
/* ── Stylised post-it shading on BOTH sticky notes (the feed doodle + the composer) ────────────────
   One pointer-events:none overlay above the (opaque) canvas, clipped to the note's rounded corners.
   Pure-CSS gradients in a Noto-emoji vein — no photo texture: a soft top-left highlight fading into a
   gentle bottom-right shade for volume, plus a faint centre-out edge vignette for paper depth. Plain
   rgba (no blend mode) so it reads the same over any profile-coloured fill. */
.nuke-container .nuke-post-doodle::before,
.selfpost-sticky::before {
  content: "";
  position: absolute;
  /* Reach the note's OUTER edge (over the 2px border) so the shading fills the rounded corners — an
     inset:0 overlay sits inside the border and its corner rounds too tight, leaving a corner gap. */
  inset: 0;
  z-index: 1;
  pointer-events: none;
  border-radius: inherit;
  background:
    linear-gradient(145deg, rgba(255, 255, 255, 0.16) 0%, rgba(255, 255, 255, 0.04) 24%, rgba(255, 255, 255, 0) 50%),
    linear-gradient(145deg, rgba(0, 0, 0, 0) 58%, rgba(0, 0, 0, 0.03) 100%),
    radial-gradient(135% 135% at 50% 45%, rgba(0, 0, 0, 0) 76%, rgba(0, 0, 0, 0.015));
}
/* Both the in-chat doodle AND the composer note drop the post-it shading overlay (flat, transparent). */
.nuke-container .nuke-post-doodle::before,
.selfpost-sticky::before { display: none; }
/* The composer's Clear button stays above the paper overlays. */
.selfpost-sticky-clear { z-index: 3; }
.selfpost-sticky-canvas {
  display: block;
  width: 100%;
  aspect-ratio: 16 / 9; /* wide landscape — composer + feed MUST match so drawings dont distort */
  touch-action: none;
  /* The pen cursor from the missions notebook (.nuke-notebook-canvas) — same asset + hotspot. */
  cursor: url('/img/cursors/pencil.png') 5 18, crosshair !important;
}
.selfpost-sticky-clear {
  /* Square icon button, matching the brush tool buttons (.selfpost-tool). */
  width: 32px;
  height: 30px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  padding: 0;
  border: 1px solid rgba(0, 0, 0, 0.18);
  background: rgba(255, 255, 255, 0.72);
  color: #3a3320;
  border-radius: 7px;
  font-size: calc(16px - var(--font-shrink, 0px));
  cursor: pointer;
}
/* ── Doodle composer tool palette: Pencil (sizes) · Brush (dither patterns) · Eraser ──
   Sits on the post-it paper above the canvas; the size row and pattern row swap in by tool. */
.selfpost-canvas-wrap { position: relative; } /* pins the Clear button to the canvas, tools sit below */
.selfpost-tools {
  display: flex;
  flex-direction: column;
  gap: 6px;
  margin-top: 6px; /* palette is BELOW the doodle now */
}
.selfpost-tool-row,
.selfpost-brush-opts {
  display: flex;
  gap: 6px;
  flex-wrap: wrap;
  align-items: center;
}
/* The inactive row is toggled off via [hidden]; it must beat the flex display above (the UA
   `[hidden]{display:none}` rule loses to a class selector on its own). */
.selfpost-brush-opts[hidden] { display: none; }
.selfpost-tool,
.selfpost-pattern {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  padding: 0;
  border: 1px solid rgba(0, 0, 0, 0.18);
  background: rgba(255, 255, 255, 0.72);
  color: #3a3320; /* dark ink on the paper */
  border-radius: 7px;
  cursor: pointer;
  transition: background 0.1s ease, border-color 0.1s ease;
}
.selfpost-tool { width: 32px; height: 30px; font-size: calc(16px - var(--font-shrink, 0px)); }
.selfpost-pattern { width: 30px; height: 30px; }
.selfpost-tool:hover,
.selfpost-pattern:hover { background: rgba(255, 255, 255, 0.96); }
.selfpost-tool.is-active,
.selfpost-pattern.is-active {
  background: #3a3320;
  border-color: #3a3320;
  color: #ffffff; /* light icon on the dark active chip (was post-it yellow) */
}
.selfpost-pattern { overflow: hidden; }
.selfpost-pattern-sw {
  display: block;
  width: 22px;
  height: 22px;
  border-radius: 3px;
  pointer-events: none;
}
/* Brush Size slider (per-tool range) — label chip + a slim range track in the paper's ink colour. */
.selfpost-size-row { gap: 8px; }
.selfpost-size-label {
  display: inline-flex;
  align-items: center;
  color: #3a3320;
  font-size: calc(15px - var(--font-shrink, 0px));
}
.selfpost-size-slider {
  flex: 1;
  min-width: 90px;
  height: 4px;
  -webkit-appearance: none;
  appearance: none;
  background: rgba(58, 51, 32, 0.25);
  border-radius: 999px;
  cursor: pointer;
}
.selfpost-size-slider::-webkit-slider-thumb {
  -webkit-appearance: none;
  appearance: none;
  width: 14px;
  height: 14px;
  border-radius: 50%;
  background: #3a3320;
  border: 2px solid #ffffff;
  cursor: pointer;
}
.selfpost-size-slider::-moz-range-thumb {
  width: 14px;
  height: 14px;
  border-radius: 50%;
  background: #3a3320;
  border: 2px solid #ffffff;
  cursor: pointer;
}
/* Received-posts list on a profile page (the cred votes targeting the user). Layout is
   intentionally minimal — the cred tint above carries the up/down read; the verb +
   voter name + message are the data. */
/* Inset the profile's post/media/likes lists 10px each side so their .nuke-post cards line up with the
   post page + feed cards (which sit in the 10px-padded #nuke-post-modal-body / #nuke-chat-feed); the
   profile page itself renders in the 0-padding #nuke-chat-scroll, so without this its cards ran full-width. */
.nuke-container .nuke-profile-page-posts { padding: 0; }   /* no gap before the posts */
.nuke-container .nuke-profile-page-posts-title {
  font-weight: 600;
  font-size: calc(0.95rem - var(--font-shrink, 0px));
  margin-bottom: 8px;
}
.nuke-container .nuke-profile-post {
  display: flex;
  gap: 10px;
  align-items: flex-start;
  padding: 8px 0;
  border-top: 1px solid var(--color-nuke-border, rgba(var(--nk-surface-rgb), 0.08));
}
.nuke-container .nuke-profile-post-pfp { width: 36px; height: 36px; flex: 0 0 36px; }
.nuke-container .nuke-profile-post-pfp .cc-pfp { width: 36px; height: 36px; }
.nuke-container .nuke-profile-post-body { min-width: 0; flex: 1; }
.nuke-container .nuke-profile-post-head { font-size: calc(0.85rem - var(--font-shrink, 0px)); }
.nuke-container .nuke-profile-post-voter { font-weight: 600; }
.nuke-container .nuke-profile-post-msg {
  font-size: calc(0.9rem - var(--font-shrink, 0px));
  word-break: break-word;
  margin-top: 2px;
}
/* ── /home pfps: flat rounded image + 1px border ─────────────────────
   Strips the shared app-icon treatment (gloss, rim, drop shadow, hover
   tilt) from every avatar pfp on /home. Scoped to .nuke-container so the
   prod design in styles.css is untouched; the 1px ring + rounded corners
   (--pfp-radius, set per variant below) are kept. */
.nuke-container .cc-pfp-wrapper::before,
.nuke-container .cc-pfp-wrapper::after {
  display: none;
}
.nuke-container .cc-pfp {
  /* Borderless — no outer ring (was a 1px box-shadow ring via
     `--pfp-outer-border-*`); just the flat rounded image. */
  box-shadow: none;
}

.nuke-container .nuke-post-pfp {
  /* Player-list pfp chrome (gloss/rim via .cc-pfp-wrapper). */
  width: var(--nuke-chat-pfp-size);
  height: var(--nuke-chat-pfp-size);
  margin: 0;
}
.nuke-container .nuke-post-pfp .cc-pfp { width: 100%; height: 100%; }
/* Persistent avatar border — a REAL border on the .cc-pfp IMAGE, not a box-shadow "ring" on the wrapper.
   A box-shadow ON the <img> is swallowed by its overflow:clip (why this used to be a wrapper ring), but a
   real BORDER renders fine. The hover/popup rules below are lift-ONLY — the border lives on the image, so
   it stays a constant 1px through the hover scale. */
.nuke-container .nuke-post-pfp,
.nuke-container .nuke-post-quote-pfp { --pfp-outer-border-width: 1px; }
/* Default colour = the message-chain connector / reply-box border colour (--nk-border). */
.nuke-container .nuke-post-pfp .cc-pfp,
.nuke-container .nuke-post-quote-pfp .cc-pfp {
  box-sizing: border-box;
  border: var(--pfp-outer-border-width, 1px) solid var(--nk-border);
}
/* The message AUTHOR avatar matches its card edge instead (--card-border, same value as the card border). */
.nuke-container .nuke-post .nuke-post-pfp .cc-pfp {
  border-color: var(--card-border, var(--nk-border));
}
/* The REPLY avatar likewise matches ITS reply container's edge — --card-border is redefined on
   .nuke-post-quote from the quoted user's tint, so it resolves to the reply box's own border colour. */
.nuke-container .nuke-post-quote-pfp .cc-pfp {
  border-color: var(--card-border, var(--nk-border));
}
/* The chat-header profile card's avatar matches ITS card edge too — --card-border is defined on
   .nuke-chat-header-2 (it is NOT inside a .nuke-post, so the message-author rule above misses it). */
.nuke-container .nuke-chat-header-2 .nuke-post-pfp .cc-pfp {
  border-color: var(--card-border, var(--nk-border));
}
/* The big PROFILE-PAGE avatar ring = the SAME colour as the pfp borders in this user's post containers. */
.nuke-container .nuke-profile-page-pfp .cc-pfp {
  /* Post pfp borders read from --card-border = SETTING-3 of the author's hue; on the profile page that's
     --nk-border (SETTING-3 of --current-user-color). Colourless profile → --nk-border-plain fallback. */
  border-color: var(--nk-border);
  --pfp-outer-border-color: var(--nk-border);
}
.nuke-container .nuke-hovercard:not(.nuke-profile-card) .nuke-hovercard-pfp .cc-pfp {
  /* Match the POST containers' pfp border: the author's SETTING-3 tint. On the popup that's --hover-edge
     (--nuke-cmd-color @ SETTING 3, colourless → --nk-border-plain) — the same recipe posts use for --card-border.
     Full border shorthand (width = --pfp-outer-border-width, 2px on the wrapper) so it actually renders. */
  border: var(--pfp-outer-border-width, 1px) solid var(--hover-edge);
}
/* .nuke-post-pfp dot: size (--dot-size) + position come from the base .status-dot rule. */

/* Chat message-author pfp hover — the same subtle down-right lift as a reply hover (--card-lift). Rests
   at a transparent shadow so it FADES in via the .nuke-post-pfp box-shadow transition. Placed BEFORE the
   hovercard-active rule below so that, on equal specificity, the popup's larger lift wins once it opens. */
.nuke-container .nuke-post .nuke-post-pfp { box-shadow: 0 0 0 0 transparent; }
.nuke-container .nuke-post .nuke-post-pfp:hover { box-shadow: 2px 2px 0 0 rgba(0, 0, 0, 0.2); }

/* Prod calling-card pfp animation, brought to ALL nuke pfps: a snappier tilt while pressed and a
   spring-back shake on release. (The subtle tilt + shadow ON HOVER was removed site-wide — only the
   chat message/post author-pfp hover ring below is kept; see that rule.) Reuses the global `:root`
   `--pfp-*` vars + styles.css's `cc-pfp-shake` keyframe; `.cc-pressed` / `.cc-rebound` are toggled by
   the delegated press handler in index.ts (`initNukePfpPress`). */
/* The wrapper carries ALL pfp motion — rotate (--cc-tilt) + scale (--cc-press) — so the image and the status
   dot move together as one rigid unit and can never desync. Both are registered @property vars, so they ease
   via this transition (and the shake keyframes below animate --cc-tilt the same way). */
.nuke-container .cc-pfp-wrapper {
  /* ONLY a rotate (tilt) — no scale. The whole wrapper (image + dot) rotates as one rigid unit, so they can
     never desync; and with no scale the pfp never shrinks on press. --cc-tilt is a registered @property
     <angle>, so it eases via this transition. */
  transform: rotate(var(--cc-tilt, 0deg));
  transition: --cc-tilt 0.18s cubic-bezier(0.22, 1, 0.36, 1);
}
.nuke-container .cc-pfp-wrapper.cc-pressed {
  --cc-press: 0.95;
  --cc-tilt: var(--pfp-pressed-tilt);
}
.nuke-container .cc-pfp-wrapper.cc-rebound {
  animation: cc-rebound 0.42s cubic-bezier(0.45, 0.05, 0.55, 0.95);
}
/* EXCEPTIONS = the WIGGLE: NPC chat pfps and the header pfp on ANOTHER user's
   profile page only ("other"). Shake the WRAPPER via --cc-tilt so the dot wiggles
   with the image; overrides the scale-pop rebound above. */
.nuke-container .nuke-post[data-has-account="false"] .cc-pfp-wrapper.cc-rebound,
.nuke-container[data-profile-view="other"] .nuke-chat-header-2 .cc-pfp-wrapper.cc-rebound {
  animation: nuke-cc-pfp-shake 0.36s ease-in-out;
}
@keyframes nuke-cc-pfp-shake {
  0%   { --cc-tilt: 0deg; }
  18%  { --cc-tilt: -5deg; }
  36%  { --cc-tilt: 4deg; }
  54%  { --cc-tilt: -3deg; }
  72%  { --cc-tilt: 2deg; }
  88%  { --cc-tilt: -1deg; }
  100% { --cc-tilt: 0deg; }
}
/* (The status dot no longer needs its own rotation rules — it rides the wrapper's
   --cc-tilt rotation along with the image, so they can't desync.) */
.nuke-container .nuke-post-main {
  flex: 1 1 auto;
  min-width: 0;
  /* Virtualize the HEAVY content (name, message text + animated emoji, images): the browser SKIPS
     rendering it while off-screen, so the feed stays light no matter how deep you scroll (off-screen
     posts cost ~nothing to paint/composite) — WITHOUT removing DOM, so scroll position + jump-to-
     message keep working. `contain-intrinsic-size: auto` remembers each block's real height so the
     scrollbar stays accurate. Scoped to the message column (not the row/post) so the pfp's status
     dot and the confetti — which overflow their boxes — are never paint-clipped. */
  content-visibility: auto;
  contain-intrinsic-size: auto 40px;
  /* Paint containment (implied by content-visibility) clips overflow to the column box — which was
     cropping the reply box's hover LIFT (scale(1.006) + a `0 3px 9px` drop shadow, ~12px reach) and
     any other in-column hover overflow. Extend the paint-clip edge so that lift shows in full, while
     keeping the off-screen virtualization. (Verified: Chrome honors overflow-clip-margin on a
     content-visibility element.) Also clears the reply arrow, which overhangs the column's LEFT
     edge by 24px (`.nuke-post-reply-arrow { left: -24px }`) — 16px was cropping its left tip. */
  overflow-clip-margin: 26px;
  /* Center the username + first text line against the avatar for short messages:
     a min-height equal to the pfp (40px) + vertical centering. Taller (multi-line)
     messages exceed 40px, so there's no free space to distribute and the content
     naturally top-aligns — the pfp lines up with the first lines, not the middle. */
  display: flex;
  flex-direction: column;
  justify-content: center;
  /* Track the avatar size (was a hardcoded 40px): the column centres its name+copy against the pfp, so on
     phones where the pfp shrinks to 36px the centring box shrinks with it — no oversized gap under the name. */
  min-height: var(--nuke-chat-pfp-size);
}
.nuke-container .nuke-post-head {
  display: flex;
  align-items: baseline;
  /* nowrap: the balance must never wrap onto a second line and break the card — the
     username row degrades by truncation instead (see the one-line protection below). */
  flex-wrap: nowrap;
  gap: 4px;
  line-height: 1.25;
  /* Small 2px breather so the body text isn't cramped right up against the name/handle row. */
  margin-bottom: 2px;
}
.nuke-container .nuke-post-name-cluster {
  display: inline-flex;
  align-items: center;
  /* Tight gap so the level badge / @handle sit RIGHT after the name like a
     verified checkmark — 4px read as a full word-space after every username
     (the "odd space" after the name). */
  gap: 3px;
  min-width: 0;
  /* Shrink as a whole so the balance keeps its line; children degrade in order below. */
  flex: 0 1 auto;
}
/* ── Header one-line protection (all posts) ──────────────────────────────────
   Keep the username row + balance on a SINGLE line so the balance never wraps down and
   breaks the card. As the row tightens the sacrifice order is: FIRST the date collapses
   (highest shrink weight — but it shows in FULL whenever there's room), THEN the @handle
   truncates (large shrink weight), then — only if it STILL overflows — the username
   truncates (tiny shrink weight). Flag + level badge never shrink. */
/* Same sacrifice order for the post header AND the reply-quote header (its twins reuse .nuke-post-name-
   cluster): date collapses first → @handle truncates → username truncates. */
.nuke-container .nuke-post-head .nuke-post-time,
.nuke-container .nuke-post-head .nuke-post-dot,
.nuke-container .nuke-post-quote-head .nuke-post-quote-time,
.nuke-container .nuke-post-quote-head .nuke-post-quote-dot {
  min-width: 0;
  flex: 0 1000 auto;   /* highest shrink weight → yields first; natural (full) size when there's room */
  overflow: hidden;
  white-space: nowrap;
}
.nuke-container .nuke-post-name-cluster .nuke-flag,
.nuke-container .nuke-post-name-cluster .nuke-post-name-badge { flex-shrink: 0; }
.nuke-container .nuke-post-name,
.nuke-container .nuke-post-quote-name {
  min-width: 0;
  flex: 0 1 auto;   /* last-resort shrink: only after the handle is exhausted */
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.nuke-container .nuke-post-name-cluster .nuke-post-handle,
.nuke-container .nuke-post-name-cluster .nuke-post-quote-handle {
  min-width: 0;
  flex: 0 100 auto;   /* huge shrink weight → the handle truncates FIRST */
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
/* COLOR SETTING 6 (heading) → usernames are the author's own hue at setting 6; colourless → --nk-text. */
.nuke-container .nuke-post-name { font-weight: 700; --un-6: oklch(from var(--nuke-cmd-color) var(--set-6-l) var(--set-6-c) h); color: var(--un-6, var(--nk-text)); }
/* Country flag emoji after a username (feed · replies · hovercard · header · profile page). It's a
   flex child of each name row, so spacing comes from the row's own `gap`; it only needs to never
   shrink + render as a COLOUR flag (the emoji font stack) instead of the two Regional-Indicator
   letters. Slightly under 1em so it sits level with the name rather than towering over it. */
/* ══ 4chan country-flag sprite (geo.png) — replaces the emoji flag. One image for all
   flags; per-country background-position below (verbatim from 4chan's flags CSS). The
   render sites emit <span class="nuke-flag" data-cc="us">; unknown/undetected → data-cc="xx"
   (4chan's "unknown" flag), which the base rule also defaults to. ══ */
.nuke-container .nuke-flag {
  flex-shrink: 0;
  display: inline-block;
  width: 16px;
  height: 11px;
  background: url('/img/flags/geo.png') no-repeat;
  background-position: -48px -176px; /* default = an empty (transparent) sprite cell → no flag, never the "?" */
  /* The flag is a fixed 11px-tall bitmap. Flex-centering it in a text row whose line box isn't a
     whole pixel (feed = 15px × 1.25 = 18.75px) put its top on a fractional pixel that rounded up or
     down per row — the ±1px wobble. `align-self:center` is the fallback; the @supports block below
     replaces it with a whole-pixel-snapped offset so the flag sits at the SAME integer position in
     every row and context. */
  align-self: center;
}
/* Snap the flag's vertical centering to a whole pixel. `1lh` = the row's own line height, so the
   offset self-adjusts to each context (feed / hovercard / header / profile) and `round(…, 1px)`
   forces it onto the pixel grid — no more fractional rounding, no more wobble. */
@supports (margin-top: round(1px, 1px)) {
  .nuke-container .nuke-flag {
    align-self: flex-start;
    margin-top: round((1lh - 11px) / 2, 1px);
  }
}
.nuke-container .nuke-flag[data-cc="ad"] { background-position: 0 0; }
.nuke-container .nuke-flag[data-cc="ae"] { background-position: -16px 0; }
.nuke-container .nuke-flag[data-cc="af"] { background-position: -32px 0; }
.nuke-container .nuke-flag[data-cc="ag"] { background-position: -48px 0; }
.nuke-container .nuke-flag[data-cc="ai"] { background-position: -64px 0; }
.nuke-container .nuke-flag[data-cc="al"] { background-position: -80px 0; }
.nuke-container .nuke-flag[data-cc="am"] { background-position: -96px 0; }
.nuke-container .nuke-flag[data-cc="an"] { background-position: -112px 0; }
.nuke-container .nuke-flag[data-cc="ao"] { background-position: -128px 0; }
.nuke-container .nuke-flag[data-cc="aq"] { background-position: -144px 0; }
.nuke-container .nuke-flag[data-cc="ar"] { background-position: -160px 0; }
.nuke-container .nuke-flag[data-cc="as"] { background-position: -176px 0; }
.nuke-container .nuke-flag[data-cc="at"] { background-position: -192px 0; }
.nuke-container .nuke-flag[data-cc="au"] { background-position: -208px 0; }
.nuke-container .nuke-flag[data-cc="aw"] { background-position: -224px 0; }
.nuke-container .nuke-flag[data-cc="ax"] { background-position: -240px 0; }
.nuke-container .nuke-flag[data-cc="az"] { background-position: 0 -11px; }
.nuke-container .nuke-flag[data-cc="ba"] { background-position: -16px -11px; }
.nuke-container .nuke-flag[data-cc="bb"] { background-position: -32px -11px; }
.nuke-container .nuke-flag[data-cc="bd"] { background-position: -48px -11px; }
.nuke-container .nuke-flag[data-cc="be"] { background-position: -64px -11px; }
.nuke-container .nuke-flag[data-cc="bf"] { background-position: -80px -11px; }
.nuke-container .nuke-flag[data-cc="bg"] { background-position: -96px -11px; }
.nuke-container .nuke-flag[data-cc="bh"] { background-position: -112px -11px; }
.nuke-container .nuke-flag[data-cc="bi"] { background-position: -128px -11px; }
.nuke-container .nuke-flag[data-cc="bj"] { background-position: -144px -11px; }
.nuke-container .nuke-flag[data-cc="bl"] { background-position: -160px -11px; }
.nuke-container .nuke-flag[data-cc="bm"] { background-position: -176px -11px; }
.nuke-container .nuke-flag[data-cc="bn"] { background-position: -192px -11px; }
.nuke-container .nuke-flag[data-cc="bo"] { background-position: -208px -11px; }
.nuke-container .nuke-flag[data-cc="bq"] { background-position: -224px -11px; }
.nuke-container .nuke-flag[data-cc="br"] { background-position: -240px -11px; }
.nuke-container .nuke-flag[data-cc="bs"] { background-position: 0 -22px; }
.nuke-container .nuke-flag[data-cc="bt"] { background-position: -16px -22px; }
.nuke-container .nuke-flag[data-cc="bv"] { background-position: -32px -22px; }
.nuke-container .nuke-flag[data-cc="bw"] { background-position: -48px -22px; }
.nuke-container .nuke-flag[data-cc="by"] { background-position: -64px -22px; }
.nuke-container .nuke-flag[data-cc="bz"] { background-position: -80px -22px; }
.nuke-container .nuke-flag[data-cc="ca"] { background-position: -96px -22px; }
.nuke-container .nuke-flag[data-cc="catalonia"] { background-position: -112px -22px; }
.nuke-container .nuke-flag[data-cc="cc"] { background-position: -128px -22px; }
.nuke-container .nuke-flag[data-cc="cd"] { background-position: -144px -22px; }
.nuke-container .nuke-flag[data-cc="cf"] { background-position: -160px -22px; }
.nuke-container .nuke-flag[data-cc="cg"] { background-position: -176px -22px; }
.nuke-container .nuke-flag[data-cc="ch"] { background-position: -192px -22px; }
.nuke-container .nuke-flag[data-cc="ci"] { background-position: -208px -22px; }
.nuke-container .nuke-flag[data-cc="ck"] { background-position: -224px -22px; }
.nuke-container .nuke-flag[data-cc="cl"] { background-position: -240px -22px; }
.nuke-container .nuke-flag[data-cc="cm"] { background-position: 0 -33px; }
.nuke-container .nuke-flag[data-cc="cn"] { background-position: -16px -33px; }
.nuke-container .nuke-flag[data-cc="co"] { background-position: -32px -33px; }
.nuke-container .nuke-flag[data-cc="cr"] { background-position: -48px -33px; }
.nuke-container .nuke-flag[data-cc="cs"] { background-position: -64px -33px; }
.nuke-container .nuke-flag[data-cc="cu"] { background-position: -80px -33px; }
.nuke-container .nuke-flag[data-cc="cv"] { background-position: -96px -33px; }
.nuke-container .nuke-flag[data-cc="cw"] { background-position: -112px -33px; }
.nuke-container .nuke-flag[data-cc="cx"] { background-position: -128px -33px; }
.nuke-container .nuke-flag[data-cc="cy"] { background-position: -144px -33px; }
.nuke-container .nuke-flag[data-cc="cz"] { background-position: -160px -33px; }
.nuke-container .nuke-flag[data-cc="de"] { background-position: -176px -33px; }
.nuke-container .nuke-flag[data-cc="dj"] { background-position: -192px -33px; }
.nuke-container .nuke-flag[data-cc="dk"] { background-position: -208px -33px; }
.nuke-container .nuke-flag[data-cc="dm"] { background-position: -224px -33px; }
.nuke-container .nuke-flag[data-cc="do"] { background-position: -240px -33px; }
.nuke-container .nuke-flag[data-cc="dz"] { background-position: 0 -44px; }
.nuke-container .nuke-flag[data-cc="ec"] { background-position: -16px -44px; }
.nuke-container .nuke-flag[data-cc="ee"] { background-position: -32px -44px; }
.nuke-container .nuke-flag[data-cc="eg"] { background-position: -48px -44px; }
.nuke-container .nuke-flag[data-cc="eh"] { background-position: -64px -44px; }
.nuke-container .nuke-flag[data-cc="xe"] { background-position: -80px -44px; }
.nuke-container .nuke-flag[data-cc="er"] { background-position: -96px -44px; }
.nuke-container .nuke-flag[data-cc="es"] { background-position: -112px -44px; }
.nuke-container .nuke-flag[data-cc="et"] { background-position: -128px -44px; }
.nuke-container .nuke-flag[data-cc="eu"] { background-position: -144px -44px; }
.nuke-container .nuke-flag[data-cc="fam"] { background-position: -160px -44px; }
.nuke-container .nuke-flag[data-cc="fi"] { background-position: -176px -44px; }
.nuke-container .nuke-flag[data-cc="fj"] { background-position: -192px -44px; }
.nuke-container .nuke-flag[data-cc="fk"] { background-position: -208px -44px; }
.nuke-container .nuke-flag[data-cc="fm"] { background-position: -224px -44px; }
.nuke-container .nuke-flag[data-cc="fo"] { background-position: -240px -44px; }
.nuke-container .nuke-flag[data-cc="fr"] { background-position: 0 -55px; }
.nuke-container .nuke-flag[data-cc="ga"] { background-position: -16px -55px; }
.nuke-container .nuke-flag[data-cc="gb"] { background-position: -32px -55px; }
.nuke-container .nuke-flag[data-cc="gd"] { background-position: -48px -55px; }
.nuke-container .nuke-flag[data-cc="ge"] { background-position: -64px -55px; }
.nuke-container .nuke-flag[data-cc="gf"] { background-position: -80px -55px; }
.nuke-container .nuke-flag[data-cc="gg"] { background-position: -96px -55px; }
.nuke-container .nuke-flag[data-cc="gh"] { background-position: -112px -55px; }
.nuke-container .nuke-flag[data-cc="gi"] { background-position: -128px -55px; }
.nuke-container .nuke-flag[data-cc="gl"] { background-position: -144px -55px; }
.nuke-container .nuke-flag[data-cc="gm"] { background-position: -160px -55px; }
.nuke-container .nuke-flag[data-cc="gn"] { background-position: -176px -55px; }
.nuke-container .nuke-flag[data-cc="gp"] { background-position: -192px -55px; }
.nuke-container .nuke-flag[data-cc="gq"] { background-position: -208px -55px; }
.nuke-container .nuke-flag[data-cc="gr"] { background-position: -224px -55px; }
.nuke-container .nuke-flag[data-cc="gs"] { background-position: -240px -55px; }
.nuke-container .nuke-flag[data-cc="gt"] { background-position: 0 -66px; }
.nuke-container .nuke-flag[data-cc="gu"] { background-position: -16px -66px; }
.nuke-container .nuke-flag[data-cc="gw"] { background-position: -32px -66px; }
.nuke-container .nuke-flag[data-cc="gy"] { background-position: -48px -66px; }
.nuke-container .nuke-flag[data-cc="hk"] { background-position: -64px -66px; }
.nuke-container .nuke-flag[data-cc="hm"] { background-position: -80px -66px; }
.nuke-container .nuke-flag[data-cc="hn"] { background-position: -96px -66px; }
.nuke-container .nuke-flag[data-cc="hr"] { background-position: -112px -66px; }
.nuke-container .nuke-flag[data-cc="ht"] { background-position: -128px -66px; }
.nuke-container .nuke-flag[data-cc="hu"] { background-position: -144px -66px; }
.nuke-container .nuke-flag[data-cc="id"] { background-position: -160px -66px; }
.nuke-container .nuke-flag[data-cc="ie"] { background-position: -176px -66px; }
.nuke-container .nuke-flag[data-cc="il"] { background-position: -192px -66px; }
.nuke-container .nuke-flag[data-cc="im"] { background-position: -208px -66px; }
.nuke-container .nuke-flag[data-cc="in"] { background-position: -224px -66px; }
.nuke-container .nuke-flag[data-cc="io"] { background-position: -240px -66px; }
.nuke-container .nuke-flag[data-cc="iq"] { background-position: 0 -77px; }
.nuke-container .nuke-flag[data-cc="ir"] { background-position: -16px -77px; }
.nuke-container .nuke-flag[data-cc="is"] { background-position: -32px -77px; }
.nuke-container .nuke-flag[data-cc="it"] { background-position: -48px -77px; }
.nuke-container .nuke-flag[data-cc="je"] { background-position: -64px -77px; }
.nuke-container .nuke-flag[data-cc="jm"] { background-position: -80px -77px; }
.nuke-container .nuke-flag[data-cc="jo"] { background-position: -96px -77px; }
.nuke-container .nuke-flag[data-cc="jp"] { background-position: -112px -77px; }
.nuke-container .nuke-flag[data-cc="ke"] { background-position: -128px -77px; }
.nuke-container .nuke-flag[data-cc="kg"] { background-position: -144px -77px; }
.nuke-container .nuke-flag[data-cc="kh"] { background-position: -160px -77px; }
.nuke-container .nuke-flag[data-cc="ki"] { background-position: -176px -77px; }
.nuke-container .nuke-flag[data-cc="km"] { background-position: -192px -77px; }
.nuke-container .nuke-flag[data-cc="kn"] { background-position: -208px -77px; }
.nuke-container .nuke-flag[data-cc="kp"] { background-position: -224px -77px; }
.nuke-container .nuke-flag[data-cc="kr"] { background-position: -240px -77px; }
.nuke-container .nuke-flag[data-cc="kw"] { background-position: 0 -88px; }
.nuke-container .nuke-flag[data-cc="ky"] { background-position: -16px -88px; }
.nuke-container .nuke-flag[data-cc="kz"] { background-position: -32px -88px; }
.nuke-container .nuke-flag[data-cc="la"] { background-position: -48px -88px; }
.nuke-container .nuke-flag[data-cc="lb"] { background-position: -64px -88px; }
.nuke-container .nuke-flag[data-cc="lc"] { background-position: -80px -88px; }
.nuke-container .nuke-flag[data-cc="li"] { background-position: -96px -88px; }
.nuke-container .nuke-flag[data-cc="lk"] { background-position: -112px -88px; }
.nuke-container .nuke-flag[data-cc="lr"] { background-position: -128px -88px; }
.nuke-container .nuke-flag[data-cc="ls"] { background-position: -144px -88px; }
.nuke-container .nuke-flag[data-cc="lt"] { background-position: -160px -88px; }
.nuke-container .nuke-flag[data-cc="lu"] { background-position: -176px -88px; }
.nuke-container .nuke-flag[data-cc="lv"] { background-position: -192px -88px; }
.nuke-container .nuke-flag[data-cc="ly"] { background-position: -208px -88px; }
.nuke-container .nuke-flag[data-cc="ma"] { background-position: -224px -88px; }
.nuke-container .nuke-flag[data-cc="mc"] { background-position: -240px -88px; }
.nuke-container .nuke-flag[data-cc="md"] { background-position: 0 -99px; }
.nuke-container .nuke-flag[data-cc="me"] { background-position: -16px -99px; }
.nuke-container .nuke-flag[data-cc="mf"] { background-position: -32px -99px; }
.nuke-container .nuke-flag[data-cc="mg"] { background-position: -48px -99px; }
.nuke-container .nuke-flag[data-cc="mh"] { background-position: -64px -99px; }
.nuke-container .nuke-flag[data-cc="mk"] { background-position: -80px -99px; }
.nuke-container .nuke-flag[data-cc="ml"] { background-position: -96px -99px; }
.nuke-container .nuke-flag[data-cc="mm"] { background-position: -112px -99px; }
.nuke-container .nuke-flag[data-cc="mn"] { background-position: -128px -99px; }
.nuke-container .nuke-flag[data-cc="mo"] { background-position: -144px -99px; }
.nuke-container .nuke-flag[data-cc="mp"] { background-position: -160px -99px; }
.nuke-container .nuke-flag[data-cc="mq"] { background-position: -176px -99px; }
.nuke-container .nuke-flag[data-cc="mr"] { background-position: -192px -99px; }
.nuke-container .nuke-flag[data-cc="ms"] { background-position: -208px -99px; }
.nuke-container .nuke-flag[data-cc="mt"] { background-position: -224px -99px; }
.nuke-container .nuke-flag[data-cc="mu"] { background-position: -240px -99px; }
.nuke-container .nuke-flag[data-cc="mv"] { background-position: 0 -110px; }
.nuke-container .nuke-flag[data-cc="mw"] { background-position: -16px -110px; }
.nuke-container .nuke-flag[data-cc="mx"] { background-position: -32px -110px; }
.nuke-container .nuke-flag[data-cc="my"] { background-position: -48px -110px; }
.nuke-container .nuke-flag[data-cc="mz"] { background-position: -64px -110px; }
.nuke-container .nuke-flag[data-cc="na"] { background-position: -80px -110px; }
.nuke-container .nuke-flag[data-cc="nc"] { background-position: -96px -110px; }
.nuke-container .nuke-flag[data-cc="ne"] { background-position: -112px -110px; }
.nuke-container .nuke-flag[data-cc="nf"] { background-position: -128px -110px; }
.nuke-container .nuke-flag[data-cc="ng"] { background-position: -144px -110px; }
.nuke-container .nuke-flag[data-cc="ni"] { background-position: -160px -110px; }
.nuke-container .nuke-flag[data-cc="nl"] { background-position: -176px -110px; }
.nuke-container .nuke-flag[data-cc="no"] { background-position: -192px -110px; }
.nuke-container .nuke-flag[data-cc="np"] { background-position: -208px -110px; }
.nuke-container .nuke-flag[data-cc="nr"] { background-position: -224px -110px; }
.nuke-container .nuke-flag[data-cc="nu"] { background-position: -240px -110px; }
.nuke-container .nuke-flag[data-cc="nz"] { background-position: 0 -121px; }
.nuke-container .nuke-flag[data-cc="om"] { background-position: -16px -121px; }
.nuke-container .nuke-flag[data-cc="pa"] { background-position: -32px -121px; }
.nuke-container .nuke-flag[data-cc="pe"] { background-position: -48px -121px; }
.nuke-container .nuke-flag[data-cc="pf"] { background-position: -64px -121px; }
.nuke-container .nuke-flag[data-cc="pg"] { background-position: -80px -121px; }
.nuke-container .nuke-flag[data-cc="ph"] { background-position: -96px -121px; }
.nuke-container .nuke-flag[data-cc="pk"] { background-position: -112px -121px; }
.nuke-container .nuke-flag[data-cc="pl"] { background-position: -128px -121px; }
.nuke-container .nuke-flag[data-cc="pm"] { background-position: -144px -121px; }
.nuke-container .nuke-flag[data-cc="pn"] { background-position: -160px -121px; }
.nuke-container .nuke-flag[data-cc="pr"] { background-position: -176px -121px; }
.nuke-container .nuke-flag[data-cc="ps"] { background-position: -192px -121px; }
.nuke-container .nuke-flag[data-cc="pt"] { background-position: -208px -121px; }
.nuke-container .nuke-flag[data-cc="pw"] { background-position: -224px -121px; }
.nuke-container .nuke-flag[data-cc="py"] { background-position: -240px -121px; }
.nuke-container .nuke-flag[data-cc="qa"] { background-position: 0 -132px; }
.nuke-container .nuke-flag[data-cc="re"] { background-position: -16px -132px; }
.nuke-container .nuke-flag[data-cc="ro"] { background-position: -32px -132px; }
.nuke-container .nuke-flag[data-cc="rs"] { background-position: -48px -132px; }
.nuke-container .nuke-flag[data-cc="ru"] { background-position: -64px -132px; }
.nuke-container .nuke-flag[data-cc="rw"] { background-position: -80px -132px; }
.nuke-container .nuke-flag[data-cc="sa"] { background-position: -96px -132px; }
.nuke-container .nuke-flag[data-cc="sb"] { background-position: -112px -132px; }
.nuke-container .nuke-flag[data-cc="sc"] { background-position: -128px -132px; }
.nuke-container .nuke-flag[data-cc="xs"] { background-position: -144px -132px; }
.nuke-container .nuke-flag[data-cc="sd"] { background-position: -160px -132px; }
.nuke-container .nuke-flag[data-cc="se"] { background-position: -176px -132px; }
.nuke-container .nuke-flag[data-cc="sg"] { background-position: -192px -132px; }
.nuke-container .nuke-flag[data-cc="sh"] { background-position: -208px -132px; }
.nuke-container .nuke-flag[data-cc="si"] { background-position: -224px -132px; }
.nuke-container .nuke-flag[data-cc="sj"] { background-position: -240px -132px; }
.nuke-container .nuke-flag[data-cc="sk"] { background-position: 0 -143px; }
.nuke-container .nuke-flag[data-cc="sl"] { background-position: -16px -143px; }
.nuke-container .nuke-flag[data-cc="sm"] { background-position: -32px -143px; }
.nuke-container .nuke-flag[data-cc="sn"] { background-position: -48px -143px; }
.nuke-container .nuke-flag[data-cc="so"] { background-position: -64px -143px; }
.nuke-container .nuke-flag[data-cc="sr"] { background-position: -80px -143px; }
.nuke-container .nuke-flag[data-cc="ss"] { background-position: -96px -143px; }
.nuke-container .nuke-flag[data-cc="st"] { background-position: -112px -143px; }
.nuke-container .nuke-flag[data-cc="sv"] { background-position: -128px -143px; }
.nuke-container .nuke-flag[data-cc="sx"] { background-position: -144px -143px; }
.nuke-container .nuke-flag[data-cc="sy"] { background-position: -160px -143px; }
.nuke-container .nuke-flag[data-cc="sz"] { background-position: -176px -143px; }
.nuke-container .nuke-flag[data-cc="tc"] { background-position: -192px -143px; }
.nuke-container .nuke-flag[data-cc="td"] { background-position: -208px -143px; }
.nuke-container .nuke-flag[data-cc="tf"] { background-position: -224px -143px; }
.nuke-container .nuke-flag[data-cc="tg"] { background-position: -240px -143px; }
.nuke-container .nuke-flag[data-cc="th"] { background-position: 0 -154px; }
.nuke-container .nuke-flag[data-cc="tj"] { background-position: -16px -154px; }
.nuke-container .nuke-flag[data-cc="tk"] { background-position: -32px -154px; }
.nuke-container .nuke-flag[data-cc="tl"] { background-position: -48px -154px; }
.nuke-container .nuke-flag[data-cc="tm"] { background-position: -64px -154px; }
.nuke-container .nuke-flag[data-cc="tn"] { background-position: -80px -154px; }
.nuke-container .nuke-flag[data-cc="to"] { background-position: -96px -154px; }
.nuke-container .nuke-flag[data-cc="tr"] { background-position: -112px -154px; }
.nuke-container .nuke-flag[data-cc="tt"] { background-position: -128px -154px; }
.nuke-container .nuke-flag[data-cc="tv"] { background-position: -144px -154px; }
.nuke-container .nuke-flag[data-cc="tw"] { background-position: -160px -154px; }
.nuke-container .nuke-flag[data-cc="tz"] { background-position: -176px -154px; }
.nuke-container .nuke-flag[data-cc="ua"] { background-position: -192px -154px; }
.nuke-container .nuke-flag[data-cc="ug"] { background-position: -208px -154px; }
.nuke-container .nuke-flag[data-cc="um"] { background-position: -224px -154px; }
.nuke-container .nuke-flag[data-cc="us"] { background-position: -240px -154px; }
.nuke-container .nuke-flag[data-cc="uy"] { background-position: 0 -165px; }
.nuke-container .nuke-flag[data-cc="uz"] { background-position: -16px -165px; }
.nuke-container .nuke-flag[data-cc="va"] { background-position: -32px -165px; }
.nuke-container .nuke-flag[data-cc="vc"] { background-position: -48px -165px; }
.nuke-container .nuke-flag[data-cc="ve"] { background-position: -64px -165px; }
.nuke-container .nuke-flag[data-cc="vg"] { background-position: -80px -165px; }
.nuke-container .nuke-flag[data-cc="vi"] { background-position: -96px -165px; }
.nuke-container .nuke-flag[data-cc="vn"] { background-position: -112px -165px; }
.nuke-container .nuke-flag[data-cc="vu"] { background-position: -128px -165px; }
.nuke-container .nuke-flag[data-cc="xw"] { background-position: -144px -165px; }
.nuke-container .nuke-flag[data-cc="wf"] { background-position: -160px -165px; }
.nuke-container .nuke-flag[data-cc="ws"] { background-position: -176px -165px; }
.nuke-container .nuke-flag[data-cc="xk"] { background-position: -192px -165px; }
.nuke-container .nuke-flag[data-cc="xx"] { background-position: -208px -165px; }
.nuke-container .nuke-flag[data-cc="ye"] { background-position: -224px -165px; }
.nuke-container .nuke-flag[data-cc="yt"] { background-position: -240px -165px; }
.nuke-container .nuke-flag[data-cc="za"] { background-position: 0 -176px; }
.nuke-container .nuke-flag[data-cc="zm"] { background-position: -16px -176px; }
.nuke-container .nuke-flag[data-cc="zw"] { background-position: -32px -176px; }
/* Wrap every displayed username in [brackets], via ::before/::after so no render site or the name
   STRING is touched (the flag stays a separate sibling → "[name] 🇺🇸"). Applied to the pure-name
   spans + the profile page's dedicated name-text span (whose outer element also holds the flag/badge,
   which must stay OUTSIDE the brackets). `:not(:empty)` skips the brackets on spans whose textContent
   isn't set yet (hovercard / header before populate) so they never flash a bare "[]". */
.nuke-container .nuke-post-name:not(:empty)::before,
.nuke-container .nuke-post-quote-name:not(:empty)::before,
.nuke-container .nuke-hovercard-name:not(:empty)::before,
.nuke-container .nuke-chat-header-name:not(:empty)::before,
.nuke-container .nuke-post-modal-title-name:not(:empty)::before,
.nuke-container .nuke-profile-page-name-text:not(:empty)::before { content: '['; }
.nuke-container .nuke-post-name:not(:empty)::after,
.nuke-container .nuke-post-quote-name:not(:empty)::after,
.nuke-container .nuke-hovercard-name:not(:empty)::after,
.nuke-container .nuke-chat-header-name:not(:empty)::after,
.nuke-container .nuke-post-modal-title-name:not(:empty)::after,
.nuke-container .nuke-profile-page-name-text:not(:empty)::after { content: ']'; }
/* Brackets are 400-weight (vs the 700 username) and take the BODY-COPY colour (SETTING 7,
   --body-text-themed, the uploader's hue) so the [ ] match the message text, not the name;
   outside a post (hovercard/header/profile) --body-text-themed is unset → quiet --nk-text-2. */
.nuke-container .nuke-post-name::before, .nuke-container .nuke-post-name::after,
.nuke-container .nuke-post-quote-name::before, .nuke-container .nuke-post-quote-name::after,
.nuke-container .nuke-hovercard-name::before, .nuke-container .nuke-hovercard-name::after,
.nuke-container .nuke-chat-header-name::before, .nuke-container .nuke-chat-header-name::after,
.nuke-container .nuke-post-modal-title-name::before, .nuke-container .nuke-post-modal-title-name::after,
.nuke-container .nuke-profile-page-name-text::before, .nuke-container .nuke-profile-page-name-text::after {
  font-weight: 400;
  color: var(--body-text-themed, var(--nk-text-2));
}
/* Level icon as a verified-checkmark-style badge right after a username. A
   transparent 1em square — the size of one username character — that the icon
   fills, sitting right after the name like the next character. One rule for
   BOTH badge classes so the size is identical in every context (message head,
   reply quote, profile header, hover card, contact row, left profile panel) and
   the two classes can't fight over size by source order. `1em` is relative, so
   each row self-sizes from its own font-size; `align-self: center` keeps every
   badge vertically centered on its row. */
.nuke-container .nuke-post-name-badge,
.nuke-container .nuke-post-level-icon {
  width: 1em;
  height: 1em;
  object-fit: contain;
  flex-shrink: 0;
  align-self: center;
  display: block;
}
/* Sender's balance, top-right of the name row — same weight/size/color as the
   username (`.nuke-post-name`), pushed right and baseline-aligned with it. */
.nuke-container .nuke-post-balance {
  margin-left: auto;
  flex-shrink: 0;
  /* Center on the head row (not baseline) so it lines up vertically with the "..."
     button beside it, which is also center-aligned. */
  align-self: center;
  padding-left: 8px;
  /* Black, bold to match the profile's balance metric (800). */
  font-weight: 800;
  color: var(--nk-text);
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}
/* "..." more-options button at the right end of the post head (inert for now). The
   `--lead` variant carries the right-push when there's no balance before it. */
.nuke-container .nuke-post-more {
  margin-left: 6px;
  align-self: center;
  flex-shrink: 0;
  /* Negative block margins so the 22px button keeps its hit-area/hover circle WITHOUT
     inflating the head row past its ~19px text line (which pushed the message body down). */
  margin-top: -3px;
  margin-bottom: -3px;
  width: 22px;
  height: 22px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  border: none;
  border-radius: 999px;
  background: transparent;
  color: var(--nk-text-2);
  font-size: calc(16px - var(--font-shrink, 0px));
  line-height: 1;
  cursor: pointer;
}
.nuke-container .nuke-post-more--lead { margin-left: auto; }
.nuke-container .nuke-post-more:hover { background: rgba(0, 0, 0, 0.06); color: var(--nk-text); }
.nuke-container .nuke-post-name-cluster[data-has-account="true"] { cursor: pointer; }
/* The header username underlines only when its OWN name cluster is hovered — NOT
   on a hover anywhere in the post. (Previously any in-post hover, e.g. over an
   @mention, underlined the header name; that's been dropped so a mention hover
   only underlines the mention itself.) */
.nuke-container .nuke-post-name-cluster[data-has-account="true"]:hover .nuke-post-name { text-decoration: underline; }
/* Darken TINT on ANY post hover — the lift shadow is now PERMANENT (see the base rule), so only the tint
   changes here. Lives on plain `:hover` so it TRANSITIONS in/out via the background-color transition. */
.nuke-container .nuke-post:hover,
.nuke-container .nuke-post.is-hovercard-active {
  background-color: var(--card-hover);
}
/* Hovering a REPLY (.nuke-post-quote) inside the message drops the message's TINT back to its resting fill
   (the shadow stays), so only the reply reads active. Every OTHER inner target (image, video, music, doodle,
   receipt, avatar) NO LONGER blocks the tint — the message stays hover-tinted while you hover them. */
.nuke-container .nuke-post:hover:has(.nuke-post-quote:hover) {
  background-color: var(--card-bg);
}
/* /word send: the SOURCE post snaps ALL its hover transitions off for the moment of the send (JS
   toggles `.nuke-post-hover-snap` for ~400ms). If the source is the newest post, the sent message
   animates in ABOVE it and the source un-hovers — without this its 0.22s hover-EXIT fade would linger
   on top of that entrance animation. */
.nuke-container .nuke-post.nuke-post-hover-snap,
.nuke-container .nuke-post.nuke-post-hover-snap * { transition: none !important; }
/* Seamless hovercard morph: while the card is open, the hovered message's 40px pfp scales up
   to the card's 64px avatar (40 × 1.6) so it reads as growing INTO the card. Uses the
   individual `scale` property — it composes with the wrapper's `transform: rotate(--cc-tilt)`
   (the tilt) instead of clobbering it, and transitions on its own track. The `scale`
   transition is added to every post pfp (alongside the inherited --cc-tilt ease) so the
   grow-up AND the shrink-back on close are both smooth; timed + eased to match the card's
   `nuke-hovercard-pop` rebound so the avatar and the card move as one. */
.nuke-container .nuke-post-pfp {
  /* Base/exit: clean ease-out shrink back to 1 (transitions use the TO-state's timing, so this
     governs the close). 0.2s — clearly visible, no bounce on the way down. */
  transition:
    --cc-tilt 0.18s cubic-bezier(0.22, 1, 0.36, 1),
    scale 0.16s cubic-bezier(0.22, 1, 0.36, 1),
    box-shadow 0.16s ease;
}
.nuke-container .nuke-post.is-hovercard-active .nuke-post-pfp {
  scale: 1.6;
  z-index: 3;
  /* Lift-off shadow ON THE WRAPPER (the <img> swallows box-shadows; the rounded wrapper
     doesn't), and it grows with the 1.6× transform so the avatar reads as RISING off the page. */
  box-shadow: 0 6px 16px rgba(0, 0, 0, 0.32); /* lift only — the ring is now the real 1px img border */
  /* Grow-UP rebounds via a SPRING (back-out) transition — overshoots past 1.6 then settles.
     Done as a transition (not a keyframe animation) so it can't be clobbered by the base
     scale transition; the springy `y1 > 1` in the bezier is what makes it overshoot. */
  transition:
    --cc-tilt 0.18s cubic-bezier(0.22, 1, 0.36, 1),
    scale 0.16s cubic-bezier(0.22, 1, 0.36, 1),
    box-shadow 0.16s ease;
}
/* Pre-popup affordance: hovering the PFP itself (not the name/@handle) slightly lifts it —
   a small scale + a wrapper drop shadow — as instant feedback that the hovercard is about to
   open during the ~800ms hover-intent delay. Account posts only (NPCs don't open a popup), and
   suppressed once the popup is open, where the 1.6 morph above takes over (both transition on
   the same base, so 1.07 → 1.6 hands off smoothly). */
.nuke-container .nuke-post[data-has-account="true"]:not(.is-hovercard-active) .nuke-post-pfp[data-tgid]:hover {
  scale: 1.07;
  z-index: 2;
  box-shadow: 2px 2px 0 0 rgba(0, 0, 0, 0.2); /* hard offset drop-shadow (no resting shadow) */
}
.nuke-container .nuke-post-handle,
/* SETTING 4 (muted) → post timestamp + its "·" in the poster's hue; colourless → --nk-text-2. */
.nuke-container .nuke-post-time,
.nuke-container .nuke-post-dot { color: var(--set4-themed, var(--nk-text-2)); font-weight: 400; }
/* Timestamp (+ its "·" separator) matches the @handle pill's size — same --handle-scale.
   Scoped to the post/reply headers so the shared .nuke-post-dot in contact rows (which
   inherits its own 15px) is untouched. */
.nuke-container .nuke-post-head .nuke-post-time,
.nuke-container .nuke-post-head .nuke-post-dot,
.nuke-container .nuke-post-quote-head .nuke-post-quote-time,
.nuke-container .nuke-post-quote-head .nuke-post-quote-dot {
  font-size: calc(1em * var(--handle-scale));
}
/* All @handles render italic, site-wide: messages, reply quotes, the profile
   header, the hover card, profile/nav calling cards, and contact-row subtitles. */
.nuke-container .nuke-post-handle,
.nuke-container .nuke-post-quote-handle,
.nuke-container .nuke-chat-header-handle,
.nuke-container .nuke-hovercard-handle,
.nuke-container .nuke-profile-status,
.nuke-container .nuke-row-subtitle {
  font-style: italic;
  /* Italic glyphs lean past their advance width, so the last letter's slant gets
     shaved in any clipping container (the profile header handle and contact-row
     subtitle use overflow:hidden + ellipsis). Pad the right edge to give the lean
     room (overflow clips at the padding box, so it stays visible), and cancel the
     padding with an equal negative margin so nothing after the handle shifts. */
  padding-right: 0.2em;
  margin-right: -0.2em;
}
/* @handle rides in a subtle rounded PILL next to the username (feed rows + reply
   quotes). Background = the SENDER's profile colour — the SAME oklch-brightened --nuke-cmd-color
   the /word command messages use (see .nuke-chat-command) — so the pill matches those. Symmetric
   padding overrides the italic-lean hack above; margin-right resets its negative pull so the pill
   owns its own box and whatever follows sits a clean gap away. */
.nuke-container .nuke-post-handle,
.nuke-container .nuke-post-quote-handle {
  font-size: calc(1em * var(--handle-scale));
  background: oklch(from var(--nuke-cmd-color, #808080) clamp(0.85, l, 0.9) calc(c * 0.5) calc(h + 180) / 0.5);
  color: oklch(from var(--nuke-cmd-color, #808080) clamp(0.3, l, 0.42) calc(c * 0.5) calc(h + 180));
  border-radius: 999px;
  /* Asymmetric h-padding (less left, more right): the leading "@" carries extra
     left side-bearing and the italic lean crowds the trailing glyph against the
     right edge, so equal padding reads left-heavy — 5/8 recenters the text. */
  padding: 1px 8px 1px 5px;
  margin-right: 0;
  /* Fallback for browsers without round(): flex-centered like everything else. */
  align-self: center;
}
/* Snap the pill's vertical position to a whole pixel so it sits identically in
   every message (no ±1px wobble) — the same fix the flag uses. The pill's smaller
   font makes its own `1lh` = scale × the cluster's line box, so `1lh / scale`
   recovers the cluster line box (the name's height); the pill's box is that
   `1lh` plus its 2px vertical padding. Center within the name's box, snap to 1px,
   pin from flex-start (top-of-line is content-independent, unlike center). */
@supports (margin-top: round(1px, 1px)) {
  .nuke-container .nuke-post-handle,
  .nuke-container .nuke-post-quote-handle {
    align-self: flex-start;
    margin-top: round((1lh / var(--handle-scale) - 1lh - 2px) / 2, 1px);
  }
}
/* Sender's level icon + DPR%, pushed to the opposite (right) end of the header
   same level data prod shows on its cards. Pinned to the post's bottom-right
   corner (inside the 12px/16px padding). */
/* Full-width footer bar resting flush against the post's bottom edge: the
   negative side margins cancel the post's 16px h-padding (edge to edge) and the
   negative bottom cancels the 12px b-padding (no gap below); the inner padding
   re-insets the content. Holds the timestamp + reactions. */
.nuke-container .nuke-post-footer {
  display: flex;
  position: relative;                      /* anchor the cred badge absolutely in the pfp gutter */
  /* Reactions vertically centered in the footer band. */
  align-items: center;
  gap: 8px;
  /* Sides (-16) + bottom (-12) cancel the post's 16/12 padding so the band is
     edge-to-edge and flush with the post bottom. Symmetric 9px top/bottom
     padding centers the reaction pill in the band. Left padding is 16 + 52
     (the 40px pfp + 12px row gap) so footer content starts at the MESSAGE
     FIELD (text column) and never bleeds left into the avatar gutter. */
  margin: 0 -16px -12px;
  padding: 9px 16px 9px 68px;
}
/* Reactions live inside the bar now — hug the pill content; ordered LAST so the pill
   sits at the bar's RIGHT edge, after the cred stat + action icons. */
.nuke-container .nuke-post-footer .nuke-post-reactions {
  margin-left: 0;
  margin-top: 0;
  order: 2;
  flex: 0 0 auto;
  gap: 6px;
}
/* The bar holds the cred pill, the 4 action icons (Use Item / Compose / Like / Dislike)
   and the reaction pill as direct children, spread by space-between so EVERY gap is
   equal — cred→icon, icon→icon, and icon→reaction-pill all match. Fills the footer width
   left of the share button (flex: 1). */
.nuke-container .nuke-post-bar {
  order: 1;
  flex: 1;
  min-width: 0;
  display: flex;
  align-items: center;
  justify-content: space-between;
}
/* Mid-chain posts (no cred/icons) leave just the reaction pill in the bar — pin it right
   so it doesn't jump to the left edge. */
.nuke-container .nuke-post-bar > .nuke-post-reactions:only-child { margin-left: auto; }
/* The Copy-link button is the bar's LAST child — `space-between` parks it at the far right,
   one equal gap past Dislike (so it's no longer crammed against it). Never shrink it. */
.nuke-container .nuke-post-bar > .nuke-post-action-link {
  flex-shrink: 0;
}
.nuke-container .nuke-post-action-btn {
  width: 28px;
  height: 28px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  border: none;
  border-radius: 9999px;
  background: transparent;
  /* SETTING 6 (heading) → the footer action SVG icons (currentColor) in the poster's hue. */
  color: var(--icon-themed, var(--nk-text-2));
  font-size: calc(16px - var(--font-shrink, 0px));
  line-height: 1;
  cursor: pointer;
}
/* No hover COLOUR change on the action buttons; the gray hover circle + glyph scale-pop are defined
   with the vote buttons below. */
/* Share button — resting = just the icon (container removed), sized 24px (border-box) to line up with
   the other action icons; inherits the base .nuke-post-action-btn transparent/no-border resting state. */
.nuke-container .nuke-post-action-btn.nuke-post-action-link {
  box-sizing: border-box;
  width: 20px;
  height: 20px;
  font-size: calc(13px - var(--font-shrink, 0px));
}
/* "Use Item" pill — black capsule with white label + box icon (icon to the RIGHT
   of the text). White capsule sharing the cred/reaction pill's chrome — var(--nk-border)
   border — and the cred text style (Conduit, #536471). Border-box so the border
   doesn't add to the 24px height (matches the vote/cred pill). */
/* Use Item — icon-only button matching the like/dislike/compose buttons (no text,
   background, or border; the label is hidden). */
.nuke-container .nuke-post-use-item {
  width: 20px;
  height: 20px;
  flex-shrink: 0;
  box-sizing: border-box;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  /* Resting = just the icon, no container. Use Item is boxed ONLY inside the post footer (see the
     .nuke-post-bar rule below) — the chat-header user-card Use Item stays icon-only, like the composes. */
  border: none;
  border-radius: 999px;
  background: transparent;
  color: var(--nk-text-2);
  font-size: calc(13px - var(--font-shrink, 0px));
  line-height: 1;
  cursor: pointer;
}
.nuke-container .nuke-post-use-item i { font-size: calc(13px - var(--font-shrink, 0px)); }
/* Use Item glyph is a rounded square with a small filled radioactive symbol inside (16px). */
.nuke-container .nuke-post-use-item > svg { width: auto; height: 16px; flex-shrink: 0; }
.nuke-container .nuke-post-use-item-label { display: none; }
/* Just-copied feedback: the glyph swaps to a check and turns green for ~1.2s. */
.nuke-container .nuke-post-action-btn.nuke-post-action-link.is-copied,
.nuke-container .nuke-post-action-btn.nuke-post-action-link.is-copied:hover {
  background: none;   /* no green circle — the ✅ react animation is the only copy feedback now */
  color: #00BA7C;
}
/* Anchor the react-slam ✅ glyph to THIS button (playReactSlam appends an absolutely-positioned glyph);
   without a positioning context it lands on a higher ancestor (next to the Use-Item button). */
.nuke-container .nuke-post-action-link { position: relative; }
/* While the copy click plays its react-slam (playReactSlam '✅'), hide the link icon so the ✅ emoji
   glyph reads as the icon CHANGING to a checkmark; it fades back when .is-reacting clears. */
.nuke-container .nuke-post-action-link.is-reacting > svg { opacity: 0; }
/* Centre the ✅ glyph on the copy button — the base .nuke-vote-react is offset up (-57%) for the vote thumb. */
.nuke-container .nuke-post-action-link .nuke-vote-react { transform: translate(-50%, -50%); }
/* Reddit-style vote pill in the footer action row: upvote arrow, the cred value
   in the centre, downvote arrow — one rounded capsule. Chrome deliberately matches
   the reaction pill (`.nuke-reaction-pill`): #F7F9FA fill, 1px var(--nk-border) border, fully
   rounded; count is the same #666 / 12px as `.nuke-reaction-count`. */
/* Cred STAT pill — just the score now (the up/down vote buttons moved out into the
   action row). Matches the Use Item pill height/chrome. */
.nuke-container .nuke-post-vote {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  flex-shrink: 0;
  box-sizing: border-box;
  height: 20px;
  /* Width is locked to one chat-pfp so the badge is a constant size no matter how many
     digits of cred sit inside (justify-content centers them); no content-hug padding. */
  width: var(--nuke-chat-pfp-size);
  padding: 0;
  background: var(--nk-surface-2);
  /* Cred pill border = the CRED OWNER'S hue (SETTING 3 from --nuke-cmd-color, set on the post/row/hovercard
     of whoever HAS the cred). Falls back to --nk-border (the viewer's) only where no owner colour is set —
     e.g. the logged-in user's own header pill, where the owner IS the viewer. */
  --cred-border-themed: oklch(from var(--nuke-cmd-color) var(--set-3-l) var(--set-3-c) h);
  border: 2px solid var(--cred-border-themed, var(--nk-border-plain));   /* colourless owner → neutral, NOT the viewer colour */
  border-radius: 0; /* fully squared-off cred badge */
}
/* ── CRED BADGE TIERS ─────────────────────────────────────────────────────────────────────
   The cred pill upgrades with cred (data-metal, set by credMetalTier):
     0 POOP (cred <100 — BAD, muddy brown)  ·  (tier 1 = plain base badge, no attr, DEFAULT 100-350)
     →  2 bronze (350-500) · 3 silver (500-650) · 4 PURE GOLD (650-800).
   POOP is a matte "bad" brown rectangle; 2..4 are the metal ladder — each a top→bottom gradient with
   a soft sheen ridge in the upper third + a blended (metal-tone) edge + ink. The DEV badge is a dev
   testing tool that cycles all tiers on click (shared.ts). */
/* RED — cred <100 (BAD): a bright red badge with light-RED ink. */
.nuke-container .nuke-post-vote[data-metal="0"] { background: #f83a44; border-color: #d42a33; }
.nuke-container .nuke-post-vote[data-metal="0"] .nuke-post-vote-count { color: #ffc4c4; } /* light RED (not near-white), stays in the red family */
/* Metal tiers = every data-metal EXCEPT poop. Simplified metal: a vertical highlight→base→shade
   gradient + one soft top sheen; the border blends into the metal. */
.nuke-container .nuke-post-vote[data-metal]:not([data-metal="0"]) {
  background: var(--metal-bg);
  border-color: var(--metal-edge); /* keep the 2px width from the base rule; recolour to blend with the metal */
  box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.3);
}
.nuke-container .nuke-post-vote[data-metal]:not([data-metal="0"]) .nuke-post-vote-count {
  color: var(--metal-ink);
}
/* Per-tier metal palettes — a top→bottom gradient with a soft SHEEN ridge in the upper third (the
   polished-metal reflection, not a stark mirror band) + a blended edge + ink. */
.nuke-container .nuke-post-vote[data-metal="2"] { /* bronze — brighter copper-orange, less brown */
  --metal-bg: linear-gradient(180deg, #e3a659, #f8c980 35%, #d68b3b 62%, #b47530);
  --metal-edge: #b47530; --metal-ink: #4a2b0d;
}
.nuke-container .nuke-post-vote[data-metal="3"] { /* silver — the cool, bright (ex-platinum) design */
  --metal-bg: linear-gradient(180deg, #d5dde6, #f2f6fa 35%, #c6d0dc 62%, #abb7c4);
  --metal-edge: #abb7c4; --metal-ink: #222932;
}
.nuke-container .nuke-post-vote[data-metal="4"] { /* PURE GOLD — cred 650+ (the top) */
  --metal-bg: linear-gradient(180deg, #f6cb2e, #ffe486 35%, #eec015 62%, #d5a212);
  --metal-edge: #d5a212; --metal-ink: #6b4e00;
  box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.4), 0 0 5px rgba(255, 215, 0, 0.5); /* keep a soft glow */
}
/* The cred pill keeps its natural (number-hugging) size, but the footer spaces the action
   icons as if it occupied the reply button's fixed 24px footprint — so an ever-changing score
   (87 → 800) never re-flows the row. The pill is left-anchored in this 24px slot and allowed
   to overflow to the RIGHT (into the gap before Use Item); `overflow: visible` keeps the real
   pill chrome at full size while the LAYOUT only ever reserves 24px. Only used in the footer
   (the header/profile/hovercard cred pills stand alone, so they need no slot). */
.nuke-container .nuke-post-vote-slot {
  flex: 0 0 24px;
  width: 24px;
  display: inline-flex;
  align-items: center;
  justify-content: flex-start;
  overflow: visible;
}
/* Cred badge relocated INTO the pfp gutter — pinned under the 40px avatar (the footer's -16px margin
   puts its border-box at the post's left edge, so left:16px = the pfp's x). Vertically centered in the
   footer band (top:50%) so it keeps its old vertical placement; out of the bar's flex so the action
   icons reflow to fill the row. Same 40px width as the avatar it sits below. */
.nuke-container .nuke-post-footer .nuke-post-vote-slot {
  position: absolute;
  left: 16px;
  top: 50%;
  transform: translateY(-50%);
  width: var(--nuke-chat-pfp-size);
  z-index: 1;                              /* sit ABOVE the thread connector line (z-index 0) so it passes behind */
}
/* Like / dislike group — sits at the left of the footer, just before the reaction pill. */
.nuke-container .nuke-post-likedislike {
  display: flex;
  align-items: center;
  /* Room between 👍/👎, plus a gap after the group so it isn't cramped against the
     reaction pill (or the share button, when there are no reactions). */
  gap: 16px;
  margin-right: 16px;
  flex-shrink: 0;
}
/* Like / dislike (cred up/down) + Compose — icon buttons. */
/* Self-post Compose buttons (header card + the one on each of YOUR OWN posts in the feed/post page) carry
   BOTH glyphs: a pencil (default) and a clock-timer. While the shared 3h posting cooldown is active the pencil
   swaps to the clock — driven by `data-post-cooldown` on the container so it survives feed re-renders (new
   buttons show the right glyph instantly); the "Post again in Xh Ym" time shows per-button via its tooltip
   (data-tooltip). The per-post cred-vote composes are untagged, so they keep their own separate cred cooldown. */
.nuke-container .nuke-post-compose-self > .nuke-post-compose-timer { display: none; }
.nuke-container[data-post-cooldown] .nuke-post-compose-self > .nuke-post-compose-pencil { display: none; }
.nuke-container[data-post-cooldown] .nuke-post-compose-self > .nuke-post-compose-timer { display: inline-flex; }
.nuke-container .nuke-post-vote-btn,
.nuke-container .nuke-post-compose {
  width: 20px;
  height: 20px;
  flex-shrink: 0;
  box-sizing: border-box;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  /* Resting = just the icon — no container (bordered surface fill removed). The 24px box stays as the
     hit-area; border-radius keeps the hover circle round. */
  border: none;
  border-radius: 999px;
  background: transparent;
  color: var(--nk-text-2);
  font-size: calc(13px - var(--font-shrink, 0px));
  line-height: 1;
  cursor: pointer;
  /* `scale` (individual prop) carries the glyph hover-pop; `transform` carries the press SLAM-DOWN
     (a snappy ~0.085s push-in on :active) so the two compose cleanly. Both durations are vars (with
     fallbacks) for easy tuning. */
  transition: background 0.15s ease, color 0.15s ease, scale var(--nuke-vote-scale-dur, 0.10s) ease,
    transform var(--nuke-vote-press-dur, 0.085s) ease;
}
/* Footer + header action buttons: NO hover disc (removed) — just the glyph scale-pop on hover (below).
   The transition below still eases the .is-reacting / .is-copied feedback backgrounds. */
.nuke-container .nuke-post-use-item,
.nuke-container .nuke-post-action-link { transition: background 0.15s ease; }
/* Footer action buttons run container-less — no box/border/fill, just the glyph. SETTING 6 (heading):
   every footer glyph (Like · Dislike · Use Item · Compose · Copy-link) takes the POSTER's hue via
   --icon-themed; colourless author → the neutral --nk-text-2. */
.nuke-container .nuke-post-bar .nuke-post-vote-btn,
.nuke-container .nuke-post-bar .nuke-post-compose,
.nuke-container .nuke-post-bar .nuke-post-use-item,
.nuke-container .nuke-post-bar .nuke-post-action-btn.nuke-post-action-link {
  color: var(--icon-themed, var(--nk-text-2));
}
/* EVERY footer action glyph carries the hover-pop / press-push (Use Item · Compose · Like ·
   Dislike · Copy-link, and the cooldown clock). inline-flex so the `scale` transform applies and
   the glyph stays centred; the scale transition lives on the glyph. */
.nuke-container .nuke-post-vote-up > :is(i, svg),
.nuke-container .nuke-post-vote-down > :is(i, svg),
.nuke-container .nuke-post-use-item > :is(i, svg),
.nuke-container .nuke-post-compose > :is(i, svg),
.nuke-container .nuke-post-action-link > :is(i, svg),
.nuke-container .nuke-post-vote-cooldown > :is(i, svg) {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  transition: scale var(--nuke-vote-scale-dur, 0.10s) ease;
}
/* Inline-SVG action icons (Like · Dislike · Compose · Copy-link · cooldown) — sized to sit with the
   ~13px RemixIcon glyphs (Use Item keeps its <i>). fill:currentColor already tracks the button colour. */
.nuke-container .nuke-post-vote-up > svg,
.nuke-container .nuke-post-vote-down > svg,
.nuke-container .nuke-post-compose > svg,
.nuke-container .nuke-post-action-link > svg,
.nuke-container .nuke-post-vote-cooldown > svg {
  width: 17px;
  height: 17px;
}
/* Up/down (like/dislike) thumbs — matched to the Use Item glyph (13px, see .nuke-post-use-item i). */
.nuke-container .nuke-post-vote-up > svg,
.nuke-container .nuke-post-vote-down > svg {
  width: 13px;
  height: 13px;
  flex-shrink: 0; /* never shrink to the button (the bug that hid earlier size bumps) */
}
/* Share/upload glyph — a touch above the Use Item glyph (15px), smaller than the 17px action default above. */
.nuke-container .nuke-post-action-link > svg { width: 15px; height: 15px; }
/* Compose (cred post) pencil + BOTH cooldown timers (the per-post cred cooldown .nuke-post-vote-cooldown
   AND the self-post .nuke-post-compose-timer, which is itself a .nuke-post-compose > svg) share ONE size so
   the pencil↔clock swap never jumps — change this single value to move all of them together. 15px. */
.nuke-container .nuke-post-compose > svg,
.nuke-container .nuke-post-vote-cooldown > svg { width: 15px; height: 15px; }
/* Optical nudge — the arrows read ~1px inward; push the up glyph 1px left, the down glyph 1px right.
   `translate` (individual prop) composes with the hover `scale:` below without clobbering it. */
.nuke-container .nuke-post-vote-up > svg { translate: -0.5px 0; }
.nuke-container .nuke-post-vote-down > svg { translate: 0.5px 0; }
/* Selected vote → fill the arrow in (hollow at rest, solid once you've liked/disliked). Only the chosen
   side gets `.is-active`; stroke is already currentColor, so fill:currentColor makes it a solid glyph. */
.nuke-container .nuke-post-vote-up.is-active > svg,
.nuke-container .nuke-post-vote-down.is-active > svg { fill: currentColor; }
/* Hover pop — all action glyphs, including a voted thumb and the cooldown clock ("only the hover"). */
.nuke-container .nuke-post-vote-up:hover > :is(i, svg),
.nuke-container .nuke-post-vote-down:hover > :is(i, svg),
.nuke-container .nuke-post-use-item:hover > :is(i, svg),
.nuke-container .nuke-post-compose:hover > :is(i, svg),
.nuke-container .nuke-post-action-link:hover > :is(i, svg),
.nuke-container .nuke-post-vote-cooldown:hover > :is(i, svg) { scale: 1.1; }
/* Press push — Use Item / Compose / Copy-link push their GLYPH in (their click has no slam, so the
   push-in IS the feedback). */
.nuke-container .nuke-post-use-item:active > :is(i, svg),
.nuke-container .nuke-post-compose:active > :is(i, svg),
.nuke-container .nuke-post-action-link:active > :is(i, svg) { scale: 0.92; }
/* Like / dislike PRESS = the slam-DOWN. The WHOLE button (thumb + count) scales down on mousedown
   and HOLDS there; releasing fires the reaction, whose `.is-reacting` rebound springs it back up
   from exactly this size — so the punch-down is felt the instant you press and the spring on
   release: the slam is split across the click-and-release for a responsive feel. Suppressed once
   voted (no re-press). */
.nuke-container .nuke-post-vote-up:not(.is-voted):active,
.nuke-container .nuke-post-vote-down:not(.is-voted):active { transform: scale(0.72); }
/* Combined vote control: [thumb-up] [total] [thumb-down] — the two thumb buttons flank the total count
   (all likes + dislikes + reactions). One group so the footer bar treats it as a single control. */
.nuke-container .nuke-post-vote-group {
  display: inline-flex;
  align-items: center;
  gap: 0;
  box-sizing: border-box;
  /* Container-less — the up/count/down sit as bare icons + number, no pill fill/border. */
  height: 20px;
  padding: 0 2px;
  background: transparent;
  border: none;
}
/* Thumb buttons inside the (now container-less) like/dislike group — a touch larger than the other
   action buttons so the up/down arrows read bigger. */
.nuke-container .nuke-post-vote-group .nuke-post-vote-btn {
  width: 22px;
  height: 22px;
}
.nuke-container .nuke-post-vote-count {
  min-width: 1.4em;
  text-align: center;
  /* Match the footer thumb SVGs — SETTING 6 (--icon-themed, poster hue); colourless → --nk-text-2. */
  color: var(--icon-themed, var(--nk-text-2));
  font-family: var(--font-conduit);
  font-size: calc(12px - var(--font-shrink, 0px));
  font-weight: 600;
  font-variant-numeric: tabular-nums;
}
/* Like / dislike counts float OUT of the layout flow: a count appearing or disappearing must
   not move the thumb (which would otherwise re-center on icon+count) OR any neighbor. The
   button stays a fixed 24px with its thumb centered exactly as in the count-less state; the
   number is pinned just right of the button's container, sitting in the gap. (The cred pill uses the same
   `.nuke-post-vote-count` class but lives in `.nuke-post-vote`, so it's untouched.) */
.nuke-container .nuke-post-vote-up,
.nuke-container .nuke-post-vote-down { position: relative; }
.nuke-container .nuke-post-vote-up .nuke-post-vote-count,
.nuke-container .nuke-post-vote-down .nuke-post-vote-count {
  position: absolute;
  left: 100%;
  top: 50%;
  transform: translateY(-50%);
  min-width: 0;
  /* Sit just OUTSIDE the button's container border (positive gap), not tucked over it. */
  margin-left: 3px;
}
/* Cred-vote cooldown: while the per-target 24h window is active, a countdown shows in
   place of the like/dislike buttons (no buttons → re-voting is gated). Standalone in the
   action row now (the up/down moved out of the cred pill). */
.nuke-container .nuke-post-vote-cooldown {
  box-sizing: border-box;
  width: 20px;
  height: 20px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  flex-shrink: 0;
  /* Container-less — just the clock + countdown in the username colour, matching the other footer
     action icons (the Compose button it replaces during cooldown is now container-less too). */
  border: none;
  background: transparent;
  color: var(--nk-text);
  font-family: var(--font-conduit);
  font-size: calc(12px - var(--font-shrink, 0px));
  font-weight: 600;
  font-variant-numeric: tabular-nums;
}
.nuke-container .nuke-post-vote-cooldown i {
  /* Match the footer's other action icons (13px) — the clock looked undersized. */
  font-size: calc(13px - var(--font-shrink, 0px));
  line-height: 1;
}
/* `.nuke-post-level-icon` is sized together with `.nuke-post-name-badge` in the
   unified badge rule above (hug-the-glyph). No size rule here — a standalone
   block would override that one by source order and reintroduce the square box. */
.nuke-container .nuke-post-dpr {
  /* Conduit font, 15px (inherited), weight 400 — gray like the @handle. */
  font-family: var(--font-conduit);
  color: var(--nk-text-2);
  font-weight: 400;
}
/* Everything in the body flows at natural next-line spacing — no paragraph gaps
   between segments, images, or the DPR row. Sequential messages, captions, and
   images all read like consecutive lines of one message (line-height does the
   spacing). */
.nuke-container .nuke-post-body { display: flex; flex-direction: column; gap: 0; }
/* Reactions SLOT — reserves a fixed 3-reaction width regardless of how many
   reactions a message has, so the footer action icons never shift with the count.
   Widened to 170px (was 126px) to fit three 2-digit badges at the larger Conduit
   count font on ONE line. The pill is right-aligned inside and hugs its badges;
   0–2 reactions leave empty space on the slot's LEFT, never moving the actions. */
.nuke-container .nuke-post-reactions {
  flex: 0 0 170px;
  display: flex;
  align-items: center;
  justify-content: flex-end;
}
/* The visible pill — chrome + badges, hugging its content, right-aligned within
   the slot. Rendered only when the message has reactions (no empty chip). A rare
   3-digit-count pill wraps inside the slot rather than overflowing the actions. */
.nuke-container .nuke-reaction-pill {
  gap: 8px;
  padding: 3px 10px;
  background: var(--nk-surface-2);
  border: 1px solid var(--nk-border);
  border-radius: 999px;
}
.nuke-container .nuke-post-text {
  overflow-wrap: anywhere;
  line-height: 1.35;
  /* Message text a touch smaller than the 15px post base. */
  font-size: calc(14px - var(--font-shrink, 0px));
}
/* Level icon + DPR% floated to the top-right of the first text block, right-aligned
   so its right edge lines up under the balance's right edge. The first line of text
   wraps before it; it's a single row tall, so line 2 onward clears it and runs
   full width. Posts with no DPR don't get this span, so their text is full width. */
.nuke-container .nuke-post-rate {
  float: right;
  display: flex;
  align-items: center;
  gap: 3px;
  /* Wider gap so the first line of text cuts to the next line a bit sooner,
     well clear of the rate rather than crowding right up against it. */
  margin-left: 20px;
}
/* Image-only posts: the level icon + DPR ride their own right-aligned row at the
   top of the body (under the balance), since there's no text line to float into. */
.nuke-container .nuke-post-rate-row {
  display: flex;
  align-items: center;
  justify-content: flex-end;
  gap: 3px;
}
/* DPR + level icon match the @handle / header text size — no longer shrunk: the
   DPR inherits the base 15px `.nuke-post-dpr` and the level icon is a 1em square
   via the unified badge rule. */
/* Images get X.com-style breathing room above them — a roomier, CONSISTENT gap
   (not next-line) whatever precedes the image: a caption, the DPR row (image-only
   post), or a prior message. Body gap is 0, so this margin is the whole gap. This
   also covers a reply's own image (the gap ABOVE it). */
.nuke-container .nuke-post-media { margin-top: 8px; }
/* Inline video embed (a video post) — 16:9 player filling the post width.
   NB: class is `nuke-post-video-EMBED`, distinct from the `.nuke-post-video` icon
   button in the chat-header action row. */
.nuke-container .nuke-post-video-embed {
  position: relative; /* contains the facade's absolute poster */
  margin-top: 8px;
  width: 100%;
  aspect-ratio: 16 / 9;
  /* Squared, uniform 2px border all round (--card-border). */
  border-radius: 0;
  overflow: hidden;
  background: #000;
  border: 2px solid var(--card-border);
  /* Hover lift + shadow (shared with the other media, see below). */
  transition: transform 0.09s ease, box-shadow 0.18s ease;
  /* Own compositor layer so the hover scale rasters the border + poster as ONE texture — kills the
     sub-pixel dark-bg hairline that otherwise shows around the poster on scale. */
  will-change: transform;
}
.nuke-container .nuke-post-video-embed iframe,
.nuke-container .nuke-post-video-embed video {
  display: block;
  width: 100%;
  height: 100%;
  border: none;
}
/* YouTube FACADE — a clean poster + centered play button in place of the live iframe (which is
   only inserted on click, see video-embed.ts). The whole tile is the click target; the play button
   is purely visual (pointer-events none). */
.nuke-container .nuke-video-facade { cursor: pointer; }
.nuke-container .nuke-video-poster {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover; /* crops hqdefault's 4:3 letterbox to a clean 16:9 */
  display: block;
}
.nuke-container .nuke-video-play {
  position: absolute;
  left: 50%;
  top: 50%;
  transform: translate(-50%, -50%);
  width: 60px;
  height: 60px;
  border: none;
  border-radius: 50%;
  background: rgba(0, 0, 0, 0.55);
  pointer-events: none;
  display: flex;
  align-items: center;
  justify-content: center;
  transition: background 0.15s ease;
}
.nuke-container .nuke-video-facade:hover .nuke-video-play { background: rgba(0, 0, 0, 0.72); }
.nuke-container .nuke-video-play::before {
  content: '';
  width: 0;
  height: 0;
  border-style: solid;
  border-width: 10px 0 10px 17px;
  border-color: transparent transparent transparent #fff;
  margin-left: 4px;
}
/* Music post (a song upload) — the album art rendered as a CD: a circular disc that SPINS while
   playing, with a centre spindle hole + hub ring and a soft gloss (::after). The WHOLE tile is
   the click target (play/pause, wired in music-embed.ts); the play/pause badge fades in on hover
   over the centre. Play state lives on the tile's data-state. The gradient is a fallback for a
   source with no cover (its <img> is hidden on error). */
/* Outer CD chrome — an UNMASKED wrapper. The disc itself (.nuke-post-music-tile) has a CSS mask for
   the spindle hole, and a mask clips any outline / box-shadow, so the 4px keyline AND the hover
   drop-shadow can't live on the disc — they live here on the parent instead. Shrink-wraps the disc. */
.nuke-container .nuke-post-music-cd {
  /* Pin the CD base colour to the CURRENT (cool) light --nk-border-soft (#EFF3F4) so the disc + rings
     (all color-mix variants of it, below) keep the cool-light look even in dark mode. */
  --nk-border-soft: #EFF3F4;
  display: block;
  width: fit-content;   /* wrap tightly around the 320px disc; phones cap via max-width */
  max-width: 100%;
  box-sizing: border-box;
  margin-top: 8px;      /* the disc's old top gap, moved up here */
  border: 3px solid color-mix(in srgb, var(--nk-border-soft) 96%, #000);  /* outer keyline (disc colour darkened 4%) — a CD's fine outer edge */
  border-radius: 50%;
  /* Backstop for the outer seam: teal ONLY at the disc edge (behind the tile teal ring), so the sub-pixel
     gap between the tile edge and this keyline reads as teal, not the chat panel. Transparent in the
     centre so the spindle hole still shows the panel through the ::before. */
  background: radial-gradient(circle at 50% 50%, transparent 0 65%, color-mix(in srgb, var(--nk-border-soft) 75%, #000) 65%);
  cursor: pointer;
  /* Drop shadow lives here (off the masked disc): a RESTING lift like the sticky note, stronger on hover
     (:hover → --card-lift-strong below, :active → --card-lift-press). NO will-change: it pins a
     pre-rasterised layer that resamples the keyline on the hover transform, LIGHTENING it. */
  /* No drop shadow — the disc sits flat (resting/hover/press lift shadows all removed). */
  transition: transform 0.09s ease;
  position: relative;   /* positioning context for the ::before hole-insert */
}
/* The spindle hole is a REAL cutout, so a layer BEHIND the disc shows through it. This ::before is a
   hole-sized disc sitting behind the tile: its keyline reads as the border AROUND the hole (same
   colour as the outer keyline), and on hover an INSET shadow paints INSIDE the hole (depth, as if the
   disc lifts). Sized just over the ~15.56% hole so the border overhangs the disc opaque edge (no gap) at any
   scale; centred on the hole. z-index/order: painted before the tile, so the tile (its own layer) sits
   on top and the transparent hole reveals this. */
.nuke-container .nuke-post-music-cd::before {
  content: "";
  position: absolute;
  left: 50%;
  top: 50%;
  width: 16.1%;   /* slightly > the ~15.56% hole so the 4px border overhangs to the disc opaque edge (11.3%): no transparent gap between this border and the darker ::after ring */
  aspect-ratio: 1 / 1;
  transform: translate(-50%, -50%);
  border-radius: 50%;
  box-sizing: border-box;
  border: 4px solid color-mix(in srgb, var(--nk-border-soft) 96%, #000);  /* same as the sticky-note border */
  pointer-events: none;
  z-index: 0;
  /* No inner spindle-hole shadow (resting + hover insets removed) — the hole reads as a clean cutout. */
}
.nuke-container .nuke-post-music-tile {
  position: relative;
  z-index: 1;    /* above the wrapper ::before hole-insert */
  /* ONE knob for the whole disc size — the wrapper shrink-wraps it (fit-content), and the spindle hole,
     grooves, rings, shine + art masks are all %-based so they scale with this automatically. Only the
     fixed-px borders/blur need their own (small) overrides. Phones set --music-cd-size below. */
  width: var(--music-cd-size, 320px);
  max-width: var(--music-cd-size, 320px);
  aspect-ratio: 1 / 1;
  border-radius: 50%; /* the CD disc */
  overflow: hidden;
  /* Opaque disc face — shows in the hub (art masked out) + as the no-cover fallback. */
  background: var(--nk-border-soft); /* disc face = the base outer-2px-keyline colour */
  border: 4px solid color-mix(in srgb, var(--nk-border-soft) 75%, #000); /* outer ring — darker variant of the keyline (a CD's fine dark edge) */
  cursor: pointer;
  /* The tile itself never transforms: hover lift/shadow live on the wrapper, and the spin drives the
     art + ::before (not the disc) — so no transition / will-change here. */
  /* Spindle hole = a REAL transparent cutout through the entire disc (not a painted circle): the
     chat panel behind shows through it, so the hole picks up the message row's background — including
     its hover colour. Centred, so it holds still while the art spins. */
  -webkit-mask: radial-gradient(circle at 50% 50%, transparent 11%, #000 11.3%);
  mask: radial-gradient(circle at 50% 50%, transparent 11%, #000 11.3%);
}
/* While a track is PLAYING the disc is a jog wheel (music-embed.ts pointer scrub). Claim TOUCH drags on it
   so the browser scrubs instead of scrolling the feed (a scroll would cancel the pointer stream and kill the
   drag). ONLY while playing, so a PAUSED disc still scrolls the page and a tap just plays it. Covers the art
   child too, since a touch can land on the image. */
.nuke-container .nuke-post-music-tile[data-state="playing"],
.nuke-container .nuke-post-music-tile[data-state="playing"] .nuke-post-music-art {
  touch-action: none;
}
.nuke-container .nuke-post-music-art {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
  opacity: 1; /* music thumbnail (album art) at full opacity */
  /* Punch the artwork out of the centre hub so the middle ring carries NO label art — the disc's
     dark base + the ::after hub rings show through instead, like a real CD's clear plastic centre.
     The hole meets the ::after outer black line (~16.85%) so that line stays clean. Centred → invisible under the spin. */
  -webkit-mask: radial-gradient(circle at 50% 50%, transparent 14.3%, #000 14.5%);
  mask: radial-gradient(circle at 50% 50%, transparent 14.3%, #000 14.5%);
}
.nuke-container .nuke-post-music-art.nuke-post-music-art-missing { display: none; }
.nuke-container .nuke-post-music-audio { display: none; }
/* Disc shine — two soft white glows (bottom-left + top-right) forming a diagonal sheen. A STATIC
   reflection: it is NOT in the --cd-rot spin selector, so the light holds still while the label turns.
   Below the hub rings (z-index 1), above the art. */
.nuke-container .nuke-post-music-tile::before {
  content: '';
  position: absolute;
  inset: 0;
  border-radius: inherit;
  pointer-events: none;
  z-index: 1;
  /* The OLD disc-face highlight, restored (it replaced the spectral+cone shine): a soft DIRECTIONAL sheen
     (bright upper-left -> shadow lower-right) that carries the curved disc read, with two faint white gloss
     arcs riding on top — one broad sweep (upper-left) + one short glint (lower-right). Its arcs sit on the
     same upper-left/lower-right diagonal the new highlights occupied (286-344deg / 130-145deg), so the old
     look lands where the new highlights were. Kept the new radial mask + a light 2px blur + spin (the framing). */
  background:
    conic-gradient(from 0deg at 50% 50%,
      transparent 286deg, rgba(255, 255, 255, 0.035) 286deg, rgba(255, 255, 255, 0.035) 344deg, transparent 344deg),
    conic-gradient(from 0deg at 50% 50%,
      transparent 130deg, rgba(255, 255, 255, 0.028) 130deg, rgba(255, 255, 255, 0.028) 145deg, transparent 145deg),
    linear-gradient(125deg,
      rgba(255, 255, 255, 0.20) 2%, rgba(255, 255, 255, 0) 42%, rgba(0, 0, 0, 0) 58%, rgba(0, 0, 0, 0.20) 100%);
  -webkit-mask: radial-gradient(circle at 50% 50%, transparent 14%, #000 34%, #000 90%, transparent 100%);
  mask: radial-gradient(circle at 50% 50%, transparent 14%, #000 34%, #000 90%, transparent 100%);
  filter: blur(2px);
}
/* Concentric rings + groove texture + diffraction sheen, above the art + shine. At the hub: cool (teal)
   clamp rings just outside the spindle hole, then a DEEP dark opacity band (the CD centre ring) with NO
   ring wrapping its outer edge. At the rim: a fine dark lead-out line + a faint white clear-rim highlight. */
.nuke-container .nuke-post-music-tile::after {
  content: '';
  position: absolute;
  inset: 0;
  border-radius: inherit;
  pointer-events: none;
  z-index: 2;
  background:
    radial-gradient(circle at 50% 50%,
      transparent 0 11%,
      color-mix(in srgb, var(--nk-border-soft) 60%, #000) 11% 12%,        /* cool ring — teal hole rim */
      color-mix(in srgb, var(--nk-border-soft) 92%, #000) 12% 12.9%,      /* light teal groove between rings */
      color-mix(in srgb, var(--nk-border-soft) 70%, #000) 12.9% 14.53%,   /* cool ring — teal clamp (meets the art edge) */
      rgba(0, 0, 0, 0.64) 14.53% 21.6%,      /* DARK hub opacity band — the deep centre ring (kept), clean outer edge */
      transparent 21.6% 87%,
      rgba(0, 0, 0, 0.22) 87% 87.7%,         /* fine dark data lead-out line, close to the thinned edge */
      transparent 87.7% 89%,
      rgba(255, 255, 255, 0.13) 89% 89.7%,   /* thin clear-rim highlight at the very edge */
      transparent 89.7%),
    /* Fine concentric DATA-TRACK grooves — a hair-thin alternating dark/light ring pair every ~2.4px,
       radially symmetric so they hold still under the spin: the etched groove texture of a real disc (very faint). */
    repeating-radial-gradient(circle at 50% 50%, rgba(0, 0, 0, 0.018) 0 1.2px, rgba(255, 255, 255, 0.016) 1.2px 2.4px),
    /* Iridescent DIFFRACTION sheen — two VERY low-opacity spectral arcs on opposite sides (cyan/magenta/
       green peaks, near-clear troughs): the faint rainbow raking light throws across a CD. Static on the
       non-spinning ::after, so it holds against the light while the label turns. */
    conic-gradient(from 30deg at 50% 50%,
      hsl(190 95% 60% / 0.045), hsl(265 95% 62% / 0.012) 60deg, hsl(330 95% 62% / 0.045) 120deg,
      hsl(45 95% 60% / 0.012) 200deg, hsl(110 90% 55% / 0.045) 260deg, hsl(190 95% 60% / 0.012) 320deg,
      hsl(190 95% 60% / 0.045));
  /* Clip the grooves + sheen to the DATA ANNULUS: out of the spindle hole (<=9%) and off the outer rim
     (>=90%), so only the recorded surface between hub and edge carries them (the hub rings sit on top). */
  -webkit-mask: radial-gradient(circle at 50% 50%, transparent 0 9%, #000 11%, #000 90%, transparent 92%);
  mask: radial-gradient(circle at 50% 50%, transparent 0 9%, #000 11%, #000 90%, transparent 92%);
}
/* Spin the whole illustrated disc — the art (label) AND the shines (::before) share a single rotation var
   so they turn together as one drawn object (the hub ::after is radially symmetric, so it needs no
   spin). The angle is driven frame-by-frame from JS (music-embed.ts): it accelerates from rest up to
   speed on play and winds DOWN to a stop on pause (keeps turning while decelerating, then holds its
   final angle) — behaviour a CSS keyframe loop can't express. --cd-rot defaults to 0deg when idle. */
.nuke-container .nuke-post-music-art,
.nuke-container .nuke-post-music-tile::before {
  transform: rotate(var(--cd-rot, 0deg));
}
.nuke-container .nuke-post-music-art { will-change: transform; }
/* No play/pause badge — the disc itself is the affordance (spinning = playing, frozen = paused);
   the whole tile toggles on click (music-embed.ts). */
/* A reply's image renders above the quoted-message container; give the gap BELOW
   it (image → quote box) the same 12px as every other image, instead of the
   tighter 8px the quote box's own default margin would give. */
.nuke-container .nuke-post-media + .nuke-post-reply { margin-top: 12px; }
.nuke-container .nuke-post-media-1 .nuke-post-img-wrap {
  /* Fill the full post width; height comes from the inline aspect-ratio. */
  width: 100%;
  max-width: 100%;
  position: relative;
  /* Squared, uniform 2px border all round in the card's own border colour (--card-border). */
  border-radius: 0;
  border: 2px solid var(--card-border);
  /* Hover lift transition (snappy). The press → rebound is driven in JS
     (`playCardPress`) so a fast click still plays the full animation and reads the
     same across sizes — see chat-render.ts. */
  transition: transform 0.09s ease, box-shadow 0.18s ease;
}
/* Noto-emoji "framed picture" (🖼️) style frame around POST images only — self-post uploads
   (selfPostImageHtml) tag their wrap `.nuke-post-framed`; chat-message images (renderMedia) omit it and
   keep the plain 1px/rounded chrome. NOT stickers (transparent-bg → bare) and NOT the multi-image grid.
   Scoped selector also gates the hover-translate exception + the JS translate press (nukeIsFramedImage).
   A 9-slice border-image (90-unit SVG, slice 30 → 24px border): a MITRED orange-wood frame that runs right
   to the photo edge (no white mat), built as THREE nested mitred bands — OUTER 8px (medium tone), MIDDLE
   12px (lightest/lit), INNER 4px (deepest wood) — for a stepped-molding depth (the 3 tone-sets were cycled
   outer→inner→middle→outer, so the dominant middle band is brightest with a dark inner edge at the photo).
   The band inset ranges in
   SVG units are outer 0..10, middle 10..25, inner 25..30; rendered widths follow the border-image scale
   (0.8 = 24/30), so they double if you change the border width. Each band is split into 4 sides by the 45°
   corner diagonals (a `band(p,q,colours)` helper emits the 4 trapezoids for the inset range), so it's
   3×4 = 12 flat polygons. A bottom-right light per band: left+top burnt-orange (shadow), right+bottom amber
   (lit). All divisions — the diagonal miters AND the band boundaries — are just colour changes; NO keylines,
   NO ornament, NO gradients, NO drawn seams. 4px rounded corners: the SVG's clip rect is rx 5 (×0.8 = 4px)
   and the element's border-radius is 4px, so the frame's outer silhouette rounds 4px (the inner photo is
   square). border-image-repeat is STRETCH — one stretched slice per edge, zero tile seams.
   box-sizing:border-box keeps the framed footprint stable; the <img> fills the padding box inside the frame. */
.nuke-container .nuke-post-media-1 .nuke-post-img-wrap.nuke-post-framed:not(.is-sticker) {
  box-sizing: border-box;
  border: 24px solid transparent;
  border-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='90' height='90'%3E%3Cdefs%3E%3CclipPath id='rc'%3E%3Crect x='0' y='0' width='90' height='90' rx='5' ry='5'/%3E%3C/clipPath%3E%3C/defs%3E%3Cg clip-path='url(%23rc)'%3E%3Cpolygon points='0,0 90,0 80,10 10,10' fill='%23bc6f26'/%3E%3Cpolygon points='90,0 90,90 80,80 80,10' fill='%23d8982d'/%3E%3Cpolygon points='0,90 90,90 80,80 10,80' fill='%23ddac38'/%3E%3Cpolygon points='0,0 0,90 10,80 10,10' fill='%23b1611f'/%3E%3Cpolygon points='10,10 80,10 65,25 25,25' fill='%23cc8837'/%3E%3Cpolygon points='80,10 80,80 65,65 65,25' fill='%23e0ae51'/%3E%3Cpolygon points='10,80 80,80 65,65 25,65' fill='%23e0b75d'/%3E%3Cpolygon points='10,10 10,80 25,65 25,25' fill='%23c2792e'/%3E%3Cpolygon points='25,25 65,25 60,30 30,30' fill='%23a65f1c'/%3E%3Cpolygon points='65,25 65,65 60,60 60,30' fill='%23c28123'/%3E%3Cpolygon points='25,65 65,65 60,60 30,60' fill='%23c88b28'/%3E%3Cpolygon points='25,25 25,65 30,60 30,30' fill='%23954f17'/%3E%3C/g%3E%3C/svg%3E") 30 / 24px / 0 stretch;
  border-radius: 4px;
  /* No resting shadow — matches chat images: the shadow only grows on hover (the shared
     .nuke-post-img-wrap:hover:not(.is-sticker) rule) and clears on unhover. */
}
/* Extremely subtle INNER shadow on the framed photo — it reads as slightly recessed below the frame.
   inset:0 resolves to the wrap padding box (the photo area, inside the 24px border); overflow:hidden on
   the wrap clips it there. pointer-events:none so it never eats clicks. */
.nuke-container .nuke-post-media-1 .nuke-post-img-wrap.nuke-post-framed:not(.is-sticker)::after {
  content: "";
  position: absolute;
  inset: 0;
  pointer-events: none;
  box-shadow: inset 0 0 12px rgba(0, 0, 0, 0.3);
}
/* [light-chat baseline] disabled — feed framed images use the light gold frame now
.theme-lightsout .nuke-container .nuke-post-media-1 .nuke-post-img-wrap.nuke-post-framed:not(.is-sticker) {
  border-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='90' height='90'%3E%3Cdefs%3E%3CclipPath id='rc'%3E%3Crect x='0' y='0' width='90' height='90' rx='5' ry='5'/%3E%3C/clipPath%3E%3C/defs%3E%3Cg clip-path='url(%23rc)'%3E%3Cpolygon points='0,0 90,0 80,10 10,10' fill='%2396591e'/%3E%3Cpolygon points='90,0 90,90 80,80 80,10' fill='%23ad7a24'/%3E%3Cpolygon points='0,90 90,90 80,80 10,80' fill='%23b18a2d'/%3E%3Cpolygon points='0,0 0,90 10,80 10,10' fill='%238e4e19'/%3E%3Cpolygon points='10,10 80,10 65,25 25,25' fill='%23a36d2c'/%3E%3Cpolygon points='80,10 80,80 65,65 65,25' fill='%23b38b41'/%3E%3Cpolygon points='10,80 80,80 65,65 25,65' fill='%23b3924a'/%3E%3Cpolygon points='10,10 10,80 25,65 25,25' fill='%239b6125'/%3E%3Cpolygon points='25,25 65,25 60,30 30,30' fill='%23854c16'/%3E%3Cpolygon points='65,25 65,65 60,60 60,30' fill='%239b671c'/%3E%3Cpolygon points='25,65 65,65 60,60 30,60' fill='%23a06f20'/%3E%3Cpolygon points='25,25 25,65 30,60 30,30' fill='%23773f12'/%3E%3C/g%3E%3C/svg%3E") 30 / 24px / 0 stretch;
}
*/
/* Images + video: no hover scale (removed). */
/* The CD scales UP on hover like the sticky note (.nuke-post-doodle) — not a translate lift. will-change
   is OFF on the wrapper so the scale re-renders the thin keyline crisply instead of resampling a pinned
   layer (which was what lightened it). */
.nuke-container .nuke-post-music-cd:hover {
  transform: scale(1.006);
}
/* CD hover: scale only (above) — no drop shadow. */
/* Pressed — flatter shadow (the scale press itself is the JS animation). The CD is excluded: it has no
   press shadow (all its lift shadows were removed). */
.nuke-container .nuke-post-media-1 .nuke-post-img-wrap:active:not(.is-sticker),
.nuke-container .nuke-post-video-embed:active {
  box-shadow: var(--card-lift-press);
}
/* Sticker (transparent-bg image, tagged by markStickerIfTransparent): no framed
   container — drop the border, radius, gray placeholder fill + shimmer so the
   image sits bare in the post and its transparency shows the post behind it.
   Sizing (width + aspect-ratio) is unchanged. */
.nuke-container .nuke-post-media-1 .nuke-post-img-wrap.is-sticker {
  border: none;
  border-radius: 0;
  background: transparent;
  box-shadow: none;
}
.nuke-container .nuke-post-img-wrap.is-sticker::before {
  display: none;
}
/* Multi-image grid (2/3/4, +N on overflow). */
.nuke-container .nuke-post-media-grid {
  display: grid;
  gap: 2px;
  width: 100%;
  max-width: 380px;
  aspect-ratio: 16 / 9;
  position: relative;
  /* Squared, uniform 2px border all round (--card-border). */
  border-radius: 0;
  overflow: hidden;
  border: 2px solid var(--card-border);
}
.nuke-container .nuke-post-media-2 { grid-template-columns: 1fr 1fr; }
.nuke-container .nuke-post-media-3 { grid-template-columns: 1fr 1fr; grid-template-rows: 1fr 1fr; }
.nuke-container .nuke-post-media-3 .nuke-post-grid-cell:first-child { grid-row: 1 / span 2; }
.nuke-container .nuke-post-media-4 { grid-template-columns: 1fr 1fr; grid-template-rows: 1fr 1fr; }
.nuke-container .nuke-post-grid-cell { background: var(--card-border); } /* grid gap-lines match the border */
.nuke-container .nuke-post-media-grid .nuke-chat-img-wrap { border-radius: 0; }
.nuke-container .nuke-post-grid-more {
  position: absolute;
  inset: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  background: rgba(0, 0, 0, 0.42);
  color: #FFFFFF;
  font-size: calc(22px - var(--font-shrink, 0px));
  font-weight: 700;
  z-index: 3;
}
/* Reply wrapper — holds the arrow OUTSIDE the scaling quote box so the arrow
   doesn't move/scale with the hover/press animation. Carries the flow spacing. */
.nuke-container .nuke-post-reply {
  position: relative;
  margin-top: 8px;
}
/* Quote-tweet box (replies). */
.nuke-container .nuke-post-quote {
  position: relative;
  /* Tinted by the QUOTED user — the person being replied to: --card-bg/--card-border derive from
     THEIR colour (--nuke-quote-hl), the same treatment a message card gets from its author. Same
     --card-border bevel, white inner top-left border, squared, + the top-half sheen. Carries the
     dot texture too, at the SAME opacity as the message card (0.025) — the full nuke-post-texture. */
  /* COLOR SETTING 2, same as the message card, but from the QUOTED user's hue
     (--nuke-quote-raw); colourless quote → invalid → neutral --nk-hover-1. */
  --card-bg-themed: oklch(from var(--nuke-quote-raw) var(--set-2-l) var(--set-2-c) h);
  --card-bg: var(--card-bg-themed, var(--nk-hover-1));
  /* COLOR SETTING 3 (border) from the quoted user's hue → reply box border + its pfp ring. */
  --card-border-themed: oklch(from var(--nuke-quote-raw) var(--set-3-l) var(--set-3-c) h);
  --card-border: var(--card-border-themed, var(--nk-border-plain));
  /* COLOR SETTING 7 (body text) from the quoted user's hue → reply body text. */
  --body-text-themed: oklch(from var(--nuke-quote-raw) var(--set-7-l) var(--set-7-c) h);
  /* SETTING 4 (muted) → reply timestamp; SETTING 6 → reply icons (--icon-themed). */
  --set4-themed: oklch(from var(--nuke-quote-raw) var(--set-4-l) var(--set-4-c) h);
  --icon-themed: oklch(from var(--nuke-quote-raw) var(--set-6-l) var(--set-6-c) h);
  /* Reply HOVER, toned to match — an OKLCH step (L.910/C.032) below the reply's resting fill, from the
     QUOTED user's hue (--nuke-quote-raw); colourless quote → neutral light grey. */
  --card-hover-themed: oklch(from var(--nuke-quote-raw) 0.910 0.032 h);
  --card-hover: var(--card-hover-themed, color-mix(in oklab, #808080 9%, var(--nk-surface)));
  /* Reply: FILL + inner white 1px bevel kept; no sheen, no dot pattern, no DROP shadow (the lift vars
     neutralise the hover + active drop shadows too), squared. Border + hover tint kept. */
  --card-lift-strong: 2px 2px 0 0 rgba(0, 0, 0, 0.2);   /* pfp-hover-style hard offset shadow (animates away on :active) */
  --card-lift-press: 0 0 0 0 transparent;
  background-color: var(--card-bg);   /* fill kept — only the sheen + dot pattern were removed */
  border: var(--nk-panel-border-width) solid var(--card-border);
  border-bottom-width: 2px;
  border-right-width: 2px;
  box-shadow: inset 1px 1px 0 0 var(--card-bevel, #fff), var(--card-lift-strong);   /* white inner bevel + subtle drop */
  transition: transform 0.09s ease, box-shadow 0.18s ease, background-color 0.22s ease;
  transform-origin: center;
  border-radius: 0;
  padding: 8px 10px;
  cursor: pointer;
}
/* Guest / NPC (no account) cards are FLAT + colourless — no sheen, no pattern — matching a colour-removed
   account (they share the neutral --nk-hover-1 fill + --nk-border-plain edges). */
.nuke-container .nuke-post[data-has-account="false"],
.nuke-container .nuke-post-quote[data-has-account="false"] {
  background-image: none;
}
/* Old reply arrow, restored — flipped upside down, in the quote-border color,
   resting on the right edge of the reply avatar and pointing down at the quote. */
.nuke-container .nuke-post-reply-arrow {
  position: absolute;
  top: 8px;
  left: -24px;
  /* Matches the host message card's border colour (--card-border, inherited from the
     enclosing .nuke-post); neutral border as the fallback. */
  color: var(--card-border, var(--nk-border));
  width: 18px;
  height: 18px;
  font-size: calc(18px - var(--font-shrink, 0px));
  stroke-width: 2.67;   /* ~2px rendered (24 viewBox @ 18px) — reply-chain arrow. No text-stroke, so it
                           renders identically to the cred elbow (which never had one). */
  transform: scaleY(-1);
  pointer-events: none;
}
/* Reply keeps its resting tint when the post is hovered (like the static message card). */
.nuke-container .nuke-post:hover .nuke-post-quote,
.nuke-container .nuke-post.is-hovercard-active .nuke-post-quote { background-color: var(--card-bg); }
/* Hovering the reply darkens it subtly (--card-hover), same as the message hover. */
.nuke-container .nuke-post:hover .nuke-post-quote:hover,
.nuke-container .nuke-post-quote:hover {
  background-color: var(--card-hover);
}
/* Account reply hover — a smaller scale-up than the media (it's a nested, smaller card) + the shared
   down-right lift shadow (the white inner bevel is kept in both states). The on-click press
   (nukeCardPressDown) scales it in from this 1.003 hover rest, then rebounds. */
.nuke-container .nuke-post-quote:hover {
  transform: none;   /* no hover scale */
  box-shadow: inset 1px 1px 0 0 var(--card-bevel, #fff), var(--card-lift-strong);
}
.nuke-container .nuke-post-quote:active {
  box-shadow: inset 1px 1px 0 0 var(--card-bevel, #fff), var(--card-lift-press);
}
/* NPC reply: no hover lift/scale (rests at 1, matching nukeCardPressDown's npcReply branch) — bevel only. */
.nuke-container .nuke-post-quote[data-has-account="false"]:hover {
  transform: none;
  box-shadow: inset 1px 1px 0 0 var(--card-bevel, #fff), var(--card-lift);
}

/* Dark-mode lift shadows are handled by the theme-aware --card-lift / --card-lift-press variables
   (remapped to --nk-shadow-lift in the dark block), so no per-component dark overrides are needed. */
.nuke-container .nuke-post-quote-head {
  display: flex;
  align-items: center;
  min-width: 0;   /* let the header shrink so its children can truncate (date → handle → username) */
  /* 3px — match the feed/header/hovercard name rows so the reply's badge/handle
     hug the name too. */
  gap: 3px;
  /* 2px — pulls the reply body text UP 1px tighter under the pfp/header (was 3px). */
  margin-bottom: 2px;
  /* Tight line box so the name/@handle/time/balance text hugs its glyphs and
     centers cleanly against the 20px pfp (the inherited 15px text otherwise
     carries a tall line box that pushes the glyphs optically high). */
  line-height: 1;
}
.nuke-container .nuke-post-quote-pfp {
  width: 20px;
  height: 20px;
  margin: 0;
  flex: 0 0 auto;
}
.nuke-container .nuke-post-quote-pfp .cc-pfp { width: 100%; height: 100%; }
/* Tiny status dot scaled for the 20px quote pfp (vs 10px on the 40px post pfp). */
/* .nuke-post-quote-pfp dot: size + offset are proportional now (base .status-dot). */
/* SETTING 6 → reply username = the quoted user's own hue (--nuke-quote-raw); colourless → --nk-text. */
.nuke-container .nuke-post-quote-name { font-weight: 700; --un-6: oklch(from var(--nuke-quote-raw) var(--set-6-l) var(--set-6-c) h); color: var(--un-6, var(--nk-text)); }
/* Cred-vote RECEIVER row — a reply-style pfp + username shown as the first line of a cred post about
   another user (see credTargetRowHtml). Reuses .nuke-post-quote-pfp (20px) + .nuke-post-quote-name
   (bracketed, 700 weight); this rule just lays them out as a row above the message text. */
.nuke-container .nuke-post-cred-target {
  display: flex;
  align-items: center;
  gap: 3px;
  margin-top: 2px;      /* nudge down off the post author name above */
  margin-bottom: 3px;
}
/* The interacted user's pfp + name are click targets → their profile. Hovering EITHER underlines the
   name (the pfp underline is surfaced via :has() on the row). */
.nuke-container .nuke-post-cred-target .nuke-post-quote-pfp,
.nuke-container .nuke-post-cred-target .nuke-post-quote-name { cursor: pointer; }
.nuke-container .nuke-post-cred-target:has(.nuke-post-quote-pfp:hover) .nuke-post-quote-name,
.nuke-container .nuke-post-cred-target .nuke-post-quote-name:hover { text-decoration: underline; }
/* Reply arrow to the LEFT of the pfp — inline here (the reply quote pins it absolute at left:-24px). */
.nuke-container .nuke-post-cred-target .nuke-post-reply-arrow,
.nuke-container .nuke-post-cred-target .nuke-post-cred-elbow {
  position: static;
  width: 18px;
  height: 18px;
  flex: 0 0 auto;
  color: var(--card-border, var(--nk-border));   /* reply-chain colour — matches the host card border, like the reply arrow */
  stroke-width: 2.67;        /* ~2px rendered (24 viewBox @ 18px) — matches the reply-chain arrow */
}
/* Reaction icon between the elbow and arrow — our animated Noto emoji (😍 love / 😡 hate). The emoji
   <img> renders at 1em, so font-size matches it to the post-body emoji size (.nuke-post-text = 14px). */
.nuke-container .nuke-post-cred-target .nuke-post-cred-face {
  display: inline-flex;
  align-items: center;
  font-size: calc(14px - var(--font-shrink, 0px));
  line-height: 1;
  flex: 0 0 auto;
}
/* The elbow glyph has ~5px of empty space on its right (its drawing ends at x=17 in the 24-unit box),
   so pull everything after it closer to the elbow with a negative right margin. */
.nuke-container .nuke-post-cred-target .nuke-post-cred-elbow {
  margin-right: -5px;
}
/* The arrow glyph has ~3px of empty space on its left; pull the arrow + everything after it (pfp, name)
   slightly closer to the sentiment word. */
.nuke-container .nuke-post-cred-target .nuke-post-reply-arrow {
  margin-left: -3px;
}
/* Sentiment word (LOVE! / HATE!) right of the reaction face — bold caps, muted to match the row. */
.nuke-container .nuke-post-cred-target .nuke-post-cred-word {
  font-size: calc(12px - var(--font-shrink, 0px));
  font-weight: 800;
  letter-spacing: 0.03em;
  line-height: 1;
  color: var(--nk-text-2);
  flex: 0 0 auto;
}
/* Per-kind colour of the sentiment WORD only: UP = bright green, DOWN = bright red. The elbow + arrow
   use the reply-chain colour (above); the emoji stays full-colour. */
.nuke-container .nuke-post-cred-target.is-up .nuke-post-cred-word { color: oklch(from #22C55E calc(l * 0.9) c h); }   /* directed-post word: bright green, slightly darkened */
.nuke-container .nuke-post-cred-target.is-down .nuke-post-cred-word { color: oklch(from #EF4444 calc(l * 0.9) c h); } /* directed-post word: bright red, slightly darkened */
/* Directed-post TARGET name (the user the arrow points at): a touch smaller than the post-body name, and
   truncates with … when the row is too narrow — same behaviour as the post-container username. min-width:0
   on the row lets it shrink so the name (already flex:0 1 auto + ellipsis) actually clips. */
.nuke-container .nuke-post-cred-target { min-width: 0; }
.nuke-container .nuke-post-cred-target .nuke-post-quote-name { font-size: calc(12.5px - var(--font-shrink, 0px)); }
/* Quoted sender's balance — pushed to the top-right of the quote header, bold
   like the per-message balance (inherits the quote header's font size). */
.nuke-container .nuke-post-quote-balance {
  margin-left: auto;
  flex-shrink: 0;
  padding-left: 8px;
  /* Match the chat balance weight (.nuke-post-balance is 800). */
  font-weight: 800;
  color: var(--nk-text);
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}
.nuke-container .nuke-post-quote-handle {
  /* Reply handle belongs to the QUOTED user → derive from --nuke-quote-raw (unset when colourless → #808080
     neutral, matching the post handle), NOT --nuke-cmd-color (which is the accent for /command tokens here). */
  color: oklch(from var(--nuke-quote-raw, #808080) clamp(0.3, l, 0.42) calc(c * 0.5) calc(h + 180));
  background: oklch(from var(--nuke-quote-raw, #808080) clamp(0.85, l, 0.9) calc(c * 0.5) calc(h + 180) / 0.5);
}
.nuke-container .nuke-post-quote-dot,
.nuke-container .nuke-post-quote-time { color: var(--set4-themed, var(--nk-text-2)); font-weight: 400; }
.nuke-container .nuke-post-quote-text { color: var(--body-text-themed, var(--nk-text-msg)); line-height: 1.3; overflow-wrap: anywhere; font-family: var(--font-conduit); font-size: calc(14px - var(--font-shrink, 0px)); }
/* Reply-quote body: the image (when the quoted message is an image) fills the
   full allocated width at its natural aspect ratio, with an optional caption
   stacked below it. */
.nuke-container .nuke-post-quote-body {
  display: flex;
  flex-direction: column;
  gap: 6px;
}
.nuke-container .nuke-post-quote-img {
  line-height: 0;
}
/* Lift the thumbnail off the quoted username ONLY when the image leads (image-only
   quote): the quote-head gives 3px, cramped for an image, so ~8px total (3px head +
   5px) matches the regular post's name→image gap. When a caption sits above the image
   the body `gap` already spaces them, so no extra top margin is added. */
.nuke-container .nuke-post-quote-body > .nuke-post-quote-img:first-child {
  margin-top: 5px;
}
.nuke-container .nuke-post-quote-img img {
  display: block;
  /* Fill the allocated width, but scale the WHOLE image down (never crop) when
     it would otherwise be taller than the cap — so a very tall image shrinks to
     `max-height` instead of taking over. Real images (wider than the quote)
     fill the width; a capped tall one sits left at its scaled-down size. */
  width: auto;
  height: auto;
  max-width: 100%;
  /* Matches TEST_IMG_MAX_H in chat-render.ts. High enough that square +
     landscape images fill the quote width (max-width:100% binds); only very
     tall portraits get scaled down to this height (never cropped). */
  max-height: 510px;
  /* 2px border in the card's own colour (--card-border — the quoted user's tint here), so it matches a
     regular image post's border instead of a flat neutral panel colour. */
  border-radius: 0;
  border: 2px solid var(--card-border);
}
/* Navigate-to-message flash (new structure) — a one-shot blue highlight that
   snaps on, holds, then fades out. Implemented as a keyframe ANIMATION (not an
   always-on background-color transition) so it doesn't delay the hover
   darkening — the post bg and reply quote now darken at the same instant. */
@keyframes nuke-msg-flash {
  0%   { background-color: rgba(var(--nk-accent-rgb), 0.14); }
  60%  { background-color: rgba(var(--nk-accent-rgb), 0.14); }
  100% { background-color: transparent; }
}
.nuke-container [data-msg-id].chat-msg-flash-border {
  animation: nuke-msg-flash 1s ease-out;
}
/* Scroll-to highlight (reply-quote / ticker-pill click). JS holds `nuke-post-hl`
   for the whole scroll, then swaps to `nuke-post-hl-out` to fade once the scroll
   settles — see flashScrollToNukePost. Colour is `--nuke-hl-color`, a soft tint of
   the scrolled-to user's profile colour set inline by JS; with no profile colour
   (NPCs / no-colour users) it falls back to a neutral grey strong enough to read as
   a deliberate highlight. The fade's first keyframe matches the held colour so the
   swap is seamless. */
.nuke-container [data-msg-id].nuke-post-hl,
.nuke-container .nuke-post.nuke-post-hl {
  /* Pulse to the STRONG darken (--card-darken, 24% — same peak as the new-message flash); falls back
     to the JS-set soft tint for any non-card highlight target. */
  background-color: var(--card-darken, var(--nuke-hl-color, var(--nk-hl-soft)));
  transition: none; /* held solid while the scroll runs (not the hover fade) */
}
.nuke-container [data-msg-id].nuke-post-hl-out,
.nuke-container .nuke-post.nuke-post-hl-out {
  animation: nuke-msg-fadeout 1s ease-out; /* a gentle, toned-down version of the arrival ease */
}
@keyframes nuke-msg-fadeout {
  0%   { background-color: var(--card-darken, var(--nuke-hl-color, var(--nk-hl-soft))); }
  100% { background-color: var(--card-bg, transparent); } /* ease back to the card's resting tint */
}

/* ─── Reaction pop-in + emoji burst ────────────────────────────────
   `.nuke-reaction-badge` is the inline-flex container holding the
   emoji glyph + count. Positioned (relative) so the particle copies
   below can absolute-position from the badge's center.

   • `.nuke-reaction-pop` spring-scales a fresh badge from nothing
     to slightly oversized → settle (`nuke-reaction-pop` keyframe).
   • `.nuke-reaction-particle` is a JS-injected copy of the same
     emoji glyph that flies outward from the badge's center, using
     the inline `--dx` / `--dy` set by the spawner. Removed on
     `animationend` by the existing scroll-level listener.
   • `.nuke-reaction-bump` is a subtler count-only bounce for the
     "+1 same emoji" case — the count span scales 1 → 1.4 → 1
     without re-popping the whole badge. */
.nuke-container .nuke-reaction-badge {
  position: relative;
}
/* Glyph box — fixed 1em square that holds two layers:
   1. `.nuke-reaction-glyph-fallback` (unicode emoji, base layer,
      visible immediately so the pop is always visible);
   2. the animated-WebP emoji `<img>` (in the markup, autoplays on load,
      sits on top of the fallback and covers it visually).
   Both are absolutely positioned to fill the same 1em box so the
   transition is in-place — no swap, no layout shift, no flicker. */
.nuke-container .nuke-reaction-glyph {
  position: relative;
  display: inline-block;
  width: 1em;
  height: 1em;
  vertical-align: -0.2em;
  font-size: calc(14px - var(--font-shrink, 0px));
  line-height: 1;
  /* Skip PAINTING the animated-WebP reaction emoji while its post is off-screen (the footer's
     per-frame cost the message body's content-visibility didn't cover). The glyph keeps its fixed
     1em box, so nothing reflows; the particle burst (a sibling badge child) is NOT contained, so
     it's never clipped. */
  content-visibility: auto;
}
.nuke-container .nuke-reaction-glyph-fallback {
  position: absolute;
  inset: 0;
  display: grid;
  place-items: center;
  font-family: 'Noto Color Emoji', 'Apple Color Emoji', 'Segoe UI Emoji', sans-serif;
  font-variant-emoji: emoji;
  /* Slight scale-down so unicode visually matches the WebP emoji's
     drawn glyph (which has internal padding inside its 1em box). */
  font-size: 0.92em;
  line-height: 1;
  /* Longer (240ms) ease-out crossfade so any small color/size
     delta between the unicode fallback and the WebP emoji's first
     painted frame is hidden inside the fade rather than a hard cut. */
  transition: opacity 0.24s ease-out;
}
/* JS sets `data-glyph-ready="1"` on the glyph once the WebP emoji `<img>` has
   painted. Hide the unicode fallback so we don't see both layers stacked. */
.nuke-container .nuke-reaction-glyph[data-glyph-ready="1"] .nuke-reaction-glyph-fallback {
  opacity: 0;
}
.nuke-container .nuke-reaction-glyph img[data-noto-emoji] {
  position: absolute !important;
  inset: 0;
  width: 100% !important;
  height: 100% !important;
}
/* Lightbox-open: hide the chat's animated emoji so an animating WebP under the
   ~95%-white blurred overlay doesn't force the backdrop-filter to re-blur every
   frame. visibility:hidden = no paint = no backdrop cost (toggled by
   pause/resumeNukeChatLotties). */
.nuke-container #nuke-chat-scroll.nuke-chat-emoji-paused [data-noto-emoji] {
  visibility: hidden;
}
/* Particles use the same color-emoji font stack as the fallback. */
.nuke-container .nuke-reaction-particle {
  font-family: 'Noto Color Emoji', 'Apple Color Emoji', 'Segoe UI Emoji', sans-serif;
  font-variant-emoji: emoji;
}
.nuke-container .nuke-reaction-pop {
  animation: nuke-reaction-pop 0.36s cubic-bezier(0.34, 1.56, 0.64, 1);
  transform-origin: center;
}
.nuke-container .nuke-reaction-bump {
  display: inline-block;
  animation: nuke-reaction-count-bump 0.22s ease-out;
  transform-origin: center;
}
.nuke-container .nuke-reaction-particle {
  position: absolute;
  left: 50%;
  top: 50%;
  pointer-events: none;
  font-size: calc(10px - var(--font-shrink, 0px));
  line-height: 1;
  animation: nuke-reaction-particle-burst calc(var(--nuke-slam-dur, 0.42s) * 0.71) ease-out forwards;
}
@keyframes nuke-reaction-pop {
  0%   { transform: scale(0.2); opacity: 0; }
  55%  { transform: scale(1.35); opacity: 1; }
  78%  { transform: scale(0.92); }
  100% { transform: scale(1); }
}
@keyframes nuke-reaction-count-bump {
  0%   { transform: scale(1); }
  50%  { transform: scale(1.4); }
  100% { transform: scale(1); }
}
@keyframes nuke-reaction-particle-burst {
  0%   { transform: translate(-50%, -50%) scale(0.4); opacity: 1; }
  100% { transform: translate(calc(-50% + var(--dx, 0px)), calc(-50% + var(--dy, 0px))) scale(1); opacity: 0; }
}

/* React-slam: a real-time reaction swaps the matching like/dislike thumb FOR the reacted emoji.
   To keep it seamless (not a tiny glyph ballooning out of a dot), the thumb `<i>` is the WRAPPER
   that carries one shared scale "slam" — the thumb glyph (::before) and the emoji both ride it,
   so the handoff between them is a pure opacity CROSSFADE at a matched size + shared centre. The
   scale motion masks the swap: the eye reads one glyph popping and changing its face, then a
   recoil/settle, a hold, and a crossfade back to the thumb. Driven by playReactSlam(). */
.nuke-container .nuke-post-vote-btn.is-reacting {
  position: relative;
  /* The release REBOUND: the whole button (frame + thumb + emoji + count) springs back up out of
     the press-down (0.72) as one fluid motion. `scale: 1` keeps the glyph hover-pop out of the way;
     `transition: none` SNAPS the :active→is-reacting handoff (no tween fighting the keyframe) so the
     rebound starts cleanly at the pressed size — the press held 0.72, this picks it up from 0.72. */
  scale: 1;
  transition: none;
  animation: nuke-vote-rebound var(--nuke-slam-dur, 0.42s) linear both;
  z-index: 5; /* lift the slamming button + react emoji ABOVE the confetti (z4) for this beat */
}
/* During the slam the reaction COLOR is always live — the circle (background) + glyph color flash
   on, even on an already-voted thumb (whose hover colors are otherwise suppressed). The circle
   rides the slam scale, so it pops with the punch. */
.nuke-container .nuke-post-vote-up.is-reacting { background: rgba(255, 69, 0, 0.14); color: #FF4500; }
.nuke-container .nuke-post-vote-down.is-reacting { background: rgba(113, 147, 255, 0.18); color: #7193FF; }
/* The like/dislike COUNT carries its own gray color, so tint it to the reaction color during the
   slam too (it won't inherit the button's color otherwise). */
.nuke-container .nuke-post-vote-up.is-reacting .nuke-post-vote-count { color: #FF4500; }
.nuke-container .nuke-post-vote-down.is-reacting .nuke-post-vote-count { color: #7193FF; }
.nuke-container .nuke-post-vote-btn.is-reacting > i {
  position: relative;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  overflow: visible;
  /* Drop the thumb's hover/press scale during the reaction so the slam (carried by the whole
     button) is the ONLY scale in play — the morph reads the same whether or not the pointer is
     hovering. We KEEP the glyph's base scale transition (no `transition: none`) so that when the
     slam ends while the pointer is still over the button, the glyph EASES into the hover scale
     (1 → 1.1) instead of snapping — fixing the jank on hover-through-completion. The slam fires
     after the click releases, so the glyph is already ≈1 by then and the launch doesn't compound. */
  scale: 1;
}
/* Thumb glyph (phosphor ::before): crossfades OUT under the rising emoji, back IN as it leaves.
   No own scale — it inherits the wrapper's, so it never changes size relative to the emoji. */
.nuke-container .nuke-post-vote-btn.is-reacting > i::before {
  animation: nuke-vote-thumb-fade calc(var(--nuke-slam-dur, 0.42s) + 1s) ease-in-out both;
}
/* SVG thumb (the arrow IS the glyph now — no ::before): drop the hover scale during the slam and run
   the SAME crossfade OUT under the react emoji, back IN. (The > i::before rule above is legacy.) */
.nuke-container .nuke-post-vote-btn.is-reacting > svg {
  scale: 1;
  animation: nuke-vote-thumb-fade calc(var(--nuke-slam-dur, 0.42s) + 1s) ease-in-out both;
}
.nuke-container .nuke-vote-react {
  position: absolute;
  left: 50%;
  top: 50%;
  z-index: 3;
  pointer-events: none;
  font-size: calc(15px - var(--font-shrink, 0px));
  line-height: 1;
  transform: translate(calc(-50% + 0.5px), calc(-57% + 1px)); /* +0.5px to optically centre; -57% +1px sits it a touch above the thumb centre (the wrapper supplies the scale) */
  font-family: 'Noto Color Emoji', 'Apple Color Emoji', 'Segoe UI Emoji', sans-serif;
  font-variant-emoji: emoji;
  animation: nuke-vote-emoji-fade calc(var(--nuke-slam-dur, 0.42s) + 1s) ease-in-out both;
}
/* The morphed-in glyph is now an animated-WebP <img>. The global `img { max-width: 100% }` reset
   clamps it to its narrow shrink-to-fit parent (squishing it horizontally), so lift the clamp —
   the img's inline `width/height: 1em` then renders it square. */
.nuke-container .nuke-vote-react img {
  max-width: none;
}
/* Shared scale carried by the wrapper — scales the thumb AND the emoji identically.
   The RELEASE rebound: the press (`:active`) already slammed the button down to 0.72 and held it;
   on release this springs it back UP out of that pressed size, past full into a soft overshoot,
   then settles. Just the up-half of a slam — the down-half is the press. */
@keyframes nuke-vote-rebound {
  /* Picks up at the pressed size (0.72) so the handoff from :active is seamless, then SPRINGS up. A
     brisk decelerating launch into the overshoot (so the release feels snappy), easing-in-out into
     the settle (slope 0 at the overshoot + rest) so the spring lands softly — no snap. */
  0%  { transform: scale(0.72); animation-timing-function: cubic-bezier(0.18, 0.9, 0.32, 1); }    /* spring UP out of the pressed slam */
  52% { transform: scale(1.16); animation-timing-function: cubic-bezier(0.45, 0, 0.55, 1); }      /* soft overshoot → smooth settle */
  100%{ transform: scale(1); }                                                                    /* rest */
}
/* Crossfade thumb⇄emoji — runs on a MUCH longer timeline than the rebound (slam-dur + 1s) so the
   emoji LINGERS well after the scale has settled. The press held the THUMB; on release it swaps to
   the EMOJI almost immediately (≈2→7%, a quick matched-size morph as the spring launches), which
   then holds through the overshoot AND a long linger (to 90%), and only over the final ~10% smoothly
   turns back into the svg. */
@keyframes nuke-vote-thumb-fade {
  0%   { opacity: 1; }
  2%   { opacity: 1; }
  7%   { opacity: 0; }
  90%  { opacity: 0; }
  100% { opacity: 1; }
}
@keyframes nuke-vote-emoji-fade {
  0%   { opacity: 0; }
  2%   { opacity: 0; }
  7%   { opacity: 1; }
  90%  { opacity: 1; }
  100% { opacity: 0; }
}
/* Like/dislike CONFETTI. Pieces are normally appended to the clicked POST (so they ride its scroll
   + sit below the react emoji); this layer is the FALLBACK host when there's no post. Selectors are
   unscoped (the layer/pieces can live outside `.nuke-container`). Position + rotation + opacity are
   driven per-frame from JS (confettiTick); CSS only sets the chrome. `will-change` → smooth motion. */
.nuke-confetti-layer {
  /* Pinned over the chat PANEL (its parent .nuke-chat-container is position:relative), ABOVE the
     post-modal overlay (z40) so the burst shows on the /post + /profile subviews too. Not fixed —
     it lives inside the panel, so it's clipped only at the (far) panel edges, never the viewport. */
  position: absolute;
  inset: 0;
  pointer-events: none;
  overflow: visible;
  z-index: 60;
}
.nuke-confetti {
  position: absolute;
  left: 0;
  top: 0;
  z-index: 4;            /* above the post's content, but BELOW the slamming button + react emoji (z5) */
  pointer-events: none;
  line-height: 1;
  user-select: none;
  will-change: transform, opacity;
  font-family: 'Noto Color Emoji', 'Apple Color Emoji', 'Segoe UI Emoji', sans-serif;
  font-variant-emoji: emoji;
}

/* --- Disclaimer toast + lightbox blur -------------------------- */

/* Disclaimer toast dismiss animation — quick scale-down + fade out.
   `display: none` is applied via JS after the transition completes.
   Box-shadow set explicitly here (not via Tailwind `shadow-*`)
   because Tailwind's `ring-1` already drives the `box-shadow`
   property via its --tw-ring-shadow CSS variable — composing a
   second arbitrary shadow utility on top via --tw-shadow can fail
   silently when preflight isn't initializing those variables. A
   direct rule wins by being late + more specific. */
.nuke-container #nuke-chat-disclaimer {
  /* Temporarily hidden for now — remove this line to bring the Stay Safe
     toast back. */
  display: none;
  transition: opacity 0.1s ease-out, transform 0.1s ease-out;
  transform-origin: center;
  /* Matches the chat-bubble shadow exactly so the toast reads as
     the same chrome family. */
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
}
/* Image lightbox blur — written directly here (not via Tailwind's
   `backdrop-blur-*` utility) because Tailwind composes
   `backdrop-filter` from a chain of CSS variables that need
   preflight to initialize the unused ones to identity values.
   Preflight is disabled in this build, so a direct rule wins. */
.nuke-container #nuke-chat-lightbox {
  /* No backdrop blur — just a plain dark scrim (see .nuke-chat-lightbox bg). */
}
.nuke-container #nuke-chat-disclaimer.dismissing {
  opacity: 0;
  transform: scale(0.9);
  pointer-events: none;
}

/* --- Search row (header + input + icon + add-contact) ---------- */

/* Nuke page reuses the main app's search bar chrome but adopts the
   thinner height, italic gray placeholder font, and inline magnifying-
   glass icon from the original nuke design. The right-side glyph
   button holds the add-contact icon instead of the magnifying glass. */
.nuke-container .nuke-search-row {
  display: flex;
  align-items: center;
  gap: 4px;
  /* Fixed 38px height = the profile-tab ticker height, so the search bar
     matches it and centers in the 54px subheader (8px each side). */
  height: 38px;
}
.nuke-container .nuke-search-row .user-search-wrapper {
  flex: 1;
}
/* Add-contact button: perfect square 26×26 (matching the bar's full
   visual height — 24px box + 1px ring outside). Rounded 6px corners
   match the bar. The 16px icon centers via flex layout; with the 1px
   border, that leaves ~4px breathing room on each side. */
.nuke-container .nuke-search-row .nuke-search-add-btn {
  width: 24px;
  height: 24px;
  padding: 0;
  border-radius: 6px;
  flex-shrink: 0;
}
/* Add-contact button's icon — was `text-[#2B784A] text-base` inline. */
.nuke-container .nuke-search-add-icon {
  font-size: calc(18px - var(--font-shrink, 0px)); /* matches Tailwind text-base */
  color: #2B784A;
}

/* Adjacent contact-groups: zero the top padding on any group after
   the first so the gap between groups equals the offline group's
   bottom-edge padding (instead of double-stacking online's `pb-2`
   + offline's `pt-2`). */
.nuke-container .contact-group + .contact-group {
  padding-top: 0;
}

/* --- Contact group header (Online/Offline) --------------------- */

.nuke-container .contact-group-header .nuke-group-caret {
  font-size: calc(12px - var(--font-shrink, 0px));
  color: #555;
}
.nuke-container .contact-group-header .nuke-group-label {
  font-weight: 700;
  color: #333;
}
.nuke-container .contact-group-header:hover .nuke-group-label {
  text-decoration: underline;
}

/* --- Disclaimer toast body + close + chat footer + lightbox ---- */

/* Disclaimer body color + bg + radius (was inline Tailwind). */
.nuke-container #nuke-chat-disclaimer {
  background: #FFFFE1;
  color: #666;
  border-radius: 6px;
}
/* Keep the safety text clear of the top-right close (x) button. */
.nuke-container #nuke-chat-disclaimer p {
  padding-right: 12px;
  /* Closer to the chat message body (15px) instead of the small container base. */
  font-size: calc(14px - var(--font-shrink, 0px));
  line-height: 1.35;
}
/* Disclaimer close button glyph color + hover. */
.nuke-container .nuke-chat-disclaimer-close {
  color: #888;
}
.nuke-container .nuke-chat-disclaimer-close:hover {
  color: #333;
}
.nuke-container .nuke-chat-disclaimer-close .ri-close-line {
  font-size: calc(10px - var(--font-shrink, 0px));
}
/* Frosted chat header — mirrors the footer's chrome at the TOP of the chat.
   Empty for now; the chat scrolls behind it. */
.nuke-container .nuke-chat-header {
  position: absolute;
  top: 0;
  left: 0;
  right: 0;
  z-index: 5;
  /* Background = the logged-in user's BORDER colour (SETTING 3, --nk-border-themed), matching the left nav;
     falls back to --nk-border (the SAME neutral the posts use) when there is no colour. */
  background: var(--nk-border-themed, var(--nk-border));
  /* ONLY the light-gray PANEL FRAME here (like the stream's outer panel border). The white highlight +
     colour edge come from the LCD player (.nuke-radio-bar-info) INSIDE — which already carries the exact
     stream-screen bevel — so outer→inner reads gray, white, colour, matching the stream. (The LCD sits
     flush against this frame because .nuke-radio-bar drops its side padding.) */
  border-left: 1px solid var(--nk-border-2);
  border-right: 1px solid var(--nk-border-soft);
  -webkit-backdrop-filter: blur(8px);
  backdrop-filter: blur(8px);
  display: flex;
  flex-direction: column;
  align-items: stretch;
  /* Collapse-on-scroll (see initNukeChatHeaderCollapse): scrolling DOWN pushes the header up 1:1 with
     the feed scroll (inline transform per rAF frame, NO transition — glued to the content). Scrolling UP
     pops it fully back in on a short inline transition (snap-reveal, not proportional). Both transforms
     are set from JS, so none is declared here. .nuke-chat-container (overflow:hidden) clips it once up. */
  will-change: transform;
}
/* Top row of the pinned header — radio bar + feed-filter + back button (the former standalone
   .nuke-chat-header layout). 60px tall, centered row; the identity/contribute row sits below it. */
.nuke-container .nuke-chat-header-top {
  display: flex;
  align-items: center;
  height: 42px;   /* = the radio bar height, so no top/bottom space around it */
}
/* Back button on the top header's LEFT — only shown on a profile page. */
.nuke-container .nuke-chat-back-btn {
  display: none;
  align-items: center;
  justify-content: center;
  width: 32px;
  height: 32px;
  margin-left: 8px;
  border: none;
  border-radius: 9999px;
  background: transparent;
  color: var(--nk-text);
  font-size: calc(18px - var(--font-shrink, 0px));
  line-height: 1;
  cursor: pointer;
}
.nuke-container .nuke-chat-back-btn:hover {
  background: rgba(0, 0, 0, 0.06);
}
/* No top header on a profile page — the banner runs to the very top. */
.nuke-container[data-profile-view] .nuke-chat-header {
  display: none;
}
/* x.com-style feed tabs filling the top header — 3 equal-width labels; the active
   tab gets a centered, text-width underline bar. Scaffold (no click wiring yet).
   Hidden in profile view, where the header shows the back button instead. */
.nuke-container .nuke-feed-tabs {
  display: flex;
  /* The header is itself a flex row (for the back button), so grow to fill its
     full width — otherwise this shrinks to content and the tabs bunch up. */
  flex: 1 1 auto;
  height: 100%;
}
.nuke-container[data-profile-view] .nuke-feed-tabs {
  display: none;
}
.nuke-container .nuke-feed-tab {
  flex: 1 1 0;
  position: relative;
  display: flex;
  align-items: center;
  justify-content: center;
  height: 100%;
  border: none;
  background: none;
  cursor: pointer;
  color: var(--nk-text-2);
  font-family: inherit;
  font-size: calc(15px - var(--font-shrink, 0px));
  font-weight: 500;
}
.nuke-container .nuke-feed-tab-label {
  position: relative;
  display: inline-flex;
  align-items: center;
  height: 100%;
}
/* Merged Likes/Dislikes profile tab: the label + a sort arrow that flips between
   them. The base .nuke-feed-tab is already flex-centered, so the arrow just sits
   inline after the label with a small gap. */
.nuke-container .nuke-profile-tab-react { gap: 3px; }
.nuke-container .nuke-profile-react-sort {
  display: inline-flex;
  align-items: center;
  font-size: calc(11px - var(--font-shrink, 0px));
  line-height: 0;
  opacity: 0.7;
  transition: opacity 0.12s ease;
}
.nuke-container .nuke-profile-tab-react:hover .nuke-profile-react-sort,
.nuke-container .nuke-profile-tab-react.is-active .nuke-profile-react-sort { opacity: 1; }
/* Active tab — dark bold label + the underline bar pinned to the header's bottom
   edge, spanning just the label (text) width. */
.nuke-container .nuke-feed-tab.is-active {
  color: var(--nk-text);
  font-weight: 700;
}
.nuke-container .nuke-feed-tab.is-active .nuke-feed-tab-label::after {
  content: '';
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  height: 4px;
  border-radius: 9999px;
  background-color: var(--nk-accent);
}
/* ── Feed content filters — funnel button (right of the feed tabs) + its dropdown ──────────────────
   The button toggles a dropdown of show/hide toggles (wired in nuke/index.ts). Hidden in profile view
   like the tabs. Highlights (accent) whenever a filter is hiding content. */
.nuke-container .nuke-feed-filter-btn {
  flex: 0 0 auto;
  display: flex;
  align-items: center;
  justify-content: center;
  width: 34px;
  height: 34px;
  margin: 0 6px 0 2px;
  border: none;
  border-radius: 9999px;
  background: transparent;
  color: var(--nk-text-2);
  font-size: calc(17px - var(--font-shrink, 0px));
  line-height: 1;
  cursor: pointer;
  transition: background 0.12s ease, color 0.12s ease;
}
.nuke-container .nuke-feed-filter-btn:hover { background: var(--nk-hover-1); color: var(--nk-text); }
.nuke-container .nuke-feed-filter-btn.has-active-filter { color: var(--nk-accent); }
.nuke-container[data-profile-view] .nuke-feed-filter-btn { display: none; }
/* Dropdown panel — mounts on <body> (fixed, positioned under the button), so it inherits the --nk-*
   theme tokens from body.page-nuke but isn't clipped by the header. */
.nuke-feed-filter-menu {
  position: fixed;
  z-index: 60;
  min-width: 224px;
  padding: 6px;
  border: 1px solid var(--nk-border);
  border-radius: 0;
  background: var(--nk-surface);
  box-shadow: 0 10px 34px rgba(0, 0, 0, 0.24);
  color: var(--nk-text);
  font-family: var(--font-conduit);
}
.nuke-feed-filter-title {
  padding: 6px 10px 4px;
  font-size: calc(11.5px - var(--font-shrink, 0px));
  font-weight: 700;
  letter-spacing: 0.03em;
  text-transform: uppercase;
  color: var(--nk-text-2);
}
.nuke-feed-filter-row {
  display: flex;
  align-items: center;
  gap: 10px;
  width: 100%;
  padding: 8px 10px;
  border: none;
  border-radius: 10px;
  background: transparent;
  color: inherit;
  font-family: inherit;
  text-align: left;
  cursor: pointer;
}
.nuke-feed-filter-row:hover { background: var(--nk-hover-1); }
.nuke-feed-filter-check {
  flex: 0 0 auto;
  display: flex;
  align-items: center;
  justify-content: center;
  width: 20px;
  height: 20px;
  border: 1.5px solid var(--nk-border);
  border-radius: 6px;
  color: #fff;
  font-size: calc(13px - var(--font-shrink, 0px));
}
.nuke-feed-filter-check i { opacity: 0; }
.nuke-feed-filter-row[aria-checked="true"] .nuke-feed-filter-check {
  background: var(--nk-accent);
  border-color: var(--nk-accent);
}
.nuke-feed-filter-row[aria-checked="true"] .nuke-feed-filter-check i { opacity: 1; }
.nuke-feed-filter-text { display: flex; flex-direction: column; min-width: 0; line-height: 1.25; }
.nuke-feed-filter-label { font-size: calc(14px - var(--font-shrink, 0px)); font-weight: 600; }
.nuke-feed-filter-desc { font-size: calc(11.5px - var(--font-shrink, 0px)); color: var(--nk-text-2); }
/* Header "Feed" button — replaces the old feed tabs; opens the feed-filter dropdown (checkbox
   toggles). The caret tints to the accent when a filter is active (mirrors the old funnel's highlight). */
.nuke-container .nuke-feed-sort-btn {
  flex: 0 0 auto;
  display: flex;
  align-items: center;
  justify-content: center;
  width: 34px;
  height: 34px;
  margin-left: 10px;
  padding: 0;
  border: none;
  border-radius: 9px;
  background: transparent;
  color: var(--nk-text-2);
  cursor: pointer;
  transition: background 0.12s ease;
}
.nuke-container .nuke-feed-sort-btn:hover { background: var(--nk-hover-1); }
.nuke-container .nuke-feed-sort-icon { width: 20px; height: 20px; font-size: calc(20px - var(--font-shrink, 0px)); line-height: 1; }
.nuke-container .nuke-feed-sort-btn.has-active-filter .nuke-feed-sort-icon { color: var(--nk-accent); }
/* Second frosted header — sits directly below the first, twice its height
   (104px). Holds player actions; the chat scrolls behind it too. */
.nuke-container .nuke-chat-header-2 {
  /* Bottom row of the pinned header (merged in from the old scroll header). `relative` keeps it the
     containing block for its absolutely-positioned balance + stake/unstake btns. */
  position: relative;
  flex-shrink: 0;
  height: 104px;
  box-sizing: border-box;
  /* Styled EXACTLY like a .nuke-post card: self-tinted fill (--current-user-color at colour SETTING 2),
     white 1px top/left bevel edges, --card-border 2px bottom / 1px right, squared, flat (no shadow). */
  --nuke-cmd-color: var(--current-user-color);
  --card-bg-themed: oklch(from var(--nuke-cmd-color) var(--set-2-l) var(--set-2-c) h);
  --card-bg: var(--card-bg-themed, var(--nk-hover-1));
  --card-border-themed: oklch(from var(--nuke-cmd-color) var(--set-3-l) var(--set-3-c) h);
  --card-border: var(--card-border-themed, var(--nk-border-plain));
  background-color: var(--card-bg);
  background-image: none;
  border: none;
  border-top: 1px solid #FFFFFF;
  border-left: 1px solid #FFFFFF;
  border-bottom: 2px solid var(--card-border);
  border-right: 1px solid var(--card-border);
  border-radius: 0;
  box-shadow: none;
  /* Player-actions content is laid out like a post: avatar gutter + content, same 12px gap + 12px/16px padding. */
  display: flex;
  align-items: flex-start;
  gap: 12px;
  padding: 12px 16px;
}
/* Third header — sits directly below header-2 (52 + 104 = 156px) and holds the
   recent-actions pill ticker (moved here from the right-column page header).
   Transparent (no fill, blur, or border) so the pills float over the chat. */
/* Profile page view (`/~handle`): the body holds the X.com-style profile page.
   The identity header-2 is hidden (the page IS the identity), and the banner runs
   to the very top — scrolling UNDER the sticky back-button header (header-1). */
.nuke-container[data-profile-view] .nuke-chat-header-2 { display: none; }
.nuke-container[data-profile-view] #nuke-chat-disclaimer { display: none; }

/* ── Single-post modal (`/~handle/<postId>`) ─────────────────────────────────
   Conceptually a modal, but styled like a full PANEL TAKEOVER (the old X-style
   "open post" page): an opaque overlay mounted inside the chat panel that covers
   it with a "← Post" bar + the single post. Because it's only an overlay, the
   feed underneath is never re-rendered → its scroll position is kept. The panel's
   own rounded overflow:hidden clips these corners. */
.nuke-container .nuke-post-modal-overlay {
  position: absolute;
  inset: 0;
  z-index: 40;
  display: flex;
  flex-direction: column;
  /* Transparent canvas — the chat panel background (now the same textured page bg) shows through. */
  background: transparent;
  /* Smoothly fade the profile/post page background when the theme colour changes — e.g. opening a profile
     (built with your colour, then re-themed to theirs) or navigating profile → profile. */
  transition: background-color 0.4s ease;
  /* Side frame MOVED onto the subview's own elements (nav bars + profile info section) as their left/right
     borders — the overlay no longer frames its edges. */
  border-radius: 0;
  overflow: hidden;
}
/* Top bar — a back arrow + "Post" (same as the prior detail page). A column so a
   second row (the notifications sorting strip) can stack beneath the top row. */
.nuke-container .nuke-post-modal-bar {
  display: flex;
  flex-direction: column;
  flex-shrink: 0;
}

/* ── Subview header BACK + TITLE hosted in the PAGE HEADER (aligned over the centre chat panel) ─────────
   For /post /profile /notif /season /info, the back arrow + title live in the fixed page header
   (#nuke-page-subview-bar in index.html) — the NAVIGATION tabs stay in their own bar inside the overlay.
   Positioned over the centre chat panel (same left/width), transparent (shows the header's profile-colour
   fill), WHITE content like the logo. Hidden until a subview opens (JS toggles [hidden] + [data-subview]). */
.nuke-container .nuke-page-header .nuke-page-subview-bar {
  position: absolute;
  top: 0;
  bottom: 0;
  left: calc(50% - var(--trio-shift) - var(--chat-w) / 2);   /* centre chat panel left */
  width: var(--chat-w);                                      /* centre chat panel width */
  display: flex;
  flex-direction: column;
  justify-content: center;
  box-sizing: border-box;
}
.nuke-container .nuke-page-header .nuke-page-subview-bar[hidden] { display: none; }
/* bar-top fills the header height; white back arrow + title on the dark header fill. */
.nuke-container .nuke-page-header .nuke-page-subview-bar .nuke-post-modal-bar-top { height: 100%; }
/* The header back control is a labelled SQUARE button ("< Back") sitting in the nav column where the logo
   was. Outlined in white so it reads as a button on the dark header fill; auto width to fit the label. */
.nuke-container .nuke-page-header .nuke-page-subview-bar .nuke-post-modal-close {
  width: auto;
  height: 22px;   /* match the profile-tab nav buttons */
  padding: 0 11px 0 8px;
  gap: 5px;
  /* Background = a BRIGHT, highly-saturated take on the handle pill's INVERTED profile hue
     (.nuke-post-quote-handle rotates the hue by calc(h + 180)); here we keep that inversion but
     force high lightness + chroma so it reads as a vivid chip. Colourless (--current-user-color
     unset) → the themed var is invalid → neutral light-grey fallback. Black outline + black text. */
  --back-btn-bg-themed: oklch(from var(--current-user-color) 0.82 0.16 calc(h + 180));
  border: 1px solid #000;
  border-radius: 0;
  background: var(--back-btn-bg-themed, #E0E0E0);
  box-shadow: 4px 4px 0 0 rgba(0, 0, 0, 0.2);   /* hard offset shadow — same as .nuke-side-box */
  color: #000;
  font-family: var(--font-conduit);
  font-size: calc(13px - var(--font-shrink, 0px));   /* match the profile-tab nav buttons */
  transition: transform 0.12s ease, box-shadow 0.12s ease;
}
.nuke-container .nuke-page-header .nuke-page-subview-bar .nuke-post-modal-close:hover { transform: scale(1.03); box-shadow: 6px 6px 0 0 rgba(0, 0, 0, 0.2); }
/* Press: the hard shadow SUCKS IN (button pushes flush) — pairs with the scale-down press. :active after
   :hover so it wins while held; the box-shadow transition eases it in/out. */
.nuke-container .nuke-page-header .nuke-page-subview-bar .nuke-post-modal-close:active { box-shadow: 1px 1px 0 0 rgba(0, 0, 0, 0.2); }
.nuke-container .nuke-page-header .nuke-page-subview-bar .nuke-post-modal-close-label { font-size: calc(13px - var(--font-shrink, 0px)); font-weight: 500; line-height: 1; }
/* Arrow: stroke-width 2 (overrides the SVG's attribute) so its thickness matches the profile nav icons. */
.nuke-container .nuke-page-header .nuke-page-subview-bar .nuke-post-modal-close-arrow { width: 15px; height: 15px; stroke-width: 2; transform: scaleX(-1); }
.nuke-container .nuke-page-header .nuke-page-subview-bar .nuke-post-modal-title,
.nuke-container .nuke-page-header .nuke-page-subview-bar .nuke-post-modal-title-name { color: #fff; }
.nuke-container .nuke-page-header .nuke-page-subview-bar .nuke-post-modal-title-sub { color: rgba(255, 255, 255, 0.72); }
/* Post + Notifications header titles — the Marlboro display face, white, at the nav-item size (matches
   the brand wordmark + left nav). Other subviews (profile shows a name, season/info) keep their look. */
.nuke-container .nuke-page-header .nuke-page-subview-bar[data-subview="post"] .nuke-post-modal-title,
.nuke-container .nuke-page-header .nuke-page-subview-bar[data-subview="notif"] .nuke-post-modal-title {
  font-family: var(--font-marlboro);
  font-size: calc(32px - var(--font-shrink, 0px));
  font-weight: 400;
  line-height: 1;   /* match the wordmark's line-height so the two sit at the SAME vertical position */
  color: #fff;
  /* Same downward nudge as the wordmark — Marlboro's caps sit high in the em box. */
  transform: translateY(0.09em);
}
/* NOTIF now shows the back button like the other subviews — it closes back to the feed via onSubviewBack(). */
/* On /post + /profile (any subview WITH a back button), the back arrow REPLACES the brand logo: the logo
   hides, and the back button moves out to the nav-column spot (same left as the logo). The title then sits
   alone at the centre-panel left instead of after the arrow. */
.nuke-container .nuke-page-header:has(.nuke-page-subview-bar:not([hidden])) .nuke-nav-logo-item {
  display: none;
}
.nuke-container .nuke-page-header .nuke-page-subview-bar .nuke-post-modal-close {
  position: absolute;
  left: calc(24px - var(--col-gap) - var(--nav-col-w));
  top: 0;
  bottom: 0;
  margin: auto 0;
}
/* PROFILE: title stacks the viewed user's name over the post-count; docked Edit/Add-friend pinned right,
   revealed once the in-content button scrolls under the header (data-action-docked, set in index.ts). */
.nuke-container .nuke-page-header .nuke-page-subview-bar[data-subview="profile"] .nuke-post-modal-title {
  flex-direction: column;
  align-items: flex-start;
  justify-content: center;
  gap: 0;
}
.nuke-container .nuke-page-header .nuke-page-subview-bar[data-subview="profile"] .nuke-post-modal-title-name {
  font-size: calc(17px - var(--font-shrink, 0px));
  line-height: 1.25;
}
/* Docked Edit/Add-friend action DISABLED — the button no longer moves into the header on scroll
   (.nuke-profile-docked-action stays display:none regardless of [data-action-docked]). */
/* One robust clip for the whole profile overlay — kills a ~1px backdrop-filter leak at the panel
   edges. clip-path clips the entire subtree (incl. the frosted bar's backdrop) to the panel shape
   (square top now that the panel reaches the page top). */
.nuke-container .nuke-post-modal-overlay[data-subview="profile"] {
  clip-path: inset(0);
}
/* Profile subview: the nav bar FLOATS over the scrolling profile (banner + content scroll UNDER it). It
   now holds ONLY the docked Post/Media/Wallet tab nav (back + title moved to the page header), so it's
   TRANSPARENT at rest — invisible until the in-content nav scrolls up and docks, when it turns solid. */
.nuke-container .nuke-post-modal-overlay[data-subview="profile"] .nuke-post-modal-bar {
  position: absolute;
  top: 0;
  left: 0;
  right: 0;
  z-index: 5;
  background: transparent;
  border-radius: 0;
}
/* When the nav docks, the bar turns into a clean opaque strip in the user's themed PAGE bg (SETTING 1),
   so the docked tabs read on a solid background (matching the profile canvas). Colourless → --nk-surface. */
.nuke-container .nuke-post-modal-overlay[data-subview="profile"][data-tabs-docked="true"] .nuke-post-modal-bar {
  /* Match the post CONTAINERS: SETTING-2 fill; REAL side borders carry the panel FRAME, the card's own
     white-left / setting-3-right retained just inside as inset shadows. */
  background: var(--self-card-fill);
  border-left: 1px solid var(--nk-border-2);
  border-right: 1px solid var(--nk-border-soft);
  box-shadow: inset 1px 0 0 0 #FFFFFF, inset -1px 0 0 0 var(--nk-border);
}
/* The back-bar now lives in the PAGE HEADER (above the panel), not floating over this body — so the
   banner starts at the very top of the panel (no top padding to reserve). Targets the body by ID to win
   the specificity race against the base `#nuke-post-modal-body { padding: 8px 10px 0 }`. */
.nuke-container .nuke-post-modal-overlay[data-subview="profile"] #nuke-post-modal-body {
  padding-top: 0;
  /* A little breathing room at the bottom (the Stake/Unstake footer was removed). */
  padding-bottom: 16px;
  /* Clip the SCROLLING content to over-rounded top corners. clip-path (not just
     border-radius + overflow) reliably clips a scroll container's content past the
     backdrop-filter bar; the generous radius over-crops so nothing reaches the top border. */
  border-radius: 0;
  clip-path: inset(0);
}
.nuke-container .nuke-post-modal-bar-top {
  display: flex;
  align-items: center;
  gap: 12px;
  flex-shrink: 0;
  /* Match the feed's nav header (`.nuke-chat-header`): same 60px height, and the
     8px left inset lines the back arrow up with the nav header's back button. */
  height: 60px;
  padding: 0 8px;
}
/* Notifications sorting strip — hidden for post/profile; the notif view turns it
   on (see the `[data-subview="notif"]` rules below). */
.nuke-container .nuke-notif-tabs { display: none; }
.nuke-container .nuke-post-modal-close {
  width: 32px;
  height: 32px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  border: none;
  border-radius: 9999px;
  background: transparent;
  color: var(--nk-text);
  font-size: calc(18px - var(--font-shrink, 0px));
  cursor: pointer;
}
.nuke-container .nuke-post-modal-close:hover { background: rgba(0, 0, 0, 0.06); }
.nuke-container .nuke-post-modal-title {
  font-weight: 700;
  font-size: calc(17px - var(--font-shrink, 0px));
  color: var(--nk-text);
  display: inline-flex;
  align-items: center;
  gap: 6px;
  min-width: 0;
  overflow: hidden;
  white-space: nowrap;
}
/* Profile bar title name: matches `.nuke-profile-page-name` (the name by the pfp)
   so the header username reads identically. Kept in its own span so the title's
   flex gap sits only between the name and the level icon. */
.nuke-container .nuke-post-modal-title-name {
  font-size: calc(20px - var(--font-shrink, 0px));
  font-weight: 800;
  line-height: 1.2;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
}
/* Level icon next to the viewed user's name in the profile bar title. */
.nuke-container .nuke-post-modal-title-icon {
  width: 20px;
  height: 20px;
  object-fit: contain;
  flex-shrink: 0;
}
/* Profile back-bar title: X.com-style stack — the viewed user's name on top, post count below. */
.nuke-container .nuke-post-modal-overlay[data-subview="profile"] .nuke-post-modal-title {
  flex-direction: column;
  align-items: flex-start;
  justify-content: center;
  gap: 0;
}
.nuke-container .nuke-post-modal-overlay[data-subview="profile"] .nuke-post-modal-title-name {
  font-size: calc(17px - var(--font-shrink, 0px));
  line-height: 1.25;
}
/* Profile back-bar title TOP LINE — name + flag + cred badge on ONE row (the title column stacks this
   line over the post-count sub); without this wrapper the flag/badge became column children under the name. */
.nuke-container .nuke-post-modal-title-line {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  min-width: 0;
  max-width: 100%;
}
/* Cred badge in the header — smaller than the title name, nudged down + given a little left air (it read
   too large, too high and too tight against the name). */
.nuke-container .nuke-post-modal-title-line .nuke-profile-page-name-badge {
  width: 14px;
  height: 14px;
  margin-left: 2px;
  position: relative;
  top: 1px;
}
.nuke-container .nuke-post-modal-title-sub {
  font-size: calc(13px - var(--font-shrink, 0px));
  font-weight: 400;
  line-height: 1.2;
  color: var(--nk-text-2);
}
/* Docked action button (Edit profile / Add-friend) in the profile header — a copy of the identity-row
   button, pinned right (opposite the back arrow) and revealed once the in-content button scrolls up
   under the header (data-action-docked, set on scroll in index.ts). */
.nuke-container .nuke-profile-docked-action {
  margin-left: auto;
  /* Right edge lines up with the identity-row button: that sits 16px from the panel edge
     (.nuke-profile-page-id padding), while the bar-top only pads 8px — so add the missing 8px. */
  margin-right: 8px;
  flex-shrink: 0;
  display: none;
}
/* (The docked action now lives in the page-header bar — see the .nuke-page-subview-bar rule above.) */
/* When the opened post is mid-chain (its leading post carries the up-connector —
   the previous chain message is cut off above), the bar gets a bottom border so
   that up-line reads as continuing into the header. Otherwise the bar is borderless. */
.nuke-container .nuke-post-modal-overlay:has(.nuke-post-modal-body > .nuke-post-thread-above:first-child) .nuke-post-modal-bar {
  border-bottom: var(--nk-panel-border-width) solid var(--nk-border-soft);
}

/* ── Notifications view header ── No back button; a taller (104px) bordered
   header: the "Notifications" label row on top, then a feed-style sorting strip
   (All / Priority / Mentions) below it. */
.nuke-container .nuke-post-modal-overlay[data-subview="notif"] .nuke-post-modal-close { display: none; }
.nuke-container .nuke-post-modal-overlay[data-subview="notif"] .nuke-post-modal-bar {
  /* Match the post CONTAINERS below: SETTING-2 card fill + the card's white-left / setting-3-right side
     borders. The REAL side borders carry the panel FRAME (moved off the overlay); the card's own side
     borders are retained just inside as inset shadows — no width change. */
  background: var(--self-card-fill);
  border-bottom: 2px solid var(--nk-border);
  border-left: 1px solid var(--nk-border-2);
  border-right: 1px solid var(--nk-border-soft);
  box-shadow: inset 1px 0 0 0 #FFFFFF, inset -1px 0 0 0 var(--nk-border);
}
/* No back arrow → inset the label to 16px so it lines up with the list below. */
.nuke-container .nuke-post-modal-overlay[data-subview="notif"] .nuke-post-modal-bar-top {
  padding: 0 16px;
}
.nuke-container .nuke-post-modal-overlay[data-subview="notif"] .nuke-post-modal-title {
  font-size: calc(20px - var(--font-shrink, 0px));
  font-weight: 800;
}
/* The sorting strip: a second 52px row → 104px total (twice a normal header).
   Reuses `.nuke-feed-tab` chrome so the labels + active underline match the feed. */
.nuke-container .nuke-post-modal-overlay[data-subview="notif"] .nuke-notif-tabs {
  display: flex;
  /* 38px tabs + the bar's 2px border-bottom = 40px total — matches the profile page nav
     (.nuke-profile-tabs: 22px item + 8px×2 padding + 2px border = 40px). */
  height: 38px;
}
/* The post fills the body; it scrolls if it's taller than the panel. */
/* Right edge for the centre-panel modals (NOT the chat feed — that's #nuke-chat-scroll, not this): a 1px
   right border as an inset shadow ON THE BODY so it paints OVER the body's own background (the notif card-fill,
   etc. — an overlay-level shadow sat behind that bg and was invisible). It sits BEHIND the body's content, at
   the SAME edge + colour (--nk-border-soft) as the frame the profile/notif content already draws — so it
   coincides there (no double border) and fills the right edge where content doesn't reach it (e.g. /post). */
.nuke-container .nuke-post-modal-body { flex: 1; overflow-y: auto; box-shadow: inset -1px 0 0 0 var(--nk-border-soft); }
/* Notification page: panel background = the user's post-container fill (SETTING-2,
   --self-card-fill), ONLY for the notif subview. The notif cards sit on top. */
.nuke-container .nuke-post-modal-overlay[data-subview="notif"] .nuke-post-modal-body {
  background: var(--self-card-fill);
}
/* Wallet profile tab: same background as the NOTIFICATIONS page (SETTING-2 self-card fill). Scoped to
   the active-tab flag so only the Wallet tab gets it, not Post/Media. */
.nuke-container .nuke-post-modal-overlay[data-subview="profile"][data-active-tab="wallet"] .nuke-post-modal-body {
  background: var(--self-card-fill);
}
/* Uses the ID (#nuke-post-modal-body) to BEAT the base `#nuke-post-modal-body { padding: 0 10px 0 }` —
   a plain-class selector loses that specificity race, so the top gap silently collapsed and the first
   card butted against the tabs. TOP gap === the inter-card gap (--notif-card-gap) so the first card sits
   below the tabs by the same distance that separates the cards; sides keep the base 10px (offset-shadow
   room), plus a little bottom breathing. */
.nuke-container .nuke-post-modal-overlay[data-subview="notif"] #nuke-post-modal-body {
  padding: var(--notif-card-gap) 10px 10px;
}
/* Profile footer — FLOATS over the bottom of the scroll body (feed scrolls UNDER it), present on EVERY
   profile tab. Holds Stake/Unstake on your own profile; on others' profiles it's an empty bar (same
   height, for a consistent layout). Same frosted blur as the floating back-bar — 88% surface + blur(8px)
   — so the feed shows through it. Hidden for non-profile subviews (post/notif/season). */
/* The profile Stake/Unstake footer was removed — the footer element stays hidden on every subview. */
.nuke-container .nuke-post-modal-footer { display: none; }

/* ── Season Pass page (subview takeover) ──────────────────────────────────────
   Rendered into the subview body when the Season Pass nav tab is open. Three fixed
   purchasable tiers ($25 / $100 / $1000). Neutral --nk-* tokens, matching the app. */
.nuke-container .nuke-post-modal-overlay[data-subview="season"] .nuke-post-modal-body { padding: 0; }
.nuke-container .nuke-sp-page { padding: 16px 0 24px; }
.nuke-container .nuke-sp-tiers { display: flex; flex-direction: column; gap: 10px; padding: 0 16px; }
/* Tier = price + Buy, one clean row. */
.nuke-container .nuke-sp-tier {
  display: flex;
  align-items: center;
  justify-content: space-between;
  width: 100%;
  padding: 16px 18px;
  border: 1px solid var(--nk-border-soft);
  border-radius: 14px;
  background: var(--nk-surface-2);
  cursor: pointer;
  transition: border-color 0.15s ease;
}
.nuke-container .nuke-sp-tier:hover { border-color: var(--nk-accent); }
.nuke-container .nuke-sp-tier.is-buying { opacity: 0.55; pointer-events: none; }
.nuke-container .nuke-sp-tier-price { font-size: calc(24px - var(--font-shrink, 0px)); font-weight: 800; color: var(--nk-text); font-variant-numeric: tabular-nums; }
.nuke-container .nuke-sp-tier-buy {
  flex: 0 0 auto;
  padding: 8px 22px;
  border-radius: 9999px;
  background: var(--nk-accent);
  color: #fff;
  font-size: calc(14px - var(--font-shrink, 0px));
  font-weight: 800;
}
.nuke-container .nuke-sp-msg { min-height: 16px; padding: 12px 16px 0; font-size: calc(13px - var(--font-shrink, 0px)); text-align: center; color: var(--nk-text-2); }
.nuke-container .nuke-sp-msg.is-error { color: #D32F2F; }
/* NPC replies + NPC quotes in the post overlay are inert (no account → nothing to
   open, no profile) — `pointer-events: none` kills every affordance at once: no
   hover colour/shadow, no pfp press animation, no click. */
.nuke-container .nuke-post-modal-body .nuke-post[data-has-account="false"],
.nuke-container .nuke-post-modal-body .nuke-post-quote[data-has-account="false"] {
  pointer-events: none;
}
.nuke-container .nuke-post-modal-missing {
  padding: 24px 16px;
  color: var(--nk-text-2);
  text-align: center;
}

/* ============ Notifications tab modal — X.com-style rows ============
   Reuses the prod `createNotificationItem` markup (.notif-item > glyph + content)
   but re-skins it for the X look: a small flat circular glyph on the left, a bold
   title line, then a muted preview + time. (The MSN-era sizing was stripped in the
   styles consolidation, so without this the glyph SVG renders at intrinsic size —
   the "giant icons".) Scoped to `.nuke-notif-list` so it never touches the mission
   list or other `.notif-item` users. */
.nuke-container .nuke-notif-list .notif-item {
  display: flex;
  align-items: var(--notif-row-align);
  gap: var(--notif-row-gap);
  padding: var(--notif-row-pad);
  background: var(--notif-card-bg);
  border: var(--notif-card-border);
  border-radius: 0;
  margin-bottom: var(--notif-card-gap);
  font-size: calc(15px - var(--font-shrink, 0px));
  cursor: pointer;
  box-shadow: 4px 4px 0 0 rgba(0, 0, 0, 0.2);
  transition: transform 0.12s ease, box-shadow 0.12s ease, background 0.12s ease;
}
/* Hover / press mirror the header back button EXACTLY: scale UP + shadow grows on hover (lifts off the
   surface); scale DOWN + shadow sucks in on click (presses flush). */
.nuke-container .nuke-notif-list .notif-item:hover { transform: scale(1.01); box-shadow: 5px 5px 0 0 rgba(0, 0, 0, 0.2); }
.nuke-container .nuke-notif-list .notif-item:active { transform: scale(0.985); box-shadow: 1px 1px 0 0 rgba(0, 0, 0, 0.2); }
.nuke-container .nuke-notif-list .notif-item.unread {
  /* Unread tint = the page-bg LIGHTNESS (SETTING-1) with the hue INVERTED (h+180), but a much LOWER
     chroma so it reads as a near-grey wash rather than a colour. Colourless → --nk-surface fallback. */
  --notif-unread-inv: oklch(from var(--current-user-color) var(--set-1-l) 0.008 calc(h + 180));
  background: var(--notif-unread-inv, var(--nk-surface));
}

/* Glyph: a small flat tinted circle (the global gloss/rim app-icon chrome is
   suppressed for the clean X look). SVG inherits `color` via currentColor. */
.nuke-container .nuke-notif-list .notif-item-glyph {
  flex-shrink: 0;
  width: 34px;
  height: 34px;
  display: flex;
  align-items: center;
  justify-content: center;
  border-radius: 50%;
  background: #EAF4FF;
  color: var(--nk-accent);
}
.nuke-container .nuke-notif-list .notif-item-glyph::before,
.nuke-container .nuke-notif-list .notif-item-glyph::after { display: none; }
.nuke-container .nuke-notif-list .notif-item-glyph svg {
  width: 18px;
  height: 18px;
  display: block;
}
/* Force the glyph tint through the SVG: the global `.notif-item *` rule sets
   color:black directly on the svg/paths, overriding the wrapper's inherited
   color — so re-assert currentColor across the glyph subtree. */
.nuke-container .nuke-notif-list .notif-item-glyph,
.nuke-container .nuke-notif-list .notif-item-glyph svg,
.nuke-container .nuke-notif-list .notif-item-glyph svg * {
  color: var(--nk-accent);
}

/* Actor pfp in the left slot — takes priority over the type glyph (createNotificationItem). Squared with a
   1px border to match the chat-feed avatars; sized to the 34px glyph it replaces. */
.nuke-container .nuke-notif-list .notif-item-pfp {
  flex-shrink: 0;
  width: var(--notif-pfp-size);
  height: var(--notif-pfp-size);
  border-radius: var(--notif-pfp-radius);
  border: var(--notif-pfp-border);
  box-sizing: border-box;
  object-fit: cover;
  display: block;
}
/* Content column: title on top, muted preview + time below. */
.nuke-container .nuke-notif-list .notif-item-content {
  flex: 1;
  min-width: 0;
  display: flex;
  flex-direction: column;
  gap: 2px;
}
.nuke-container .nuke-notif-list .notif-item-title {
  font-weight: 700;
  font-size: calc(15px - var(--font-shrink, 0px));
  line-height: 1.3;
  color: var(--nk-text);
}
.nuke-container .nuke-notif-list .notif-item-row {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 8px;
}
.nuke-container .nuke-notif-list .notif-item-message {
  flex: 1;
  min-width: 0;
  color: var(--nk-text-2);
  /* Same size as the player-card @handle (= the notif time right below): the card font scaled by
     --handle-scale (15px * 0.82 = 12.3px). */
  font-size: calc(var(--nk-card-font, 15px) * var(--handle-scale));
  line-height: 1.3;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.nuke-container .nuke-notif-list .notif-item-time {
  flex-shrink: 0;
  /* SETTING 4 (muted) → notif time; a notif is NOT in a post, so --set4-themed = YOUR hue. */
  color: var(--set4-themed, var(--nk-text-2));
  /* Same size + style as the chat post time next to the @handle (.nuke-post-time): the card font scaled by
     --handle-scale, weight 400, --nk-text-2. Uses the explicit card-font base (not 1em) so it can't drift
     with the notif row's own cascade. */
  font-size: calc(var(--nk-card-font, 15px) * var(--handle-scale));
  font-weight: 400;
}
/* Accept/Decline BEGIN in the pfp gutter — pull the row left under the avatar (past the content indent),
   then widen it back by the same amount so it spans the full card and the trailing time still reaches the
   right edge. The offset = avatar width + the row gap (the exact content indent). */
.nuke-container .nuke-notif-list .notif-friend-actions {
  margin-left: calc(-1 * (var(--notif-pfp-size) + var(--notif-row-gap)));
}
/* Friend-request: the time rides the Accept/Decline row (pushed right, vertically centred with the buttons
   by the row's align-items:center) — no extra empty line below the buttons. A normal notif's time rides the
   message row instead (pushed right by that row's space-between). */
.nuke-container .nuke-notif-list .notif-friend-actions .notif-item-time {
  margin-left: auto;
}

/* Unread marker → small blue dot at the right edge (X-style). */
.nuke-container .nuke-notif-list .notif-unread-dot {
  top: 8px;
  right: 8px;
  width: 8px;
  height: 8px;
  /* Same colour as the header BACK BUTTON background: a bright, saturated INVERTED (h+180) take on your
     profile hue. Colourless → visible accent fallback (a functional dot must stay visible). */
  --dot-bg-themed: oklch(from var(--current-user-color) 0.82 0.16 calc(h + 180));
  background: var(--dot-bg-themed, var(--nk-accent));
}

/* Mention preview keeps a slim quote bar, restyled to the light palette. */
.nuke-container .nuke-notif-list .notif-mention-quote {
  /* Mentioning user's own colours (fill = SETTING-2, copy text = SETTING-7) from --mention-user-color;
     no border; one text line tall so the mention card matches a normal notif. */
  --mention-fill-themed: oklch(from var(--mention-user-color) var(--set-2-l) var(--set-2-c) h);
  --mention-copy-themed: oklch(from var(--mention-user-color) var(--set-7-l) var(--set-7-c) h);
  margin: 0;
  /* Hug the TEXT — size to content instead of stretching to the full row (base .notif-item-message
     is flex:1). Long mentions still shrink + ellipsis (flex-shrink:1 + inherited min-width:0/overflow). */
  flex: 0 1 auto;
  padding: 0 6px;
  background: var(--mention-fill-themed, var(--nk-hover-1));
  color: var(--mention-copy-themed, var(--nk-text-2));
  font-style: italic;
}
/* The profile CARD in the left column is redundant on a profile page (the page
   IS the profile), so hide just that box — the nav list below it stays. */
.nuke-container[data-profile-view] .nuke-nav-top-box { display: none; }
.nuke-container[data-profile-view] .nuke-chat-wrap > #nuke-chat-scroll {
  padding-top: 0;
}
/* Close the 12px gap between the top bar (48px) and the chat window so the
   profile banner butts directly against the top header — no spacing. */
.nuke-container[data-profile-view] .nuke-stage {
  top: var(--page-header-h);
}
/* Profile page keeps the base chat-panel top — rounded top corners + crisp top
   hairline (inherited, no override) — so the profile panel matches the chat
   panel. (Was squared-off + borderless; user wants the rounded/bordered top.) */

/* ============ Profile PAGE (/~handle), X.com-style ============ */
.nuke-container .nuke-profile-page { display: flex; flex-direction: column; }
/* Banner — full column width, fixed height; runs under the sticky header. */
.nuke-container .nuke-profile-page-banner {
  position: relative;
  width: 100%;
  /* Twitter/X banner aspect ratio (1500×500 = 3:1); height follows the column width. */
  aspect-ratio: 3 / 1;
  flex-shrink: 0;
  /* No banner (or it failed to load — the <img> is hidden) → fall back to a SOLID fill of the viewed
     user's profile colour: SETTING 3 (--nk-border-themed), the SAME colour as the left nav panel (the
     page is themed to the viewed user, so --nk-border-themed resolves to THEIR hue here). Colourless →
     the neutral --nk-border. */
  background-color: var(--nk-border-themed, var(--nk-border));
  overflow: hidden;
}
/* A banner IS loaded (JS adds .has-banner on the img's onload) → solid BLACK backdrop, so a banner with a
   transparent background reads on black instead of the profile colour bleeding through the transparency. */
.nuke-container .nuke-profile-page-banner.has-banner {
  background-color: #000000;
}
.nuke-container .nuke-profile-page-banner-img {
  width: 100%; height: 100%; object-fit: cover; display: block;
}
/* Hide the banner image while it has no source (placeholder banners removed, so
   the `<img>` is left empty as a wiring point). A sized `<img>` with no loadable
   src otherwise paints Chrome's broken-image frame — the gray border that reads
   around the black banner. Same for the left card's banner image. */
.nuke-container .nuke-profile-page-banner-img:not([src]),
.nuke-container .nuke-profile-page-banner-img[src=""],
.nuke-container .nuke-profile-card-banner-img:not([src]),
.nuke-container .nuke-profile-card-banner-img[src=""],
.nuke-container .nuke-hovercard-banner-img:not([src]),
.nuke-container .nuke-hovercard-banner-img[src=""] {
  display: none;
}
/* Identity block under the banner. */
.nuke-container .nuke-profile-page-id {
  display: flex;
  flex-direction: column;
  padding: 0 16px 14px;
  /* Dotted-paper texture behind the bio + action buttons. */
  background: var(--nk-dot-pattern) repeat 0 0 / 6px 6px;
}
/* Profile sort/tab bar — reuses the chat feed-tabs styling (.nuke-feed-tab /
   .nuke-feed-tab-label) for font, hover, and the active underline. The label uses
   explicit vertical padding for the text→underline gap (instead of the chat's
   height:100% chain, which doesn't resolve the same outside the fixed-height chat
   header — leaving the profile underline too close to the text). */
.nuke-container .nuke-profile-tabs {
  display: flex;
  border-bottom: 2px solid var(--nk-border);
  /* Dotted-paper texture (matches the profile id block above). The opaque #fafafa base of the pattern
     replaces the former SETTING-2 card fill; --self-card-fill kept as the image-load fallback. */
  background: var(--nk-dot-pattern) repeat 0 0 / 6px 6px, var(--self-card-fill);
}
/* The in-content nav spans the FULL panel width — break out of #nuke-post-modal-body's 10px side
   padding (like the top section) so the tab bar + its bottom divider run edge to edge. Scoped to the
   BODY copy via the #id — the docked copy already lives in the full-width frosted header. */
.nuke-container #nuke-post-modal-body .nuke-profile-tabs {
  margin-left: -10px;
  margin-right: -10px;
  /* REAL side borders carry the panel FRAME; the card's own white-left / setting-3-right retained just
     inside as inset shadows (matching the posts). Scoped to the in-content copy; the docked copy gets its
     borders from the bar it lives in. */
  border-left: 1px solid var(--nk-border-2);
  border-right: 1px solid var(--nk-border-soft);
  box-shadow: inset 1px 0 0 0 #FFFFFF, inset -1px 0 0 0 var(--nk-border);
}
/* Docked tab nav — same tabs, rendered INSIDE the frosted header bar. Hidden until the scroll
   handler flags the overlay data-tabs-docked. Transparent (the bar's single frost is the bg),
   and no backdrop-filter of its own (that's the whole point: one blur, not two). */
.nuke-container .nuke-profile-docked-nav { display: none; }
.nuke-container .nuke-post-modal-overlay[data-subview="profile"][data-tabs-docked="true"] .nuke-profile-docked-nav {
  display: flex;
}
.nuke-container .nuke-profile-docked-nav.nuke-profile-tabs {
  /* Dotted texture like the in-content nav, but TRANSPARENT base (no colour layer) so the frosted
     header still shows through behind the dots. */
  background: var(--nk-dot-pattern) repeat 0 0 / 6px 6px;
  -webkit-backdrop-filter: none;
  backdrop-filter: none;
}
/* When docked, hide the in-content nav (keep its layout space so nothing jumps) — the docked
   copy in the header takes over. */
.nuke-container .nuke-post-modal-overlay[data-subview="profile"][data-tabs-docked="true"] .nuke-post-modal-body .nuke-profile-tabs {
  visibility: hidden;
}
.nuke-container .nuke-profile-tabs .nuke-feed-tab-label {
  height: auto;
  padding: 16px 0;
}

/* ── Notif + Profile page tabs as nav-style chips ──────────────────────────────────────────────────
   Same square profile-hue chip as the left-nav buttons (actual hue, black outline + text, hard offset
   shadow). Placed after all the base .nuke-feed-tab rules so it wins the equal-specificity ties.
   The ACTIVE tab reads as PRESSED-IN (sunken 1px shadow) instead of the old underline; inactive tabs
   hover-lift + press-suck like the nav. */
.nuke-container .nuke-notif-tab,
.nuke-container .nuke-profile-tab {
  flex: 1 1 0;              /* evenly spaced across the bar, like the old tabs */
  min-width: 0;
  height: 22px;
  padding: 0 9px;
  --nav-btn-bg-themed: oklch(from var(--current-user-color) 0.82 0.16 calc(h + 180));   /* inverted hue = the Back button colour */
  border: 1px solid #000;
  border-radius: 0;
  background: var(--nav-btn-bg-themed, #E0E0E0);
  box-shadow: 4px 4px 0 0 rgba(0, 0, 0, 0.2);
  color: #000;
  font-size: calc(13px - var(--font-shrink, 0px));
  transition: transform 0.12s ease, box-shadow 0.12s ease;
}
/* Label no longer needs the underline gap padding / full height. */
.nuke-container .nuke-notif-tab .nuke-feed-tab-label,
.nuke-container .nuke-profile-tab .nuke-feed-tab-label { height: auto; padding: 0; }
/* Drop the old active-underline bar. */
.nuke-container .nuke-notif-tab.is-active .nuke-feed-tab-label::after,
.nuke-container .nuke-profile-tab.is-active .nuke-feed-tab-label::after { display: none; }
/* Active = selected → pressed flush into the surface (sunken shadow) + a slightly DARKER background than
   the inactive chips (inverted hue L 0.82 → 0.80), black label. */
.nuke-container .nuke-notif-tab.is-active,
.nuke-container .nuke-profile-tab.is-active {
  color: #000;
  box-shadow: 1px 1px 0 0 rgba(0, 0, 0, 0.2);
  --nav-btn-bg-active-themed: oklch(from var(--current-user-color) 0.80 0.16 calc(h + 180));
  background: var(--nav-btn-bg-active-themed, #DADADA);
}
/* Inactive tabs lift on hover + suck in on press, exactly like the nav chips. */
.nuke-container .nuke-notif-tab:not(.is-active):hover,
.nuke-container .nuke-profile-tab:not(.is-active):hover { transform: scale(1.03); box-shadow: 6px 6px 0 0 rgba(0, 0, 0, 0.2); }
.nuke-container .nuke-notif-tab:not(.is-active):active,
.nuke-container .nuke-profile-tab:not(.is-active):active { box-shadow: 1px 1px 0 0 rgba(0, 0, 0, 0.2); }
/* Bars: centre the chips + give them breathing room / gaps (they no longer stretch to fill). */
.nuke-container .nuke-notif-tabs { align-items: center; gap: 14px; padding: 0 10px; }
.nuke-container .nuke-profile-tabs { align-items: center; gap: 14px; padding: 8px 10px; }
/* Collapsible top area (banner + identity). Its HEIGHT is animated in explicit px by JS
   (collapse/expandTopArea in profile-page.ts), NOT a CSS grid 1fr↔0fr trick: with the fr trick
   the row track and the grid item's rendered height diverge mid-interpolation, which detached the
   nav (tracks the row) from the content (tracks the item) and opened a gap. A single px height is
   the one value BOTH the nav (pushed by it) and the content follow, so they move in lockstep.
   flex-end bottom-anchors the content (flex-shrink:0 keeps it full-height) so as the height shrinks
   the content slides UP and overflows above this box — overflow:visible (NOT hidden) lets it pass
   UNDER the floating frosted header (z-index'd above; the body clips it at y=0), exactly like
   scrolling the content behind the bar, instead of clipping dead at the header's bottom edge.
   The nav docks as a SEPARATE step (data-tabs-docked, timed in JS so the collapse lands first,
   then the in-content nav hands off to the docked copy) — see the docked-nav rules above. */
.nuke-container .nuke-profile-page-top {
  overflow: visible;
  display: flex;
  flex-direction: column;
  justify-content: flex-end;
  transition: height 0.34s cubic-bezier(0.4, 0, 0.2, 1);
  /* Break the top section OUT of #nuke-post-modal-body's 10px side padding so the banner (and the top
     block) span the FULL panel width, edge to edge — the identity text below keeps its own 16px padding.
     Negative margins on this stretched flex child widen it by 20px to reach the panel borders. */
  margin-left: -10px;
  margin-right: -10px;
  /* Read as one card matching the post CONTAINERS: SETTING-2 fill (behind the info area below the banner).
     REAL side borders carry the panel FRAME; the card's own white-left / setting-3-right retained just
     inside as inset shadows. */
  background: var(--self-card-fill);
  border-left: 1px solid var(--nk-border-2);
  border-right: 1px solid var(--nk-border-soft);
  box-shadow: inset 1px 0 0 0 #FFFFFF, inset -1px 0 0 0 var(--nk-border);
}
.nuke-container .nuke-profile-page-top-inner { flex-shrink: 0; }
/* At the collapsed/docked REST, clip the top area so the slid-up content doesn't ghost faintly
   under the frosted header. During the collapse itself it stays overflow:visible so it slides
   UNDER the bar like a scroll; data-tabs-docked flips to true exactly when the collapse lands. */
.nuke-container .nuke-post-modal-overlay[data-subview="profile"][data-tabs-docked="true"] .nuke-profile-page-top {
  overflow: hidden;
}
/* Non-wallet tab switches are a plain instant swap. Wallet transitions (to/from Wallet) fade the newly
   shown pane in — data-tab-fade is set only for those (reset to 'off' on render / non-wallet switches). */
.nuke-container .nuke-post-modal-overlay[data-tab-fade="on"] .nuke-profile-tab-pane:not([hidden]) {
  animation: nuke-ptab-fade 0.2s cubic-bezier(0.4, 0, 0.2, 1) backwards;
}
@keyframes nuke-ptab-fade {
  from { opacity: 0; transform: translateY(8px); }
  to   { opacity: 1; transform: translateY(0); }
}
@media (prefers-reduced-motion: reduce) {
  .nuke-container .nuke-profile-page-top { transition: none; }
  .nuke-container .nuke-profile-tab-pane:not([hidden]) { animation: none; }
}
.nuke-container .nuke-profile-tab-pane[hidden] { display: none; }
/* Pfp row: the big avatar overlaps the banner's bottom edge (X.com). */
.nuke-container .nuke-profile-page-toprow {
  display: flex;
  align-items: flex-end;
  justify-content: space-between;
  margin-top: -74px;
  margin-bottom: 12px;
}
/* Top-right column under the banner: Edit button stacked over the headline balance. The
   86px top pull replaces the Edit button's old offset (toprow is lifted 74px into the
   banner; 86 drops this row just below the banner edge, like X's edit row). */
.nuke-container .nuke-profile-page-toprow-actions {
  align-self: flex-start;
  margin-top: 86px;
  display: flex;
  flex-direction: column;
  align-items: flex-end;
  gap: 10px;
}
/* Hide the Edit / friend button entirely on the Wallet tab. */
.nuke-container .nuke-post-modal-overlay[data-active-tab="wallet"] .nuke-profile-page-toprow-actions {
  display: none;
}
/* Headline balance — same size/weight as the chat profile card balance (.nuke-chat-balance). */
.nuke-container .nuke-profile-page-balance {
  font-size: calc(32px - var(--font-shrink, 0px));
  font-weight: 800;
  color: var(--nk-text);
  line-height: 1;
  letter-spacing: -0.01em;
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
  flex-shrink: 0;
  margin-top: 4px;                            /* nudge down so it sits lower than the username's top */
}
/* Hide the headline balance entirely when it's empty (zero) — keeps the username row clean. */
.nuke-container .nuke-profile-page-balance:empty { display: none; }
.nuke-container .nuke-profile-page-pfp {
  width: 133px;
  height: 133px;
  margin: 0;
  /* Thicker avatar border to match this large (133px) size — the .cc-pfp image border reads from this var
     (default 1px on .nuke-post-pfp); scaled up here so it stays proportional to the big avatar. */
  --pfp-outer-border-width: 6px;
  /* Presence dot scaled to this large avatar (hovercard's 11px/7.5px at 64px ×~2.08).
     Overrides the small 8px/6px inherited from the `nuke-post-pfp` class on the wrapper. */
  --dot-size: 22px;
  /* Corner + ring scaled from the hovercard avatar (5px corner, 4px ring at 64px) to
     this 133px size (×~2.08) so the profile pfp reads with the SAME proportions: a ~10px
     corner (not the boxy 5px cap, not an over-round 13%) and an ~8px ring below. */
  border-radius: 0;                               /* squared to match the unrounded avatars */
  /* No ring — the white avatar "cutout" that used to separate the avatar from the banner is REMOVED
     (it read as a white cutout on the themed profile). Avatar sits flush over the banner/content. */
  box-shadow: none;
}
.nuke-container .nuke-profile-page-pfp .cc-pfp {
  width: 100%;
  height: 100%;
  border-radius: inherit;
}
.nuke-container .nuke-profile-page-name {
  display: flex;
  align-items: center;
  gap: 5px;
  font-size: calc(20px - var(--font-shrink, 0px));
  font-weight: 800;
  color: var(--nk-text);
  line-height: 1.2;
}
/* Headline row: name + handle column on the left, balance on the right, TOP-aligned so the
   username's top lines up with the balance's top. Bio/cred are full-width siblings below it
   (the bio spans the whole panel, not just the name column). */
.nuke-container .nuke-profile-page-headline {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: 12px;
}
.nuke-container .nuke-profile-page-namecol {
  display: flex;
  flex-direction: column;
  min-width: 0;
}
/* Header cred pill is injected via credPillHtml into this host; display:contents keeps the
   pill itself as the direct flex child of .nuke-chat-header-rate (layout unchanged). */
/* Cred badge relocated INTO the pfp gutter (under the avatar), matching the message-card footer. The
   rate row is pinned at the message field (left:68px); -52px pulls the badge back to the pfp column
   (x=16 = the avatar's left, badge is one pfp wide). Bottom-aligned with the action buttons so its
   distance from the card bottom matches the post footer's cred pill (the buttons are 34px, the badge 20px,
   so centering floated it up); out of the row's flex so Use Item / Compose reflow to the row's left. */
/* The header cred pill is the VIEWER'S own cred — keep its border on the page-themed --nk-border so a
   coloured viewer's own badge stays their colour (not the colourless fallback used for other users). */
.nuke-container .nuke-chat-header-cred .nuke-post-vote { border-color: var(--cred-border-themed, var(--nk-border)); }
.nuke-container .nuke-chat-header-cred {
  position: absolute;
  left: -52px;
  bottom: 0;
}
.nuke-container .nuke-profile-page-name-badge {
  width: 18px;
  height: 18px;
  object-fit: contain;
  flex-shrink: 0;
}
.nuke-container .nuke-profile-page-handle {
  margin-top: 3px; /* +2px gap below the username */
  font-size: calc(15px - var(--font-shrink, 0px));
  color: var(--nk-text-2);
  font-style: italic;
  line-height: 1.3;
}
.nuke-container .nuke-profile-page-bio {
  margin-top: 12px;
  font-size: calc(15px - var(--font-shrink, 0px));
  color: var(--nk-text);
  line-height: 1.35;
  overflow-wrap: anywhere;
}
/* "Joined <date>" metadata line under the bio (X-style, with a calendar glyph). The "Joined <date>" +
   Online/Offline text take the MAIN COPY colour (SETTING 7, the viewed profile's hue; --nk-text when
   colourless); the "|" divider keeps the slightly-darker post-TIMESTAMP colour (SETTING 4 −0.05 L). */
.nuke-container .nuke-profile-page-joined {
  --set4-joined: oklch(from var(--current-user-color) calc(var(--set-4-l) - 0.05) var(--set-4-c) h);
  --joined-copy: oklch(from var(--current-user-color) var(--set-7-l) var(--set-7-c) h);
  margin-top: 12px;
  font-size: calc(14px - var(--font-shrink, 0px));
  color: var(--joined-copy, var(--nk-text));
  line-height: 1.3;
  display: flex;
  align-items: center;
  gap: 5px;
}
.nuke-container .nuke-profile-page-joined-date { display: inline-flex; align-items: center; gap: 5px; }
/* Presence text inherits the join-date type EXACTLY (main-copy colour, 14px). The "|" divider is set to
   the (old) join-date TIMESTAMP colour so it reads as a distinct separator between them. */
.nuke-container .nuke-profile-page-joined-sep { color: var(--set4-joined, var(--nk-text-2)); }
/* Cred stat in the top identity section (X-style "value label"). */
.nuke-container .nuke-profile-page-stats {
  margin-top: 12px;
  font-size: calc(15px - var(--font-shrink, 0px));
}
.nuke-container .nuke-profile-page-stat-value {
  font-weight: 700;
  color: var(--nk-text);
}
.nuke-container .nuke-profile-page-stat-label {
  color: var(--nk-text-2);
}
/* Tab strip. */
/* Info block — large inline metrics laid out like X.com's Following/Followers
   row (bold value + gray label), wrapping if needed. */
/* Phantom-wallet-style total-balance headline, centered between the bio and the
   holdings list. */
.nuke-container .nuke-profile-balance {
  text-align: center;
  padding: 18px 16px 10px;
}
.nuke-container .nuke-profile-balance-amount {
  font-size: calc(52px - var(--font-shrink, 0px));
  font-weight: 800;
  color: var(--nk-text);
  line-height: 1.04;
  letter-spacing: -0.02em;
  font-variant-numeric: tabular-nums;
}
.nuke-container .nuke-profile-balance-label {
  font-size: calc(14px - var(--font-shrink, 0px));
  color: var(--nk-text-2);
  margin-top: 4px;
}
/* Stake / Unstake action row (self profile) — between the balance and the stat grid. */
.nuke-container .nuke-profile-page-actions {
  display: flex;
  gap: 10px;
  margin: 4px 16px 16px;
}
.nuke-container .nuke-profile-page-info {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 10px;
  margin: 8px 16px 16px;
}
/* Fallout contribute block on the Wallet tab — same 16px side inset as the info grids above it; the
   `.nuke-side-balance-body` class stacks the field + Buy button (reused from the right-panel widget). */
.nuke-container .nuke-profile-contribute {
  margin: 8px 16px 16px;
}
.nuke-container .nuke-profile-contribute-label {
  font-size: calc(14px - var(--font-shrink, 0px));
  font-weight: 700;
  color: var(--nk-text);
}
/* Poke button (other users' Wallet tab): the contribute Buy pill, but a solid themed fill (the viewed
   profile's hue, like the header bar) instead of the gray-until-valid state — it's always actionable. */
.nuke-container .nuke-chat-contribute-buy.nuke-profile-poke-btn {
  --btn-bg: oklch(from var(--current-user-color) 0.42 0.12 h);
  background-color: var(--btn-bg, #8899A6);
  font-variant-numeric: tabular-nums; /* steady width for the cooldown countdown */
}
/* Money Poke on cooldown → plain gray, non-actionable (shows the HH:MM:SS countdown). */
.nuke-container .nuke-chat-contribute-buy.nuke-profile-poke-btn:disabled,
.nuke-container .nuke-chat-contribute-buy.nuke-profile-poke-btn.is-cooldown {
  background: var(--nk-hover-1);
  color: var(--nk-text-2);
  cursor: default;
}
/* Staking stats as a 2x2 grid — each stat in its own bordered card: icon + label head
   on top, the value large beneath. */
.nuke-container .nuke-profile-stat-card {
  display: flex;
  flex-direction: column;
  gap: 10px;
  padding: 14px;
  border: 1px solid var(--nk-border);
  border-radius: 14px;
}
.nuke-container .nuke-profile-stat-card-head {
  display: flex;
  align-items: center;
  gap: 8px;
}
.nuke-container .nuke-profile-stat-icon {
  flex-shrink: 0;
  width: 30px;
  height: 30px;
  display: flex;
  align-items: center;
  justify-content: center;
  border-radius: 0; /* squared to match the site-wide unrounded icons */
  background: var(--nk-surface-2);
  color: var(--nk-text-2);
  font-size: calc(16px - var(--font-shrink, 0px));
}
.nuke-container .nuke-profile-stat-label {
  font-size: calc(13px - var(--font-shrink, 0px));
  font-weight: 600;
  color: var(--nk-text-2);
}
.nuke-container .nuke-profile-stat-value {
  font-size: calc(22px - var(--font-shrink, 0px));
  font-weight: 800;
  color: var(--nk-text);
  font-variant-numeric: tabular-nums;
}
.nuke-container .nuke-profile-metric {
  display: inline-flex;
  align-items: baseline;
  gap: 5px;
  white-space: nowrap;
}
.nuke-container .nuke-profile-metric-value {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  font-size: calc(22px - var(--font-shrink, 0px));
  font-weight: 800;
  color: var(--nk-text);
  font-variant-numeric: tabular-nums;
}
.nuke-container .nuke-profile-metric-icon { width: 22px; height: 22px; flex-shrink: 0; }
.nuke-container .nuke-profile-metric-label {
  font-size: calc(15px - var(--font-shrink, 0px));
  color: var(--nk-text-2);
}
/* Section header above the notifications feed. */
/* The notifications list spans the full container width (the items carry their
   own post-style padding), like the message feed. The top border is the divider
   between the profile stats above and the notifications feed below. */
.nuke-container .nuke-profile-notifs {
  width: 100%;
  border-top: var(--nk-panel-border-width) solid var(--nk-border-soft);
}
/* Empty state (e.g. Notifications tab placeholder). */
.nuke-container .nuke-profile-empty {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 6px;
  padding: 48px 16px;
  text-align: center;
}
.nuke-container .nuke-profile-empty-icon { font-size: calc(36px - var(--font-shrink, 0px)); color: #AAB8C2; }
/* Image variant (the universal "nothing to see here" NeXT98 pixel icon) — font-size can't size it, so
   give it a box; pixelated keeps the 32px source crisp when scaled up. */
.nuke-container img.nuke-profile-empty-icon { width: 48px; height: 48px; image-rendering: pixelated; }
.nuke-container .nuke-profile-empty-title { font-size: calc(17px - var(--font-shrink, 0px)); font-weight: 700; color: var(--nk-text); }
/* The universal "Nothing to see here" message: site font (--font-conduit), NOT bold, and sized to the
   profile-nav tab font (13px). Loading / error titles keep the bold 17px treatment. */
.nuke-container .nuke-profile-empty-title.nuke-empty-plain {
  font-family: var(--font-conduit);
  font-size: calc(13px - var(--font-shrink, 0px));
  font-weight: 400;
  font-style: italic;
}
.nuke-container .nuke-profile-empty-sub { font-size: calc(14px - var(--font-shrink, 0px)); color: var(--nk-text-2); max-width: 280px; }
/* Spinning loader glyph (reuses the global `btn-spin` keyframe from styles.css). */
.nuke-container .nuke-profile-spin { animation: btn-spin 0.8s linear infinite; }
/* Retry button shown in the notifications error state. */
.nuke-container .nuke-profile-notif-retry {
  margin-top: 8px;
  padding: 6px 16px;
  border: 1px solid var(--nk-border);
  border-radius: 9999px;
  background: var(--nk-surface);
  font-size: calc(14px - var(--font-shrink, 0px));
  font-weight: 700;
  color: var(--nk-text);
  cursor: pointer;
}
.nuke-container .nuke-profile-notif-retry:hover { background: #F7F9F9; }

/* Notifications tab: re-style prod's `.notif-item` to read like a message post
   (icon in the pfp gutter + content column), but ALTERED so it's clearly a notif:
   a round colored icon-circle (not a square user avatar), and a faint blue tint +
   left accent on unread ones. */
.nuke-container .nuke-profile-notifs .notif-item {
  display: flex;
  gap: 12px;
  align-items: flex-start;
  padding: 12px 16px;
  border-bottom: var(--nk-panel-border-width) solid var(--nk-border-soft);
  background: none;
  font-size: calc(15px - var(--font-shrink, 0px));
  cursor: default;
}
.nuke-container .nuke-profile-notifs .notif-item.unread {
  background: rgba(var(--nk-accent-rgb), 0.06);
  box-shadow: inset 3px 0 0 var(--nk-accent);
}
/* Drop prod's app-icon gloss/rim pseudos on BOTH the item and its glyph — the
   item's `::after` rim was painting a thick inset bottom edge (the "underline").
   The flat 1px `border-bottom` above is the only separator. */
.nuke-container .nuke-profile-notifs .notif-item::before,
.nuke-container .nuke-profile-notifs .notif-item::after,
.nuke-container .nuke-profile-notifs .notif-item-glyph::before,
.nuke-container .nuke-profile-notifs .notif-item-glyph::after { display: none; }
.nuke-container .nuke-profile-notifs .notif-item-glyph {
  flex-shrink: 0;
  width: 40px;
  height: 40px;
  border-radius: 0; /* squared to match the site-wide unrounded icons */
  background: var(--nk-border-soft);
  display: flex;
  align-items: center;
  justify-content: center;
  color: var(--nk-accent);
}
.nuke-container .nuke-profile-notifs .notif-item-glyph svg,
.nuke-container .nuke-profile-notifs .notif-item-glyph img {
  width: 22px;
  height: 22px;
}
/* Actor pfp — squared 1px-border avatar in the 40px left slot (matches the squared glyph). */
.nuke-container .nuke-profile-notifs .notif-item-pfp {
  flex-shrink: 0;
  width: 40px;
  height: 40px;
  border-radius: 0;
  border: 1px solid var(--nk-border);
  box-sizing: border-box;
  object-fit: cover;
  display: block;
}
.nuke-container .nuke-profile-notifs .notif-item-content {
  flex: 1 1 auto;
  min-width: 0;
  display: flex;
  flex-direction: column;
  gap: 1px;
}
.nuke-container .nuke-profile-notifs .notif-item-title {
  font-size: calc(15px - var(--font-shrink, 0px));
  font-weight: 700;
  color: var(--nk-text);
  line-height: 1.3;
}
.nuke-container .nuke-profile-notifs .notif-item-row {
  display: flex;
  align-items: baseline;
  gap: 6px;
}
.nuke-container .nuke-profile-notifs .notif-item-message {
  flex: 1 1 auto;
  min-width: 0;
  font-size: calc(15px - var(--font-shrink, 0px));
  color: var(--nk-text);
  line-height: 1.35;
  overflow-wrap: anywhere;
}
.nuke-container .nuke-profile-notifs .notif-item-message.notif-mention-quote {
  color: var(--nk-text-2);
  font-style: italic;
}
.nuke-container .nuke-profile-notifs .notif-item-time {
  flex-shrink: 0;
  font-size: calc(13px - var(--font-shrink, 0px));
  color: var(--nk-text-2);
  white-space: nowrap;
}
/* The green unread dot is redundant with the tint+accent; hide it. */
.nuke-container .nuke-profile-notifs .notif-unread-dot { display: none; }

/* The chat header identity (pfp + name block) is clickable → that user's page. */
.nuke-container .nuke-chat-header-2 .nuke-post-pfp,
.nuke-container .nuke-chat-header-2 .nuke-chat-header-main {
  cursor: pointer;
}
/* Secondary header laid out like a post: pfp gutter + a main column holding the
   name/@handle row and the balance (the "message text"). */
.nuke-container .nuke-chat-header-main {
  flex: 1;
  min-width: 0;
  display: flex;
  flex-direction: column;
  /* Center the name + @handle block against the 40px pfp (same as the messages:
     min-height of the pfp + vertical centering), instead of top-aligning it. */
  justify-content: center;
  min-height: 40px;
}
/* Username row. Reserve room on the right so a long name truncates before the
   absolutely-positioned balance (the meta row below is free of this so its DPR
   can sit under the balance). */
/* The chat-feed identity card (pfp + balance) drops the username row (name + level
   badge) and the @handle — they still appear in the feed posts and on the profile page. */
.nuke-container .nuke-chat-header-2 .nuke-chat-header-namerow,
.nuke-container .nuke-chat-header-2 .nuke-chat-header-handle {
  display: none;
}
.nuke-container .nuke-chat-header-namerow {
  display: flex;
  align-items: center;
  flex-wrap: nowrap;
  /* Tight, like the feed name cluster — badge hugs the name (not a word-space away). */
  gap: 3px;
  line-height: 1.25;
  font-size: calc(15px - var(--font-shrink, 0px));
  padding-right: 110px;
}
.nuke-container .nuke-chat-header-name {
  /* Centered so it aligns with the level badge beside it (the balance is now
     absolutely positioned, so there's nothing to pin away from). */
  align-self: center;
  font-weight: 700;
  color: var(--nk-text);
  white-space: nowrap;
}
.nuke-container .nuke-chat-header-handle {
  align-self: flex-start;
  color: var(--nk-text-2);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  min-width: 0;
  /* Match the message @handle / cred size (15px) — they sit outside the 15px
     namerow so they'd otherwise inherit the 13px container base and read smaller.
     line-height matches the namerow (1.25) so name → handle → cred stack evenly. */
  font-size: calc(15px - var(--font-shrink, 0px));
  line-height: 1.25;
}
.nuke-container .nuke-chat-header-rate .nuke-post-dpr {
  font-size: calc(15px - var(--font-shrink, 0px));
  line-height: 1.25;
}
/* Meta row UNDER the name — @handle · DPR% · level icon, laid out exactly like
   the message hover card's meta line. */
/* Big bold balance (Phantom-style), pinned to the header's top-right and pulled
   OUT of flow so its tall 28px line box doesn't inflate the name row. The 13.5px
   top aligns the balance's visual top with the username's (the vertically-centered
   name sits ~1.5px lower than a plain top:12 would put the balance). */
/* Balance ROW — anchored top-right, occupying the same 40px band as the name block
   (so the balance lines up with the username/handle middle). Holds the fallout
   contribute INPUT (left) + the giant balance number (right), vertically centered. */
.nuke-container .nuke-chat-balance-row {
  position: absolute;
  right: 16px;
  /* Left edge at the message-field start (16px pad + 40px pfp + 12px gap) so the
     contribute input can stretch all the way to the pfp; the balance hugs the right. */
  left: 68px;
  /* Overlay the pfp's exact vertical band (12px header padding + 40px pfp) so the input
     and balance are vertically centered with the pfp. */
  top: 12px;
  height: 40px;
  display: flex;
  align-items: center;
  justify-content: flex-end;
  gap: 10px;
}
.nuke-container .nuke-chat-balance {
  display: flex;
  align-items: center;
  font-size: calc(32px - var(--font-shrink, 0px));
  font-weight: 800;
  color: var(--nk-text);
  line-height: 1;
  letter-spacing: -0.01em;
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}
/* Balance trend colour — ONE rule for every balance on the site. Each is a `.nuke-balance` element
   (see balanceRender / balanceElHtml / applyBalanceEl) carrying data-trend, so value + colour + this
   fade all live in one place: green when it went up, red when down, default otherwise, with a shared
   linear fade. (Feed messages are re-rendered HTML each time so those can't tween — the rest do.) */
.nuke-container .nuke-balance { transition: color 0.6s linear; }
.nuke-container .nuke-balance[data-trend="up"] { color: var(--color-nuke-success); }
.nuke-container .nuke-balance[data-trend="down"] { color: var(--color-nuke-danger); }
/* Level icon (left) + DPR, pushed to the right end of the @handle row so the DPR
   lines up vertically with the handle and sits under the balance (it may overlap
   the balance — intentional). Header's default sizing (16px icon, 15px DPR). */
.nuke-container .nuke-chat-header-rate {
  /* Holds the cred vote pill + Use Item pill (same section as the post footer).
     Pinned to the header's bottom-left (left = 16px pad + 40px pfp + 12px gap, the
     message-field start) at the SAME `bottom:9px` as the stake/unstake buttons, so
     the pills' bottoms line up with the buttons'. Name/@handle stay top-aligned in
     the main column. */
  position: absolute;
  left: 68px;
  bottom: 9px;
  display: flex;
  align-items: center;
  gap: 8px;
}
/* In the header card's action row, size the Use Item / Compose to the Buy button's
   height (34px) so they line up with the Buy / Stake / Unstake buttons. The cred pill
   is intentionally left at its default size so it matches the cred pills in the message
   footers. (Scoped to the header — the post footers keep their compact 24px icons.) */
.nuke-container .nuke-chat-header-rate .nuke-post-use-item,
.nuke-container .nuke-chat-header-rate .nuke-post-compose {
  width: 34px;
  height: 34px;
  /* Drop the buttons so their (centered) icons line up with the gutter cred badge — the badge centres at
     19px from the card bottom, these 34px buttons at 26px, so a 7px nudge down aligns the icon row. */
  transform: translateY(7px);
}
/* Feed-filter button moved into this row (right of Announcement): it's already 34px, so it just needs the
   same 7px drop as its siblings; the row's gap:8px supplies its spacing, so drop its standalone margin. */
.nuke-container .nuke-chat-header-rate .nuke-feed-sort-btn {
  transform: translateY(7px);
  margin-left: 0;
}
.nuke-container .nuke-chat-header-rate .nuke-post-use-item i,
.nuke-container .nuke-chat-header-rate .nuke-post-compose i {
  font-size: calc(19px - var(--font-shrink, 0px));
}
/* Header use-item/compose are now inline SVGs (nuke-in-box + pencil) — size them to sit with the 19px
   RemixIcon siblings (announce etc.); overrides the smaller footer svg sizes in this header context. */
.nuke-container .nuke-chat-header-rate .nuke-post-use-item > svg { width: auto; height: 21px; }
.nuke-container .nuke-chat-header-rate .nuke-post-compose > svg { width: auto; height: 20px; }
/* "Unstake" + "Stake" pills in the chat panel's secondary header — bottom-right,
   side by side, X "Post"-button style (fully-rounded, bold white). Unstake is
   red, Stake is near-black. */
.nuke-container .nuke-chat-action-btns {
  position: absolute;
  right: 16px;
  bottom: 9px;
  display: flex;
  gap: 8px;
}
.nuke-container .nuke-chat-stake-btn,
.nuke-container .nuke-chat-unstake-btn,
.nuke-container .nuke-chat-signin-btn {
  height: 36px;
  padding: 0 14px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  border: none;
  border-radius: 9999px;
  background: #0F1419;
  color: #FFFFFF;
  font-weight: 700;
  font-size: calc(14px - var(--font-shrink, 0px));
  line-height: 1;
  cursor: pointer;
}
.nuke-container .nuke-chat-unstake-btn {
  background: #F4212E;
}
/* Signed-out swap: the Sign In button replaces Stake/Unstake. Hidden by default;
   the row gets data-signedout="true" (set in renderNukeHeaderBalance) while the
   user has no connected wallet, which hides Stake/Unstake and shows Sign In
   instead. data-signedout takes precedence over data-fallout. */
.nuke-container .nuke-chat-signin-btn {
  display: none;
  background: var(--nk-accent);
}
/* Nuclear fallout: hide the Stake/Unstake actions; the contribute field
   (shown below) takes their place. data-fallout is set in renderNukeHeaderBalance. */
.nuke-container .nuke-chat-action-btns[data-fallout="true"] .nuke-chat-stake-btn,
.nuke-container .nuke-chat-action-btns[data-fallout="true"] .nuke-chat-unstake-btn {
  display: none;
}
/* Contribute amount field — shown by default in nuclear fallout (in place of
   Stake/Unstake). Pinned to the action row's bottom + right edge (16px); its LEFT
   is set inline by renderNukeHeaderBalance to start just past the Use Item pill. */
/* Contribute row — holds the amount field (fills) + a Buy button (right). Pinned
   to the action row's bottom; nudged in from 16px to sit slightly further right.
   Left is sized in renderNukeHeaderBalance to start past the Use Item pill. */
.nuke-container .nuke-chat-contribute-row {
  display: none;
  position: absolute;
  bottom: 12px;
  right: 12px;
  /* Match the left profile container's Unstake button height (34px). */
  height: 34px;
  align-items: center;
  gap: 8px;
  z-index: 2;
}
.nuke-container .nuke-chat-contribute-input {
  /* Hidden by default; shown to the LEFT of the balance number during nuclear fallout
     (see the :has rules below). Fills the row — stretching from the pfp to the balance. */
  display: none;
  flex: 1 1 auto;
  min-width: 0;
  height: 34px;
  box-sizing: border-box;
  /* No left padding — text + placeholder sit flush against the squared-off left edge. */
  padding: 0 16px 0 0;
  /* Transparent border + background — the input blends into the header band. */
  border: 1px solid transparent;
  border-radius: 0 22px 22px 0;
  background: transparent;
  color: var(--nk-text);
  font-size: calc(18px - var(--font-shrink, 0px));
  font-weight: 600;
  line-height: 1;
  outline: none;
}
/* Fallout → reveal the contribute input beside the balance. The data-fallout flag lives
   on the action row (a LATER sibling of the balance row), so target it via :has on the
   header. The signed-out rule comes after so it wins (an NPC sees no input). */
.nuke-container .nuke-chat-header-2:has(.nuke-chat-action-btns[data-fallout="true"]) .nuke-chat-contribute-input {
  display: block;
}
.nuke-container .nuke-chat-header-2:has(.nuke-chat-action-btns[data-signedout="true"]) .nuke-chat-contribute-input {
  display: none;
}
/* Buy button at the right of the row — dark pill, same height as the field. */
.nuke-container .nuke-chat-contribute-buy {
  flex: 0 0 auto;
  height: 34px;
  box-sizing: border-box;
  padding: 0 16px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  border-radius: 0;
  /* Gray until a valid amount is entered; turns green via .is-valid (set in initNukeContribute) — the
     bevel + fill both re-derive from --btn-bg on that swap. */
  --btn-bg: #8899A6;
  background-color: var(--btn-bg);
  /* Sign-in button DESIGN (colour kept): top-highlight + bottom-shade gradients over the fill. */
  background-image:
    linear-gradient(to bottom, rgba(255, 255, 255, 0.10) 0%, transparent 50%),
    linear-gradient(to top, rgba(0, 0, 0, 0.10) 0%, transparent 12px);
  /* 2px 3D bevel derived from the fill: lighter top/left, darker bottom/right of the SAME hue. */
  border: 4px solid;
  border-color:
    oklch(from var(--btn-bg) calc(l * 1.28) c h)
    oklch(from var(--btn-bg) calc(l * 0.72) c h)
    oklch(from var(--btn-bg) calc(l * 0.72) c h)
    oklch(from var(--btn-bg) calc(l * 1.28) c h);
  box-shadow: 4px 4px 0 0 rgba(0, 0, 0, 0.2);
  color: #FFFFFF;
  font-weight: 800;
  font-size: calc(14px - var(--font-shrink, 0px));
  line-height: 1;
  cursor: pointer;
  transition: transform 0.12s ease, box-shadow 0.12s ease, filter 0.15s ease, background-color 0.15s ease;
}
.nuke-container .nuke-chat-contribute-buy:hover { transform: scale(1.03); filter: brightness(1.06); }
.nuke-container .nuke-chat-contribute-buy:active { transform: scale(0.97); box-shadow: 1px 1px 0 0 rgba(0, 0, 0, 0.2); }
.nuke-container .nuke-chat-contribute-buy.is-valid {
  --btn-bg: #00BA7C;
  background-color: var(--btn-bg);
}
/* Balance + presale contribute, relocated into the right-panel container. Reset the
   chat-header absolute positioning + fallout-gated visibility so they stack in the box. */
.nuke-container .nuke-side-balance-body {
  display: flex;
  flex-direction: column;
  gap: 10px;
}
.nuke-container .nuke-side-balance-body .nuke-chat-balance-row {
  position: static;
  inset: auto;
  height: auto;
  flex-direction: column;
  align-items: stretch;
  justify-content: flex-start;
  gap: 8px;
}
.nuke-container .nuke-side-balance-body .nuke-chat-balance {
  font-size: calc(30px - var(--font-shrink, 0px));
}
.nuke-container .nuke-side-balance-body .nuke-chat-contribute-input {
  display: block;
  height: 36px;
  padding: 0 12px;
  border: 1px solid var(--nk-border-soft);
  border-radius: 4px;
  background: var(--nk-surface);
  font-size: calc(15px - var(--font-shrink, 0px));
  font-weight: 600;
}
.nuke-container .nuke-side-balance-body .nuke-chat-contribute-row {
  position: static;
  inset: auto;
  height: auto;
  display: flex;
}
.nuke-container .nuke-side-balance-body .nuke-chat-contribute-buy {
  flex: 1 1 auto;
  height: 36px;
}
.nuke-container .nuke-chat-contribute-input::placeholder {
  color: #8899A6;
  font-weight: 600;
}
/* Shown in fallout — the action row carries data-fallout and the contribute row
   is its following sibling. */
.nuke-container .nuke-chat-action-btns[data-fallout="true"] ~ .nuke-chat-contribute-row {
  display: flex;
}
/* Signed-out swap (placed AFTER the fallout swap so it wins at equal specificity):
   the Sign In button replaces Stake/Unstake/Contribute whenever the row has
   data-signedout="true" (set in renderNukeHeaderBalance) — i.e. no connected
   wallet. Takes precedence over data-fallout, so an NPC during nuclear fallout
   still sees Sign In rather than Contribute. */
.nuke-container .nuke-chat-action-btns[data-signedout="true"] .nuke-chat-stake-btn,
.nuke-container .nuke-chat-action-btns[data-signedout="true"] .nuke-chat-unstake-btn {
  display: none;
}
/* Signed-out also hides the fallout contribute row (its preceding sibling carries
   data-signedout), so a logged-out user during fallout sees only Sign In. */
.nuke-container .nuke-chat-action-btns[data-signedout="true"] ~ .nuke-chat-contribute-row {
  display: none;
}
.nuke-container .nuke-chat-action-btns[data-signedout="true"] .nuke-chat-signin-btn {
  display: inline-flex;
}
/* Full-page dark overlay (x.com style): fixed over the ENTIRE viewport, above all
   page chrome — darkens everything and floats the image on top. It's a direct child
   of .nuke-container so `fixed` escapes the stage's transform. (Blur set on the id.) */
.nuke-container .nuke-chat-lightbox {
  position: fixed;
  inset: 0;
  z-index: 100;
  background: rgba(0, 0, 0, 0.95);
}
.nuke-container .nuke-chat-lightbox-close {
  width: 36px;
  height: 36px;
  border-radius: 9999px;
  background: rgba(var(--nk-surface-rgb), 0.1);
  color: #FFFFFF;
}
.nuke-container .nuke-chat-lightbox-close:hover {
  background: rgba(var(--nk-surface-rgb), 0.2);
  color: #FFFFFF;
}
.nuke-container .nuke-chat-lightbox-close .ri-close-line {
  font-size: calc(20px - var(--font-shrink, 0px));
}
.nuke-container .nuke-chat-lightbox-img {
  /* Squared in the modal — the 2px feed rounding is a small-card affordance; the full-size preview reads
     cleaner with hard edges. */
  border-radius: 0;
}
/* Prev/next swipe arrows — hidden unless a multi-image gallery is open. */
.nuke-container .nuke-chat-lightbox-nav {
  position: absolute;
  top: 50%;
  transform: translateY(-50%);
  align-items: center;
  justify-content: center;
  width: 46px;
  height: 46px;
  border: none;
  border-radius: 9999px;
  background: rgba(var(--nk-surface-rgb), 0.14);
  color: #FFFFFF;
  cursor: pointer;
  z-index: 110;
  display: none;
}
.nuke-container .nuke-chat-lightbox-nav:hover { background: rgba(var(--nk-surface-rgb), 0.26); }
.nuke-container .nuke-chat-lightbox-nav i { font-size: calc(24px - var(--font-shrink, 0px)); }
.nuke-container .nuke-chat-lightbox-prev { left: 16px; }
.nuke-container .nuke-chat-lightbox-next { right: 16px; }
.nuke-container .nuke-chat-lightbox.has-gallery .nuke-chat-lightbox-nav { display: flex; }

/* --- Contact list / chat container chrome (white squares) ------ */

/* Contact list container — overrides `.inset-box`'s asymmetric XP-inset
   borders with the same 1px gray-7 ring + 6px corner radius the search
   bar uses, so they read as a matched chrome family. */
.nuke-container .nuke-contact-list,
.nuke-container .nuke-chat-container {
  border: none;
  border-radius: 6px;
  box-shadow: none; /* border ring removed */
}

/* --- Staking panel (left player card) -------------------------- */
.nuke-container .nuke-staking {
  flex: 1;
  min-height: 0;
  display: flex;
  flex-direction: column;
  gap: 8px;
  padding: 0;
  font-family: var(--font-conduit);
}

/* --- Season Pass panel (left profile card) --------------------- */
/* 2x2 stat grid — mirrors the staking grid. */
.nuke-container .nuke-pass-grid {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 6px;
}

/* --- Middle stage browser-style tabs --------------------------- */

/* Tab strip — Chrome-style. Height matches the contact-list
   search-bar header exactly: that header is `min-height: 38px` plus
   `py-1.5` (6 + 6 = 12px padding), so its effective height is 44px.
   We size to 44px here and leave a 6px top inset that matches the
   horizontal `px-1.5`. Tabs stretch to fill the remaining 38px. */
.nuke-container .nuke-stage-tabs {
  height: 46px;
  box-sizing: border-box;
  padding-top: 6px;
  padding-bottom: 0;
  /* Override the strip's Tailwind `px-1.5` left padding so the action
     button sits flush against the container edge. */
  padding-left: 0;
}
/* Manual left-panel collapse (instant, no transition). Slides the left panel
   off-screen; the other columns reclaim the space. */
.nuke-container .nuke-stage {
  transition: none;
}
.nuke-container.is-left-collapsed .nuke-side-right {
  left: var(--page-edge-x);
}
.nuke-container.is-left-collapsed .nuke-stage {
  padding-left: calc(var(--page-edge-x) + var(--right-col-w) + 8px);
}
/* Each tab spans the full strip height, fixed wider width. Rounded
   top corners, flat bottom — visually attaches to the content area
   below it like a Chrome browser tab. Holds an icon + title pair. */
.nuke-container .nuke-stage-tab {
  position: relative;
  width: 200px;
  height: 100%;
  padding: 0 12px;
  margin-top: 1px;
  background: transparent;
  border: 1px solid transparent;
  border-bottom: none;
  border-radius: 8px 8px 0 0;
  cursor: pointer;
  display: flex;
  align-items: center;
  gap: 6px;
  -webkit-appearance: none;
  appearance: none;
}

.nuke-container .nuke-stage-tab:not(.is-active):hover::before {
  content: '';
  position: absolute;
  inset: 0 6px 7px 6px;
  background: #C8DDF2;
  border-radius: 8px;
  pointer-events: none;
}
/* Tabs that follow another tab overlap their left edge by 1px so
   adjacent borders don't double-stack into a visible 2px seam. */
.nuke-container .nuke-stage-tab + .nuke-stage-tab {
  margin-left: -1px;
}
/* Inactive-tab hover — solid white pill rounded on all four sides.
   The highlight lives on a ::before pseudo, inset 6px from the left
   and right edges of the tab so it matches the strip's 5px
   padding-top + 1px tab margin-top top gap. Pseudo + opacity
   transition for the hover fade is now defined alongside the
   base `.nuke-stage-tab` rule earlier in this file. */
/* Active tab — brighter bg that matches the stage-box surface
   (#FAFAFA), and a 1px negative bottom margin so the tab's bottom
   edge overlaps the toolbar's bottom border, "merging" with the
   content area below. */
.nuke-container .nuke-stage-tab.is-active {
  background-color: #E6F1FF;
  /* Background-image stack — first listed paints LAST (on top).
     1. Bg-colored cover from bottom up to the midpoint — sits above
        the dark shading and masks it out below 50% height.
     2. White top edge highlight.
     3/4/5. Dark shading from top/left/right edges (paints under #1). */
  background-image:
    linear-gradient(to top, #E6F1FF 0%, #E6F1FF 30%, transparent 60%),
    linear-gradient(to bottom, rgba(var(--nk-surface-rgb), 0.6) 0%, transparent 4px),
    linear-gradient(to bottom, rgba(0, 0, 0, 0.05) 0%, transparent 4px),
    linear-gradient(105deg, rgba(0, 0, 0, 0.04) 0%, transparent 10px),
    linear-gradient(255deg, rgba(0, 0, 0, 0.04) 0%, transparent 10px);
  border-color: #C8DDF2;
  margin-bottom: -1px;
  z-index: 1;
}
/* Outward "feet" — only on the active tab. Each pseudo is an 8×8
   transparent square sitting just outside the tab's bottom corner.
   Only two adjacent sides get a border, and the corner between them
   is fully rounded — so the visible result is just a thin
   quarter-circle arc OUTLINE (no fill), bulging outward from the tab
   like the rounded outer edge of a circle. */
.nuke-container .nuke-stage-tab.is-active::before,
.nuke-container .nuke-stage-tab.is-active::after {
  content: '';
  position: absolute;
  bottom: 0;
  width: 8px;
  height: 8px;
  background: transparent;
  pointer-events: none;
  box-sizing: border-box;
}
.nuke-container .nuke-stage-tab.is-active::before {
  left: -8px;
  background:
    radial-gradient(circle 8px at top left, transparent 7.5px, #C8DDF2 7.5px, #C8DDF2 8.5px, transparent 8.5px),
    radial-gradient(circle 8px at top left, transparent 8px, #E6F1FF 8px);
}
.nuke-container .nuke-stage-tab.is-active::after {
  right: -8px;
  background:
    radial-gradient(circle 8px at top right, transparent 7.5px, #C8DDF2 7.5px, #C8DDF2 8.5px, transparent 8.5px),
    radial-gradient(circle 8px at top right, transparent 8px, #E6F1FF 8px);
}
/* Grow the profile column's tab strip so the larger profile tab fits
   (50px tab + 6px top inset). The shop/chat strips stay 46px. */
/* All three panel headers share the taller 56px strip so the Profile, Shop and
   Chat tab headers line up at the same height (was 46px for Shop/Chat). */
.nuke-container .nuke-side-right .nuke-stage-tabs,
.nuke-container .nuke-stage .nuke-stage-tabs {
  height: 56px;
}
/* Tiled motif background on the chat container — `repeat` lays
   `motif.png` over a near-white surface (#FAFAFA) as a subtle
   pattern. The `!important` overrides Tailwind's `bg-white` set on
   the element. */
.nuke-container .nuke-chat-container {
  /* Wallpaper = the same page bg + brilliant.png texture + 200px top-fade as the right panel, so the feed
     canvas matches it; transitions smoothly when the page re-themes (visiting a profile / post). */
  background-color: var(--page-bg-themed, var(--nk-surface));
  background-image:
    url('/img/ui/brilliant.png'),
    linear-gradient(to bottom in oklab, var(--page-grad-top) 0, transparent 200px);
  background-repeat: repeat, no-repeat;
  background-position: 0 0, 0 0;
  background-size: auto, auto;
  background-attachment: scroll, scroll;
  transition: background-color 0.4s ease, --page-grad-top 0.4s ease;
  isolation: isolate;
}
/* A subview overlay (/post, /profile, /notif) has a TRANSPARENT background, so hide the chat panel's OWN
   contents (radio header + feed) behind it — only the panel background (the textured page bg) shows through.
   Everything except the overlay itself is hidden; visibility (not display) keeps the feed's scroll position. */
.nuke-container .nuke-chat-container:has(.nuke-post-modal-overlay) > *:not(.nuke-post-modal-overlay) {
  visibility: hidden;
}
/* SHARED PINNED BAR — the /post page (only) keeps the feed's radio/stream header VISIBLE and floats it
   ABOVE the post overlay (z above the overlay's z-40), then drops the post content below it by the header's
   live height (--nuke-chat-header-h, set by syncHeaderPad). Other subviews keep the header hidden. */
.nuke-container .nuke-chat-container:has(.nuke-post-modal-overlay[data-subview="post"]) > .nuke-chat-header {
  visibility: visible;
  z-index: 50;
}
.nuke-container .nuke-post-modal-overlay[data-subview="post"] {
  padding-top: var(--nuke-chat-header-h, 0px);
}
/* Chat panel — no tab header, no blue window-frame: the white chat fills the
   middle column with X-style vertical dividers (left + right border only, no
   top or bottom). */
/* ── CENTER-PANEL BORDER — single source of truth ─────────────────────────────
   Every center-column page (chat, post, profile, notifications, season …) draws its
   left/right dividers from this ONE token. Change it here and they ALL change — no
   loose ends. Derived from the themed --nk-border-soft, so it flips per theme too. */
body.page-nuke {
  /* ONE knob for EVERY center-panel line: the vertical edges (--nk-panel-border,
     below) AND all horizontal dividers (message / header / tab separators, which use
     `var(--nk-panel-border-width) solid <their own colour>`). Change this, everything moves. */
  --nk-panel-border-width: 1px;
  --nk-panel-border: var(--nk-panel-border-width) solid var(--nk-border-soft);
}

.nuke-container .nuke-stage .window-frame {
  border: none;
  /* Side dividers use the shared --nk-panel-border token (see above). */
  border-left: var(--nk-panel-border-width) solid var(--nk-border-2);   /* left divider — #E0E0E0 in light, darkens in dark */
  border-right: var(--nk-panel-border);   /* default profile border colour */
  border-radius: 0;
  background: none;
  box-shadow: none;
}
/* The chat's frosted header/footer have `backdrop-filter: blur`, and the
   window-frame's side border sits in their backdrop — so the blur smears it at
   the edges. Drop the border from the CHAT window-frame only (`:has` keeps the
   profile panel's border intact) and redraw the dividers crisply on top via
   `.nuke-chat-container::after` below, above the chrome's z-index. */
.nuke-container .nuke-stage .window-frame:has(.nuke-chat-container) {
  border-left: none;
  border-right: none;
  /* Square top — the panel now reaches the page top (was rounded --pfp-radius). */
  border-radius: 0;
}
.nuke-container .nuke-stage .nuke-chat-container {
  margin: 0;
  /* Square corners + no ring — the ::after overlay frames the panel. */
  border-radius: 0;
  box-shadow: none;
}
/* Crisp side + top dividers painted ABOVE the frosted header/footer (z-index 6 >
   the 5 they use) so the backdrop blur can't reach them. Same var(--nk-border-soft) hairline as
   the per-message underline; full height; click-through.
   The message cards are INSET from the panel edges (side gutter), so these full-height side dividers
   frame the whole chat panel without touching the cards' own borders. Profile view inherits the same. */
/* The panel side dividers were MOVED off this frame onto the individual elements (posts, radio header,
   stream, nav bars, profile info) as their own left/right borders — so this overlay no longer paints them. */
.nuke-container .nuke-stage .nuke-chat-container::after {
  content: '';
  position: absolute;
  inset: 0;
  border-radius: 0;
  pointer-events: none;
  z-index: 6;
}
/* Motif texture, faded to 0.6 over the blue fill — a background-image can't
   take its own opacity, so it lives on this overlay instead. Behind all chat
   content via the negative z-index within the isolated container. */
.nuke-container .nuke-chat-container::before {
  display: none; /* motif removed for the white feed */
}

/* --- Overlay scrollbars (contact list + chat) ------------------ */

/* True overlay scrollbar: hide the native one entirely and float a
   custom thumb inside the wrapper, JS-positioned. */
.nuke-container .nuke-contact-list-wrap {
  position: relative;
  display: flex;
  min-height: 0;
  /* White bg so macOS rubber-band overscroll reveals the same color
     as the list itself, not the light-blue `#E6F1FF` window frame. */
  background: var(--nk-surface);
  border-radius: 6px;
}
.nuke-container .nuke-contact-list-wrap .nuke-contact-list {
  overscroll-behavior: none;
}
.nuke-container .nuke-contact-list-wrap .nuke-contact-list {
  flex: 1;
  scrollbar-width: none; /* Firefox */
  /* Breathing room above the first group label so "Online" doesn't
     kiss the container's top edge. */
  padding-top: 4px;
  /* `contain: layout paint` tells the browser nothing inside affects
     layout/paint outside this element. During viewport resize, the
     layout pass doesn't have to walk into the contact-list subtree
     to recompute anything — big win when the list has many rows. */
  contain: layout paint;
}
.nuke-container .nuke-contact-list-wrap .nuke-contact-list::-webkit-scrollbar {
  display: none; /* Chromium */
}
.nuke-container .nuke-contact-list-thumb {
  position: absolute;
  /* Top inset much larger than the bottom — leaves visible breathing
     room below the group header. JS uses matching INSET_TOP/_BOTTOM
     constants to clamp the thumb travel. */
  top: 26px;
  right: 2px;
  width: 8px;
  /* Matches the "Connected" footer strip's top border (#D1D1D1). */
  background: #D1D1D1;
  border-radius: 4px;
  pointer-events: none;
  opacity: 0;
  transition: opacity 0.2s ease;
  /* JS sets `height` and `transform: translateY(...)` based on
     scrollTop. Hidden by default; only shown when overflow exists. */
}
.nuke-container .nuke-contact-list-wrap.is-scrolling .nuke-contact-list-thumb {
  opacity: 1;
}
/* Chat scroll — same overlay scrollbar chrome as the contact list,
   scoped to the chat conversation window's scroller. The scroller is
   absolutely positioned inside `.nuke-chat-wrap` (so it can fill the
   white container regardless of the floating disclaimer toast above
   it). The thumb hides the native bar and tracks scroll via JS. */
.nuke-container .nuke-chat-wrap > #nuke-chat-scroll {
  /* Edge-to-edge posts (override the element's Tailwind px-3 py-2 gap-2). Top padding clears the
     pinned RADIO header ONLY — the profile/contribute section now scrolls WITH the feed (moved into this
     scroll as its first child). paddingTop is synced to the header's real height in JS
     (initNukeChatHeaderCollapse); 44px is just the pre-JS fallback. */
  /* No side inset now: the panel side frame was moved OFF the .nuke-chat-container::after divider onto the
     posts / stream / radio header as their own real side borders, so content runs to the panel edge and
     the frame sits exactly where the ::after divider used to (frame position unchanged). */
  /* No bottom padding: the last (oldest) card's 2px bottom border sits flush against the panel's
     bottom edge — otherwise a bare 12px gap showed below it when scrolled to the end. */
  padding: 44px 0 0;
  gap: 0;
  overscroll-behavior: none;
  scrollbar-width: none;
  /* Same containment hint as `.nuke-contact-list` — the chat-message
     subtree (often dozens of rendered messages, images, animated
     emoji) is isolated from outer-layout work. */
  contain: layout paint;
  /* Own compositor layer — pins the animated-emoji repaints inside the
     chat to their own raster tile so they can't dirty the blurred panel
     headers / stage (the page-wide zoom otherwise shares one raster). */
  transform: translateZ(0);
}
.nuke-container .nuke-chat-wrap > #nuke-chat-scroll::-webkit-scrollbar {
  display: none;
}

/* No visible scrollbars anywhere in the app — scrolling still works (wheel / trackpad),
   matching the chat's existing bar-less look. */
.nuke-container,
.nuke-container * { scrollbar-width: none; }
.nuke-container::-webkit-scrollbar,
.nuke-container *::-webkit-scrollbar { display: none; }
.nuke-container .nuke-chat-scroll-thumb {
  /* Scrollbar removed — chat still scrolls (wheel/trackpad), just no visible
     bar (native bar is already hidden via `scrollbar-width: none`). */
  display: none;
  position: absolute;
  top: 44px;
  right: 2px;
  width: 8px;
  background: #D1D1D1;
  border-radius: 4px;
  pointer-events: none;
  opacity: 0;
  transition: opacity 0.2s ease;
}
.nuke-container .nuke-chat-wrap.is-scrolling .nuke-chat-scroll-thumb {
  opacity: 1;
}

.nuke-container .nuke-chat-scroll-btn {
  position: absolute;
  /* x.com "back to top / show new posts" pill, horizontally centered, resting just below the pinned
     feed-tabs header (60px + 8px gap). SAME colour as the header BACK BUTTON: a bright, saturated
     INVERTED (h+180) take on your profile hue. Colourless → the *-themed var is invalid → #E0E0E0
     (the back button's neutral fallback). Hover is a slightly darker shade of the same. */
  --pill-color-themed: oklch(from var(--current-user-color) 0.82 0.16 calc(h + 180));
  --pill-color: var(--pill-color-themed, #E0E0E0);
  --pill-color-hover-themed: oklch(from var(--current-user-color) 0.72 0.16 calc(h + 180));
  --pill-color-hover: var(--pill-color-hover-themed, #D0D0D0);
  top: 68px;
  left: 50%;
  height: 34px;
  padding: 0 4px;
  box-sizing: border-box;
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 7px; /* sender-pfp cluster → arrow */
  background: var(--pill-color);
  border: none;
  border-radius: var(--pfp-radius); /* rounded rectangle, matching the pfp corner radius (was a pill) */
  box-shadow: 0 2px 10px rgba(0, 0, 0, 0.22);
  flex-shrink: 0;
  opacity: 0;
  /* Slides down from under the header; translateX keeps it centered. */
  transform: translateX(-50%) translateY(-8px);
  pointer-events: none;
  transition: opacity 0.15s ease, transform 0.15s ease;
  z-index: 15;
}

.nuke-container .nuke-chat-scroll-btn-icon {
  font-size: calc(18px - var(--font-shrink, 0px));
  color: #FFFFFF;
}
/* Sender pfps on the pill (who sent the buffered new posts) — a small overlapping cluster to the
   left of the arrow. Empty span collapses (no gap) when there are none. */
.nuke-container .nuke-chat-scroll-btn-pfps {
  display: flex;
  align-items: center;
}
.nuke-container .nuke-chat-scroll-btn-pfps:empty { display: none; }
.nuke-container .nuke-chat-scroll-btn-pfp {
  width: 22px;
  height: 22px;
  border-radius: var(--pfp-radius); /* rounded square, same proportional radius as the feed pfps */
  object-fit: cover;
  background: var(--nk-surface-2);
  box-shadow: 0 0 0 1.5px var(--pill-color); /* ring in the pill colour so overlaps read cleanly */
}
.nuke-container .nuke-chat-scroll-btn-pfp + .nuke-chat-scroll-btn-pfp { margin-left: -8px; }
.nuke-container .nuke-chat-scroll-btn:hover .nuke-chat-scroll-btn-pfp { box-shadow: 0 0 0 1.5px var(--pill-color-hover); }

.nuke-container .nuke-chat-scroll-btn.is-visible {
  opacity: 1;
  transform: translateX(-50%) translateY(0);
  pointer-events: auto;
}

.nuke-container .nuke-chat-scroll-btn:hover {
  background: var(--pill-color-hover);
}

/* --- Contact list groups (online / offline) -------------------- */

/* Contact rows + group headers: rounded corners so the hover bg-fill
   reads as a soft pill rather than a sharp rectangle. */
.nuke-container .nuke-contact-list .contact-group-header,
.nuke-container .nuke-contact-list .contact-group-inner > div {
  border-radius: 6px;
}

/* =============================================================
   Contact group collapse / expand animation
   =============================================================
   COLLAPSE (snappy, fade-only):
     • Each row: opacity 1 → 0 over 100ms, no visible scale change
     • Bottom-to-top stagger: 25ms between rows, capped at 4 rows
     • Container height shrinks 200ms after the fade chain starts

   EXPAND (fancy spring pop-in):
     • Container expands first (180ms)
     • Then each row pops: opacity 0 → 1 over 150ms,
       transform scale(0.7) → scale(1) over 320ms with back-overshoot
     • Top-to-bottom stagger: 60ms between rows, capped at 4 rows

   How the "no scale on collapse" trick works: in the closed state, the
   transform transition has 0s duration AND a delay equal to the fade
   chain — so the scale snap to 0.7 happens INVISIBLY (opacity is
   already 0). On re-expand, the row is primed at scale(0.7) and gets
   the spring-back animation.

   Per-row stagger is driven by `--stagger`, set per-state with calc()
   on `--row-idx` / `--row-from-bottom` (custom props set inline by JS).
   ============================================================= */

/* Container height collapse/expand. */
.nuke-container .nuke-contact-list .contact-group-content {
  display: grid;
  grid-template-rows: 1fr;
}
.nuke-container .nuke-contact-list .contact-group[data-open="false"] .contact-group-content {
  grid-template-rows: 0fr;
}
.nuke-container .nuke-contact-list .contact-group-inner {
  overflow: hidden;
  min-height: 0;
  padding-top: 6px;
}

/* Open / closed states for rows — no transitions, just instant
   visibility toggling. */
.nuke-container .nuke-contact-list .contact-group[data-open="false"] .contact-group-inner > div {
  opacity: 0;
}

/* Caret rotates 90° counter-clockwise (points right) when collapsed. */
.nuke-container .nuke-contact-list .contact-group[data-open="false"] .contact-group-header .ri-arrow-down-s-line {
  transform: rotate(-90deg);
}

/* --- Search-bar input chrome ----------------------------------- */

/* ===== Rebuilt player search — ONE column element ====================
   Collapsed = a fixed-height input row (icon + input). On hover/focus the results
   list expands BELOW it (the bar grows downward + gains a drop shadow) and the page
   darkens via `.nuke-hover-spotlight`. The input row is identical in both states so
   the placeholder/text never shifts; the bar is position:fixed at the SAME spot in
   both states (see `.nuke-subheader .user-search-bar` below). */
.nuke-container .nuke-search-row .user-search-bar {
  display: flex;
  flex-direction: column; /* input row + results, ALWAYS — no row→column flip */
  width: 100%;
  /* `auto` (not the base bar's fixed 45px) so the bar GROWS to fit the results
     when they un-hide — otherwise overflow:hidden below clips them and the
     dropdown never appears. Collapsed height = 40px input row + shared 1px border top/bottom = 42px (matches the radio bar). */
  height: auto;
  /* Slightly darker than the surface/dropdown so the bar reads as a filled field. */
  background: color-mix(in srgb, var(--nk-surface), #000 2%);
  /* Recessed/indented field — no flat border; the inset shadow below carves the edge. */
  border: none;
  box-sizing: border-box;
  border-radius: 4px;
  overflow: visible; /* the detached dropdown floats below the bar — nothing to clip */
  /* Inset shadow: inner edge (all sides) + a top-heavy inner shadow = 'indented in'. */
  box-shadow: inset 0 0 0 1px rgba(0, 0, 0, 0.10), inset 0 2px 3px rgba(0, 0, 0, 0.16);
  transform: none; /* suppress the base bar's --cc-lift/--cc-press scale-on-hover */
  z-index: 40; /* above the .nuke-hover-spotlight scrim */
}
.nuke-container .nuke-search-row .user-search-bar:hover,
.nuke-container .nuke-search-row .user-search-bar:focus-within {
  /* Keep the same inset/indented look on hover/focus (no change). */
  box-shadow: inset 0 0 0 1px rgba(0, 0, 0, 0.10), inset 0 2px 3px rgba(0, 0, 0, 0.16);
}
/* Fixed-height input row — IDENTICAL collapsed and expanded, so the input text
   never shifts when the panel opens. Holds the icon (absolute) + the input. */
.nuke-container .user-search-inputrow {
  position: relative;
  flex: 0 0 36px;
  height: 36px;
  display: flex;
  align-items: center;
}
/* Magnifying glass anchored to the left of the input row, vertically centered. */
.nuke-container .nuke-search-row .nuke-search-icon {
  position: absolute;
  top: 50%;
  left: 14px;
  transform: translateY(-50%);
  color: #808080;
  width: 16px;
  height: 16px;
  font-size: calc(16px - var(--font-shrink, 0px));
  pointer-events: none;
  z-index: 2;
}
/* SINGLE SOURCE OF TRUTH for the search input (input here, placeholder just below). */
.nuke-container .nuke-search-row .user-search-input {
  flex: 1;
  min-width: 0;
  height: 100%;
  border: none;
  border-radius: inherit;
  outline: none;
  background: transparent;
  padding: 0 14px 0 38px; /* left clears the icon (14 + 16 + 8) */
  /* Caret matches the search bar's hovered/dropdown border ring. */
  caret-color: color-mix(in srgb, var(--current-user-color, #6a6a6a) 45%, var(--color-gray-6));
  font-family: var(--font-conduit); /* site typeface (Sunghyun Sans), not Tahoma */
  font-size: calc(14px - var(--font-shrink, 0px));
  font-style: normal;
  font-weight: 500;
  color: #808080;
}
.nuke-container .nuke-search-row .user-search-input::placeholder {
  font-family: var(--font-conduit); /* site typeface (Sunghyun Sans), not Tahoma */
  font-size: calc(13px - var(--font-shrink, 0px));
  font-style: italic;
  /* Match the chat @handle (.nuke-post-handle): weight 400 + --nk-text-2 colour. */
  font-weight: 400;
  color: var(--nk-text-2);
  opacity: 1;
}

/* --- Search bar focus expansion ------------------------------------
   When focused, the sub-header search pill drops down into the player
   list as a panel and its edges un-round from the full pill to the 6px
   radius the page-content containers use. The bar is absolutely
   positioned over a fixed-height wrapper (top-anchored) so the header
   layout stays put and the input + icon stay pinned to the top while the
   panel grows below them. */
.nuke-container .nuke-subheader-search .user-search-wrapper {
  position: relative;
  height: 42px; /* reserves the collapsed bar height (matches the radio bar) */
}
.nuke-container .nuke-subheader .user-search-bar {
  /* Fixed at the EXACT SAME spot as the focused/expanded state below, so the bar
     and its expanded dropdown are ONE continuous element that simply grows
     (height 44px → auto) on focus — no `absolute → fixed` position swap, which was
     what made the collapsed bar and the dropdown behave as two separate things.
     The wrapper still reserves the 44px collapsed height for layout. */
  position: fixed;
  top: calc((var(--page-header-h) - 38px) / 2); /* vertically centre the 38px collapsed bar in the header band (auto-adjusts with --page-header-h) */
  left: auto;
  right: calc(50% + var(--trio-shift) - var(--right-col-w) - var(--chat-w) / 2 - var(--col-gap) + var(--right-box-inset));
  width: calc(var(--right-col-w) - 2 * var(--right-box-inset));
  border-radius: 4px;
}
/* The rebuilt bar needs NO focus-state layout overrides — the input row is a
   constant 44px and the bar grows simply by un-hiding the results below (see the
   `.nuke-search-results` toggle). Expand shadow is on `:hover/:focus-within` above. */
/* Player-list results below the input, in normal flow so the panel sizes to
   them; only shown while focused. It's a `.nuke-contact-list-wrap` clone, so
   the inner `.nuke-contact-list` scrolls with the same overlay scrollbar as
   the real player list. */
/* No animation — the results just show/hide instantly on hover/focus. */
.nuke-container .nuke-search-results {
  display: none;
  min-height: 0;
  /* DETACHED dropdown: floats 6px BELOW the input bar as its own squared, bordered box (was in-flow
     inside the bar, growing it into one continuous element). */
  position: absolute;
  top: calc(100% + 6px);
  /* Match the bar's OUTER edges exactly (0/0, not -1px) so the dropdown is the same
     width as the search bar — and therefore the Top Users side box, which the bar aligns to. */
  left: 0;
  right: 0;
  box-sizing: border-box;
  background: var(--nk-surface);
  border: 1px solid var(--nk-border-soft);   /* same 1px border as the search bar */
  border-radius: 0;   /* squared edges */
  overflow: hidden;
  box-shadow: 4px 4px 0 0 rgba(0, 0, 0, 0.2);   /* hard offset shadow, matching the cards inside */
}
.nuke-container .nuke-search-clip {
  min-height: 0;
}
/* Dropdown shows only while focused AND the input actually has text (`:not(:placeholder-shown)`), so an
   empty box drops NOTHING down — results appear only as you type. Pure CSS, no JS/render timing. */
.nuke-container .nuke-subheader-search:focus-within:has(.user-search-input:not(:placeholder-shown)) .nuke-search-results {
  display: block;
}
/* No profile-card banners in the search dropdown (removed per request; also drops
   their per-row mask/grayscale paint). The main player list keeps its banners. */
.nuke-container .nuke-search-results .nuke-row-banner,
.nuke-container #nuke-top-players .nuke-row-banner {
  display: none;
}
.nuke-container .nuke-search-results .nuke-contact-list {
  /* Drop the inner 1px ring (the panel's own border frames it) and cap the
     list at the available height down to the chat bottom — short lists size to
     their rows; long ones scroll. 69px = 9px top + 44px input + 16px bottom. */
  box-shadow: none;
  border-radius: 4px;
  max-height: calc((100vh - 69px) / 2 + 64px);
  /* Match the Top Users side box's 12px left/right inner padding: with the rows'
     own 8px margin this lands the cards at the same 20px inset from the edge. */
  padding-left: 12px;
  padding-right: 12px;
}
/* No scrollbar in the search dropdown — hide the custom overlay thumb (native bar already hidden). */
.nuke-container .nuke-search-results .nuke-contact-list-thumb { display: none; }
.nuke-container .nuke-search-results .nuke-contact-list { scrollbar-width: none; }

/* --- Simplified PFP: square + 1px border, NO shading ----------- */
/* The /home pfp design layers a domed gloss (::before), a lit rim (::after),
   and a drop shadow on every avatar. On /nuke we want a flat, simple thumbnail,
   so: (1) hide the gloss + rim pseudo-elements on every pfp wrapper, and
   (2) collapse the img's box-shadow to just the outer border ring (drop the
   `--pfp-shadow`). Rounded corners + the ring stay. */
/* Avatars are slightly-rounded squares: override the design-system's
   --pfp-radius (default 16% in styles.css :root) at the /nuke root. The pfps vary
   widely in size (20px reply quotes → 56px hovercard), so use a BOUNDED
   percentage via clamp(min, %, max): the % scales the corner proportionally in
   the middle while the min keeps it from vanishing on the tiniest avatars and
   the max keeps it from over-rounding the biggest — so every size reads as the
   same gentle corner (effective range ~3–6px). A plain % left the 20px ones
   looking square and a plain px made them look rounder than the big ones.
   Cascades to every pfp that reads the var (.cc-pfp, .msg-pfp,
   .nuke-hovercard-pfp, .pill-pfp, etc.). Single source of truth — tune here.
   Scoped to .nuke-container so prod is untouched. */
.nuke-container {
  --pfp-radius: clamp(3px, 13%, 5px);
}
/* PFPs FULLY UN-ROUNDED (sharp squares) site-wide. Scope --pfp-radius to 0 on the avatar elements only, so
   it cascades to each pfp image, its gloss pseudos, and the box-shadow ring (all read the var) WITHOUT
   touching the other surfaces that borrow --pfp-radius for their rounded-edge look (chat images, reply boxes,
   banners, the search cluster). To re-round: delete this block. */
.nuke-container .cc-pfp-wrapper,
.nuke-container .cc-pfp,
.nuke-container .pill-pfp,
.nuke-container .pill-pfp-img,
.nuke-container .nuke-radio-bar-pfp,
.nuke-container .nuke-chat-scroll-btn-pfp,
.nuke-container .nuke-hovercard-pfp,
.nuke-container .nuke-profile-page-pfp,
.nuke-container .nuke-profile-post-pfp,
.nuke-container .msg-pfp-frame,
.nuke-container .msg-pfp,
.nuke-container .profile-panel-pfp-wrapper,
.nuke-container .profile-panel-pfp {
  --pfp-radius: 0px;
}
.nuke-container .cc-pfp-wrapper::before,
.nuke-container .cc-pfp-wrapper::after,
.nuke-container .msg-pfp-frame::before,
.nuke-container .msg-pfp-frame::after,
.nuke-container .profile-panel-pfp-wrapper::before,
.nuke-container .profile-panel-pfp-wrapper::after,
.nuke-container .nuke-post-pfp::before,
.nuke-container .nuke-post-pfp::after,
.nuke-container .nuke-row-pfp::before,
.nuke-container .nuke-row-pfp::after,
.nuke-container .nuke-post-quote-pfp::before,
.nuke-container .nuke-post-quote-pfp::after,
.nuke-container .nuke-hovercard-pfp::before,
.nuke-container .nuke-hovercard-pfp::after,
.nuke-container .pill-pfp::before,
.nuke-container .pill-pfp::after {
  display: none !important;
}
/* Belt-and-suspenders: kill the gloss gradient at the source so even an
   unforeseen ::before that slips through paints nothing. */
.nuke-container .cc-pfp-wrapper,
.nuke-container .msg-pfp-frame,
.nuke-container .profile-panel-pfp-wrapper,
.nuke-container .pill-pfp {
  --pfp-gloss: none;
  --pfp-rim-top: transparent;
  --pfp-rim-side: transparent;
  --pfp-rim-bottom: transparent;
}
.nuke-container .cc-pfp,
.nuke-container .msg-pfp,
.nuke-container .profile-panel-pfp,
.nuke-container .pill-pfp-img {
  /* Borderless — no outer ring (was a 1px box-shadow ring). NO gloss, rim,
     or drop shadow either, just the flat rounded image. */
  box-shadow: none !important;
}
/* Hover popup + reply quote pfps: borderless too. */
.nuke-container .nuke-hovercard-pfp .cc-pfp,
.nuke-container .nuke-post-quote-pfp .cc-pfp {
  box-shadow: none !important;
}

/* --- Profile / row PFPs ---------------------------------------- */

/* Row PFPs in the contact list — same PFP design system chrome
   (gloss/rim via the existing :before/:after selector lists) at
   a much smaller size. The right-edge status dot reuses the same
   placement pattern as the header PFP. */
.nuke-container .nuke-row-pfp {
  width: 40px;
  height: 40px;
  margin: 0;
}
.nuke-container .nuke-row-pfp .cc-pfp {
  width: 100%;
  height: 100%;
  /* Match the chat message avatar: a real 1px border + the shared --pfp-radius. */
  box-sizing: border-box;
  border: var(--pfp-outer-border-width, 1px) solid var(--card-border, var(--nk-border));
  border-radius: var(--pfp-radius);
}
/* Search-dropdown usernames stay dark (chat parity); the offline #666 muting was for the
   now-removed side panel. */
.nuke-container .nuke-search-results .nuke-row[data-status="offline"] .nuke-row-name,
.nuke-container #nuke-top-players .nuke-row[data-status="offline"] .nuke-row-name { color: var(--nk-text); }
/* Profile-card usernames use the SAME themed colour a post gives the author (SETTING 6 from that user's own
   hue, --nuke-cmd-color set per row) — regardless of online/offline, matching the post container. */
.nuke-container .nuke-search-results .nuke-row[data-status] .nuke-row-name,
.nuke-container #nuke-top-players .nuke-row[data-status] .nuke-row-name {
  --un-6: oklch(from var(--nuke-cmd-color) var(--set-6-l) var(--set-6-c) h);
  color: var(--un-6, var(--nk-text));
}
/* Status dot hangs on the bottom-right corner of the PFP — pushed
   down + right so it overlaps the visual ring with a slight outside
   overhang (centered roughly on the corner of the visual PFP). */
/* .nuke-row-pfp dot: size + offset are proportional now (base .status-dot). */

/* --- Row banner image (hover-revealed) ------------------------- */

/* Random banner image on the right side of every row, full row
   height × 160px wide (matches the prod calling-card 4:1 banner ratio
   scaled to a 40px row). Grayscale, low opacity, hidden at rest, fades
   in with the hover bg highlight. Sits BEHIND the row's normal-flow
   content via z-index: -1 inside the row's `isolate` stacking context,
   so it never paints over the PFP or text. */
.nuke-container .nuke-row-banner {
  position: absolute;
  top: 0;
  right: 0;
  width: 200px;
  height: 54px;
  border-radius: 6px;
  overflow: hidden;
  pointer-events: none;
  z-index: -1;
  opacity: 0;
  transition: opacity 0.2s ease;
  /* Fade the banner out on the left so it never crowds the text column —
     mirrors the prod `.cc-banner-img` mask. */
  mask-image: linear-gradient(to right, transparent 0%, rgba(0,0,0,0.4) 35%, black 75%);
  -webkit-mask-image: linear-gradient(to right, transparent 0%, rgba(0,0,0,0.4) 35%, black 75%);
}
.nuke-container .nuke-row:hover .nuke-row-banner {
  opacity: 1;
}
.nuke-container .nuke-row-banner img {
  width: 100%;
  height: 100%;
  object-fit: fill;
  filter: grayscale(1);
  opacity: 0.4;
}

/* --- Pill PFPs inside chat content ----------------------------- */

/* `.pill-pfp` inside the nuke chat — sized 1px smaller than the
   design-system default (13px instead of 14px). The inline-text
   alignment tricks (`vertical-align: middle` + `translateY(-1px)`)
   stay so @-mention PFPs inside bubble content sit correctly with
   the surrounding text. */
.nuke-container #nuke-chat-scroll .pill-pfp {
  width: 20px;
  height: 20px;
}
/* @-mention pills (a `.pill-pfp` left of the name inside a `.dock-pill`) show
   the name only — hide the inline avatar. The chain-header avatar is NOT inside
   a `.dock-pill`, so it stays. */
.nuke-container #nuke-chat-scroll .dock-pill .pill-pfp {
  display: none;
}
/* Chat system pills wrap to the next line instead of overflowing the
   conversation width. The base `.dock-pill` / `.pill-content` are
   `white-space: nowrap` + `flex-shrink: 0` for the header ticker; in the chat
   the wrapper is capped at 90% (TS) and the text wraps here. A very long
   unbreakable token (e.g. an address) breaks rather than forcing h-scroll. */
.nuke-container #nuke-chat-scroll .dock-pill {
  max-width: 100%;
  white-space: normal;
  overflow-wrap: anywhere;
}
.nuke-container #nuke-chat-scroll .dock-pill .pill-content {
  white-space: normal;
}

/* The global `.nuke-page-footer` was removed; its flat #EBEBEB +
   raised-bevel design now lives (flipped) on `.nuke-page-header`. */

/* --- Contact list row geometry + typography -------------------- */

/* Row geometry — 40px tall with 5px padding all sides → 30px interior.
   Padding is 5px (not 4) because the PFP's box-shadow ring extends 1px
   past its layout box; 5px CSS padding renders as 4px *visible* padding
   from the row edge to the ring, matching equally on all four sides.
   PFP wrapper = 30px (fills the interior exactly, no centering offset);
   visible PFP = 32px including the 1px ring on each side. Text column
   is also 30px to match the interior. */
.nuke-container .nuke-row {
  /* Card-tint chain (like .nuke-post): --nuke-post-hl is set inline per row from the profile colour
     → --card-bg → --card-border, tinting the pfp border exactly like a chat message card. */
  --card-bg: var(--nuke-post-hl, var(--nk-hover-1));
  --card-border: color-mix(in srgb, var(--card-bg), #000 20%);
  /* Contact-row hover/darken toned to match the feed (soft OKLCH steps, colourless → neutral grey). */
  --card-darken-themed: oklch(from var(--nuke-cmd-color) 0.880 0.045 h);
  --card-darken: var(--card-darken-themed, color-mix(in oklab, #808080 16%, var(--nk-surface)));
  --card-hover-themed: oklch(from var(--nuke-cmd-color) 0.910 0.032 h);
  --card-hover: var(--card-hover-themed, color-mix(in oklab, #808080 9%, var(--nk-surface)));
  font-size: var(--nk-card-font, 15px);   /* same head/card base as .nuke-post */
  height: 54px;
  flex-shrink: 0;
  box-sizing: border-box;
  /* Skip rendering rows scrolled out of the contact list viewport.
     With ~54px fixed height the placeholder size is exact, so scroll
     geometry stays stable. */
  content-visibility: auto;
  contain-intrinsic-size: 54px;
}
/* Search-dropdown rows styled as chat message cards: per-author tint + texture + bevel border +
   inner-white bevel & lift, squared, with an inter-card gap — the same treatment as .nuke-post.
   Selector matches `.contact-group-inner > div` (0,3,1) so its border-radius:0 beats the 6px
   contact-row rounding above. */
/* Search dropdown: drop the group px-3 side padding so rows reach the box edges (negative margins on
   the rows got clipped by .contact-group-inner overflow:hidden); re-inset the header so it stays put. */
.nuke-container .nuke-search-results .contact-group {
  padding-left: 0;
  padding-right: 0;
}
.nuke-container .nuke-search-results .contact-group-header {
  padding-left: 12px;
}
.nuke-container .nuke-search-results .contact-group-inner > .nuke-row,
.nuke-container #nuke-top-players > .nuke-row {
  /* Rest state: a filled rectangle in the user's OWN post-container background
     (COLOR SETTING 2 from their profile hue, like .nuke-post); colourless users
     fall to the neutral --nk-hover-1. */
  --card-bg-themed: oklch(from var(--nuke-cmd-color) var(--set-2-l) var(--set-2-c) h);
  background-color: var(--card-bg-themed, var(--nk-hover-1));
  /* Border like a POST container's (SETTING 3 = its right-edge border tone), themed to THIS user's OWN
     profile colour (--nuke-cmd-color, set per row in contacts.ts); colourless user -> neutral --nk-border-plain. */
  --card-border-themed: oklch(from var(--nuke-cmd-color) var(--set-3-l) var(--set-3-c) h);
  border: 1px solid var(--card-border-themed, var(--nk-border-plain));
  box-sizing: border-box;
  border-radius: 0;   /* square, matching the notification cards */
  /* Inset the coloured rectangle from the dropdown edges with 8px side margins;
     h-padding drops to 9px so the content still sits ~17px from the container edge. */
  margin-left: 8px;
  margin-right: 8px;
  padding-left: 9px;
  padding-right: 9px;
  margin-bottom: 8px;   /* = the notif card gap (--notif-card-gap) */
  /* Hard offset shadow + hover-lift / press like the notif cards, but a touch shorter offset. */
  box-shadow: 2px 2px 0 0 rgba(0, 0, 0, 0.2);
  transition: transform 0.12s ease, box-shadow 0.12s ease, background 0.12s ease;
}
/* Search dropdown user rows: on hover, the SAME per-author tint a post uses on
   hover (var(--card-hover) — oklch keyed to the profile hue, identical to .nuke-post). */
.nuke-container .nuke-search-results .nuke-row:hover,
.nuke-container #nuke-top-players .nuke-row:hover {
  background-color: var(--card-hover);
  transform: scale(1.005);
  box-shadow: 3px 3px 0 0 rgba(0, 0, 0, 0.2);
}
.nuke-container .nuke-search-results .nuke-row:active,
.nuke-container #nuke-top-players .nuke-row:active {
  transform: scale(0.985);
  box-shadow: 1px 1px 0 0 rgba(0, 0, 0, 0.2);
}
.nuke-container .nuke-row-text {
  height: 40px;
  flex: 1;
  min-width: 0;
  justify-content: center;
  gap: 2px;
}
/* Two justify-between lines — [name + flag + level] | balance, then [@handle] | cred
   pill — so the balance sits on the name line and the cred pill drops directly under it
   on the @handle line, mirroring a chat message head + footer. */
.nuke-container .nuke-row-line {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 8px;
  min-width: 0;
}
.nuke-container .nuke-row-namecluster {
  flex: 1;
  min-width: 0;
  display: flex;
  align-items: center;
  gap: 3px;
}
.nuke-container .nuke-row-namecluster .nuke-row-name { flex: 0 1 auto; min-width: 0; }
.nuke-container .nuke-row-flag { display: inline-flex; flex-shrink: 0; }
.nuke-container .nuke-row-line-bot .nuke-row-subtitle {
  flex: 0 1 auto;
  min-width: 0;
}
.nuke-container .nuke-row-balance { flex-shrink: 0; font-size: calc(13px - var(--font-shrink, 0px)); position: relative; top: -1px; }  /* weight + tabular-nums + colour come from the shared .nuke-post-balance */
.nuke-container .nuke-row-cred { flex-shrink: 0; display: inline-flex; align-items: center; }
/* Flag + handle pill: center them on the row's OWN (stable, whole-pixel) line boxes instead of the
   chat head's flex-start + snap-margin (@supports round(...)), which was calibrated for a different
   line box and drifted +-1px per row here. */
.nuke-container .nuke-row .nuke-flag,
.nuke-container .nuke-row .nuke-post-handle {
  align-self: center;
  margin-top: 0;
}
/* Weight / colour / brackets come from the shared .nuke-post-name; keep only the row's own fixed
   line box (17px) so the flag + handle centre consistently. */
.nuke-container .nuke-row-name {
  line-height: 17px;
  height: 17px;
}
/* The @handle — matches the profile hover card handle (#536471). Size comes from
   `.nuke-row-meta` below (one 15px sub-line source); it takes the row's free
   space and truncates. */
.nuke-container .nuke-row-subtitle {
  color: var(--nk-text-2);
  line-height: 17px;
  height: 17px;
  min-width: 0;
  flex: 0 1 auto;
}
/* Contact-row second line: @handle · DPR% + level icon, laid out like a message
   head's meta cluster (reuses .nuke-post-dpr / -dot / -level-icon). 15px matches
   the profile hover card's meta + the row name. */
.nuke-container .nuke-row-meta {
  display: flex;
  align-items: center;
  gap: 4px;
  min-width: 0;
  font-size: calc(15px - var(--font-shrink, 0px));
}
.nuke-container .nuke-row-meta .nuke-post-dot,
.nuke-container .nuke-row-meta .nuke-post-dpr {
  flex-shrink: 0;
  font-style: normal;
}
/* Level icon keeps the message size (1em square, from the unified badge
   rule) — no search-specific override. */

/* Heavier base body weight across the nuke page (web-standard presence). */
.nuke-container { font-weight: 500; }

/* ── Left nav collapses to icon-only SQUARE buttons when the trio touches the viewport edge ───────────
   The full trio is nav(284) + chat(600) + right(398) = 1282px, so at ≤1282px it would run edge-to-edge.
   Instead of dropping the right panel, we COLLAPSE the left nav to icon squares and narrow --nav-col-w to
   100px (→ ~52px content) — that shrinks the trio to ~1098px so the right panel stays. Below 688px the
   mobile build takes over. (The two removed tiers ≤1360 / ≤1024 no longer exist.) */
@media (max-width: 1282px) {
  .nuke-container { --nav-col-w: 100px; }
  /* The nav icons already render — hide every label (link text + the NUKE timer value) and centre the
     squares in the narrow column. */
  .nuke-container .nuke-nav-label { display: none; }
  .nuke-container .nuke-nav-list { align-items: center; }
  .nuke-container .nuke-nav-list .nuke-nav-item:not(.nuke-nav-logo-item) {
    width: 44px;
    height: 44px;
    padding: 0;
    gap: 0;
    justify-content: center;
  }
  /* Post CTA → a square compose "+". font-size:0 hides its label + any cooldown text; the "+" is a
     pseudo-element so the cooldown JS (which rewrites textContent) can't wipe it. */
  .nuke-container .nuke-nav-post-cta {
    width: 44px;
    height: 44px;
    padding: 0;
    font-size: 0;
  }
  .nuke-container .nuke-nav-post-cta::before {
    content: "+";
    font-size: calc(30px - var(--font-shrink, 0px));
    font-weight: 400;
    line-height: 1;
  }
  /* The bottom profile pill collapses to JUST the pfp avatar — hide its name/@handle column + the ···
     glyph, and snug the head pill down around the avatar so it matches the icon squares above. */
  .nuke-container .nuke-nav-top-box .nuke-hovercard-id,
  .nuke-container .nuke-nav-top-box .nuke-profile-card-more {
    display: none;
  }
  .nuke-container .nuke-nav-top-box .nuke-profile-card-head {
    padding: 0;   /* wrap tight around the 44px pfp so the collapsed pill == the 44px nav squares */
    gap: 0;
    justify-content: center;
  }
  /* The back button is normally LEFT-aligned in the nav column (24px inset). With the nav collapsed to
     centred icon squares, centre the back button in the column too: point `left` at the nav's centre
     (bar-left − col-gap − half the nav width) and pull back half its own width with translateX(-50%).
     The nav sits immediately left of the bar in every tier down through PINNED-NAV, so this one override
     stays centred whether the trio is centred or the nav is pinned left. */
  .nuke-container .nuke-page-header .nuke-page-subview-bar .nuke-post-modal-close {
    left: calc(0px - var(--col-gap) - var(--nav-col-w) / 2);
    transform: translateX(-50%);
  }
  /* Keep the centring translate on hover — the base hover sets `transform: scale(1.03)`, which would
     otherwise replace the translate and shove the button off-centre. (`:active` only touches box-shadow,
     so it inherits the base translate and needs no override.) */
  .nuke-container .nuke-page-header .nuke-page-subview-bar .nuke-post-modal-close:hover {
    transform: translateX(-50%) scale(1.03);
  }
}

/* ── Right column shrinks 20% when the collapsed-nav trio touches the viewport edge ───────────────────
   With the icon nav, the trio is nav(100) + chat(600) + right(398) = 1098px, so at ≤1098px it touches the
   edge again. Instead of dropping the right panel, shrink the whole right COLUMN by 20% — its width, its
   contents, the search bar and the search dropdown are ALL driven by --right-col-w, so scaling that token
   (and the two insets) down to 80% shrinks everything at once and the panel stays. */
@media (max-width: 1098px) {
  .nuke-container {
    --right-col-w: 318px;      /* 398 × 0.80 */
    --right-inset: 19px;       /* 24 × 0.80 */
    --right-box-inset: 13px;   /* 16 × 0.80 */
  }
}

/* ── Right panel empties + page becomes symmetric when the trio's edges touch the viewport ─────────────
   With the icon nav (100) + chat (600) + right (318) the trio's content edges hit both viewport edges at
   ~1018px. Here: EMPTY the right panel (drop its boxes) + remove the search bar, and set the right column
   to the SAME width as the icon nav so trio-shift → 0 — the page re-centres with equal left/right wings,
   the (now empty) right wing mirroring the left nav wing. */
@media (max-width: 1018px) {
  .nuke-container { --right-col-w: var(--nav-col-w); }
  .nuke-container .nuke-side-right > *,
  .nuke-container .nuke-subheader {
    display: none;
  }
  /* The right-panel search bar is gone here — surface a Search icon button in the nav (below Wallet).
     It picks up the icon-square styling from the ≤1282 tier automatically. Inert for now. */
  .nuke-container .nuke-nav-search-item { display: flex; }
}

/* ══ BREAKPOINT NAME: "PINNED-NAV" (≤800px) ══════════════════════════════════════════════════════════
   Icon nav hits the left edge → stop centring; shrink the RIGHT panel first, then the CHAT.
   In the symmetric state the wings are `50vw − 300px`, so the icon nav's column touches the viewport's
   LEFT edge at ~800px. Below that we PIN the nav to the left at a fixed width and drop the trio centring.
   The chat holds at 600px while the (empty) right wing — which fills the gap after the chat — shrinks to
   0; only once it's gone (≈700px) does the chat itself start to shrink (via the min()). Seamless at 800px
   (nav 100 · chat 600 · right 100 in both states).
   Named tiers, wide → narrow: ICON-NAV (≤1282) · RIGHT-20 (≤1098) · RIGHT-EMPTY (≤1018) · PINNED-NAV
   (≤800) · SKINNY-MODE (≤688, pfp shrinks) · MOBILE (touch + ≤688). */
@media (max-width: 800px) {
  .nuke-container .nuke-nav {
    left: 0;
    width: var(--nav-col-w);   /* fixed 100px column, pinned left */
    padding-left: 0;
    padding-right: 0;
  }
  .nuke-container .nuke-stage {
    left: var(--nav-col-w);
    transform: none;
    width: min(var(--chat-w), 100vw - var(--nav-col-w));   /* 600 until the right wing is gone, then shrinks */
  }
  .nuke-container .nuke-side-right {
    left: calc(var(--nav-col-w) + min(var(--chat-w), 100vw - var(--nav-col-w)));   /* fills after the chat */
    right: 0;
    width: auto;
  }
  /* The header uses the CENTRED trio math (every wider tier's chat is centred too, so it stays aligned).
     Here the chat is pinned left, so re-pin the header to match: the subview bar + wordmark sit at the
     chat's left (heading follows the CHAT); the back button, pinned left of the bar, follows the NAV. */
  .nuke-container .nuke-page-header .nuke-page-subview-bar {
    left: var(--nav-col-w);
    width: min(var(--chat-w), 100vw - var(--nav-col-w));
  }
  .nuke-container .nuke-page-header .nuke-nav-logo-item {
    left: var(--nav-col-w);
  }
  /* NB: the profile page keeps its REGULAR (133px) avatar through PINNED-NAV — it only shrinks at
     SKINNY-MODE (≤688) below. */
}

/* ══ BREAKPOINT NAME: "SKINNY-MODE" (≤688px, width-based) ═════════════════════════════════════════════
   Begins when the viewport reaches the OLD mobile width (688px) — on DESKTOP (mouse), where the mobile
   device build doesn't fire. Here the profile page adopts the mobile avatar proportions: the pfp shrinks
   64px with a thin 2px ring + banner half-overlap. Width-based, so mobile (also ≤688) inherits it too —
   above 688 the profile keeps its regular 133px avatar. */
@media (max-width: 688px) {
  .nuke-container .nuke-profile-page-pfp {
    width: 96px;
    height: 96px;
    border-radius: 0;                         /* squared to match the unrounded avatars */
    box-shadow: none;                         /* avatar cutout ring removed */
    --pfp-outer-border-width: 4px;            /* thinner ring than the 6px desktop avatar */
    --dot-size: 16px;                         /* 96px avatar → proportional presence dot */
  }
  .nuke-container .nuke-profile-page-toprow {
    margin-top: -48px;                        /* overlap the banner by ~half the 96px avatar */
    margin-bottom: 10px;
  }
  .nuke-container .nuke-profile-page-toprow-actions {
    margin-top: 56px;                         /* +16 to offset the deeper -48 toprow pull, so buttons stay at the banner edge */
  }
  /* Tighten feed / profile post inner padding to the trending-widget post's (8px 10px, vs the feed
     default 12px 16px) to reclaim horizontal room on the narrow viewport. Trending keeps its own value
     via higher specificity; the post-modal detail view keeps its own too. */
  .nuke-container .nuke-post {
    padding: 8px 10px;
  }
  /* Replies (quote boxes) go a touch tighter still — nested inside the already-tightened post. */
  .nuke-container .nuke-post-quote {
    padding: 6px 8px;
  }
  /* The reduced 10px left / 8px top post padding moved the avatar, but the comment-chain "reply chain"
     thread line is hardcoded to the 16px/12px padding (left:36px = 16 + 20px half-avatar; below start
     56px = 12 + 40px avatar + 4px gap). Re-centre it on the avatar's new position so the line stays in
     the gutter. (The quote-reply elbow arrow tracks the content column, so it needs no adjustment.) */
  .nuke-container .nuke-post-thread-above::before,
  .nuke-container .nuke-post-thread-below::after {
    left: 30px;                               /* 10px left padding + 20px half-avatar */
  }
  .nuke-container .nuke-post-thread-below::after {
    top: 52px;                                /* 8px top padding + 40px avatar + 4px gap */
  }
}

/* ─── Profile hover card (hover a message's pfp / username / handle) ────────
   A small rounded white card that fades in with the focused-search drop shadow;
   the user's pfp sits top-left in the same chrome as the message pfps
   (.nuke-post-pfp), with name + @handle beside it. Positioned + populated by
   src/ts/pages/nuke/hovercard.ts. */
.nuke-container .nuke-hovercard {
  position: fixed;
  z-index: 50;
  /* Fixed popup width, intentionally INDEPENDENT of the nav column (was
     `calc(var(--nav-col-w) - 24px)`, which made the popup shrink whenever the nav
     was narrowed). 324px = the original nav content width (348 − 24), kept as the
     popup's own size. The static in-rail `.nuke-profile-card` override still tracks
     the nav at 100%. */
  width: 324px;
  box-sizing: border-box;
  box-sizing: border-box;
  display: flex;
  flex-direction: column;
  overflow: hidden;
  /* Clickable → opens the shown user's profile page. */
  cursor: pointer;
  /* Recessed shading carried over from the (removed) profile-panel tab
     (.nuke-stage-tab.is-active), recolored to a light gray so the white top-edge
     highlight reads, with dark shading on the bottom plus the
     dark shading bleeding in from the top/left/right edges. The tab's
     ::before/::after "feet" are tab-specific and omitted. */
  /* Themed to the hovered user's CONTAINER colour (SETTING 2, from --nuke-cmd-color set by populate()),
     so the popup matches that user's card background. Isolated in --card-bg-themed so a colourless user
     (var unset → invalid) falls back cleanly to the neutral --nk-surface. */
  --card-bg-themed: oklch(from var(--nuke-cmd-color) var(--set-2-l) var(--set-2-c) h);
  /* Fill = the HOVERED user's page-bg colour (--nuke-cmd-color @ SETTING 1 — the right-panel recipe, but the
     hovered profile's hue, NOT the viewer's) + brilliant.png texture. No top-fade; edge-shading stays below. */
  --hover-page-bg: oklch(from var(--nuke-cmd-color) var(--set-1-l) var(--set-1-c) h);
  background-color: var(--hover-page-bg, var(--nk-surface));
  background-image:
    url('/img/ui/brilliant.png'),
    linear-gradient(to bottom, rgba(var(--nk-surface-rgb), 0.7) 0%, transparent 5px),
    linear-gradient(to bottom, rgba(0, 0, 0, 0.04) 0%, transparent 5px),
    linear-gradient(to top, rgba(0, 0, 0, 0.04) 0%, transparent 16px),
    linear-gradient(105deg, rgba(0, 0, 0, 0.04) 0%, transparent 10px),
    linear-gradient(255deg, rgba(0, 0, 0, 0.04) 0%, transparent 10px);
  /* Bevel frame, themed to the HOVERED user (--nuke-cmd-color @ SETTING 3). Each side = a 2px OUTER edge +
     a 2px INNER regular profile edge (--hover-edge, an inset shadow just inside). The outer edge is the
     bevel tone: WHITE highlight on the TOP + LEFT (lit), a slightly DARKER tint (--hover-edge-dark) on the
     RIGHT + BOTTOM (shadowed). Colourless → neutral --nk-border-plain. The left static profile card overrides
     border + box-shadow to none. */
  --hover-edge-raw: oklch(from var(--nuke-cmd-color) var(--set-3-l) var(--set-3-c) h);
  --hover-edge: var(--hover-edge-raw, var(--nk-border-plain));
  --hover-edge-dark: oklch(from var(--hover-edge) calc(l * 0.8) c h);   /* slightly darker tint of the edge */
  border: 2px solid #FFFFFF;                        /* top + left = white highlight */
  border-right: 2px solid var(--hover-edge-dark);   /* right = darker (shadow) */
  border-bottom: 2px solid var(--hover-edge-dark);  /* bottom = darker (shadow) */
  /* Squared corners, matching the unrounded avatars. */
  border-radius: 0;
  box-shadow:
    inset 2px 0 0 0 var(--hover-edge),    /* inner LEFT edge (regular) */
    inset -2px 0 0 0 var(--hover-edge),   /* inner RIGHT edge (regular) */
    inset 0 2px 0 0 var(--hover-edge),    /* inner TOP edge (regular) */
    inset 0 -2px 0 0 var(--hover-edge),   /* inner BOTTOM edge (regular) */
    4px 4px 0 0 rgba(0, 0, 0, 0.2);
  padding: 12px;
  opacity: 0;
  pointer-events: none;
  /* Hidden/exit state. The card EXPANDS OUT OF the pfp — transform-origin is set inline by JS
     (positionCard) to the card's avatar centre; this 0.8 start makes the grow clearly read as
     unfolding from the pfp. This transition only drives the fade + shrink back out on hide. */
  transform: scale(0.8);
  transform-origin: top left;
  transition: opacity 0.12s ease, transform 0.12s ease;
  font-size: calc(15px - var(--font-shrink, 0px));
}
/* x.com-style rounded card: the side panels (.nuke-plain-box) get a 16px corner. (Replies, the hover popup,
   and all chat images / media are squared at their own base rules.) */
.nuke-container .nuke-plain-box {
  border-radius: 16px;
}
.nuke-container .nuke-hovercard.is-visible {
  opacity: 1;
  pointer-events: auto;
  transform: scale(1);
  /* Opacity and scale are SEPARATE animations so they can be timed independently:
     • fade — starts immediately (0s) and finishes at ~0.1s. Starting earlier than the scale
       makes the fade itself more gradual/subtle WITHOUT moving its finish time.
     • pop — the WHOLE container (avatar included) scales out of the pfp, shoots PAST full
       size, then slams back down and softly settles — ONE clean overshoot. Per-keyframe
       easing keeps velocity continuous (no stutter at the stops): ease-out up into the peak,
       ease-in-out back down. */
  animation:
    nuke-hovercard-fade 0.08s linear both,
    nuke-hovercard-pop 0.24s linear 0.04s both;
}
@keyframes nuke-hovercard-fade {
  from { opacity: 0; }
  to   { opacity: 1; }
}
@keyframes nuke-hovercard-pop {
  0%   { transform: scale(0.8);  animation-timing-function: cubic-bezier(0.22, 1, 0.36, 1); }
  52%  { transform: scale(1.05); animation-timing-function: cubic-bezier(0.42, 0, 0.58, 1); }
  100% { transform: scale(1);    }
}
/* Respect reduced-motion: skip the bounce, just fade/snap in. */
@media (prefers-reduced-motion: reduce) {
  .nuke-container .nuke-hovercard.is-visible { animation: none; }
}
/* The popup's banner / head / pfp size + ring all come from the SHARED rules
   defined alongside the left profile card (search `.nuke-profile-card-banner` /
   `-head`). The popup markup (`.nuke-hovercard-banner`, `.nuke-hovercard-head`) is
   listed in those same selector groups, so the popup and card are guaranteed
   identical — no popup-specific pfp copies here that could drift. The status dot
   sizes/offsets proportionally off the base `.status-dot`. */
.nuke-container .nuke-hovercard-meta {
  display: flex;
  align-items: baseline;
  gap: 4px;
  min-width: 0;
}
/* Identity row below the pfp: name/@handle block on the left, balance on the
   right, vertically centered together so the balance lines up with the username
   and handle (rather than being pinned at the card's top-right). */
.nuke-container .nuke-hovercard-idrow {
  margin-top: 8px;
  display: flex;
  /* Top-align so the balance's TOP lines up with the username's top — both the
     hover popup AND the left profile box. */
  align-items: flex-start;
  justify-content: space-between;
  gap: 10px;
}
.nuke-container .nuke-hovercard-id {
  display: flex;
  flex-direction: column;
  min-width: 0;
}
/* Name + level badge on one row (badge sits next to the name like a checkmark).
   Font-size matches the 17px name so the badge's `1em` resolves to 17px. */
.nuke-container .nuke-hovercard-namerow {
  display: flex;
  align-items: center;
  /* Tight, like the feed name cluster — badge hugs the name. */
  gap: 3px;
  min-width: 0;
  font-size: calc(17px - var(--font-shrink, 0px));
}
.nuke-container .nuke-hovercard-name {
  font-weight: 700;
  color: var(--nk-text);
  font-size: calc(17px - var(--font-shrink, 0px));
  line-height: 1.25;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.nuke-container .nuke-hovercard-handle {
  margin-top: 2px; /* +2px gap below the username */
  color: var(--nk-text-2);
  font-size: calc(15px - var(--font-shrink, 0px));
  line-height: 1.25;
}
/* Bio line below the pfp/name section (hidden when the user has no bio). */
.nuke-container .nuke-hovercard-bio {
  margin-top: 8px;
  color: var(--nk-text);
  font-size: calc(15px - var(--font-shrink, 0px));
  line-height: 1.3;
  overflow-wrap: anywhere;
}
/* Identity text (username / @handle / bio) tinted to match the POST CONTAINER: username = SETTING 6
   (heading hue), @handle = the post pill's complementary text colour, bio = SETTING 7 (body copy) — all
   in the shown user's hue. Hue source = --nuke-cmd-color (the hovered user in the popup) → falls back to
   --current-user-color (the viewed profile on the profile page). Applies to the profile page + the hover
   POPUP only — NOT the left calling card (.nuke-profile-card), which keeps its own chrome. Colourless
   users fall back to the neutral text tokens. */
.nuke-container .nuke-profile-page-name,
.nuke-container .nuke-post-modal-title-name,
.nuke-container .nuke-hovercard:not(.nuke-profile-card) .nuke-hovercard-name {
  --id-name-themed: oklch(from var(--nuke-cmd-color, var(--current-user-color)) var(--set-6-l) var(--set-6-c) h);
  color: var(--id-name-themed, var(--nk-text));
}
.nuke-container .nuke-profile-page-handle,
.nuke-container .nuke-hovercard:not(.nuke-profile-card) .nuke-hovercard-handle {
  /* Full @handle PILL, exactly like the post container: complementary (h+180) text on a light
     translucent complementary fill, in the shown user's hue (colourless → the #808080 gray pill).
     align-self + fit-content hug the pill to its text inside the flex identity column. */
  background: oklch(from var(--nuke-cmd-color, var(--current-user-color, #808080)) clamp(0.85, l, 0.9) calc(c * 0.5) calc(h + 180) / 0.5);
  color: oklch(from var(--nuke-cmd-color, var(--current-user-color, #808080)) clamp(0.3, l, 0.42) calc(c * 0.5) calc(h + 180));
  border-radius: 999px;
  padding: 1px 8px 1px 5px;
  margin-right: 0;
  align-self: flex-start;
  width: fit-content;
  font-size: calc(13px - var(--font-shrink, 0px)); /* 2px smaller than the default 15px handle */
}
.nuke-container .nuke-profile-page-bio,
.nuke-container .nuke-hovercard:not(.nuke-profile-card) .nuke-hovercard-bio {
  color: #000; /* bio copy = pure black (profile page + hover popup) */
}
/* Balance on the right of the identity row, vertically centered with the
   username + @handle (28px / 700, same as the profile-header balance). Hidden via
   inline style when the user has no (non-zero) balance. */
.nuke-container .nuke-hovercard-balance {
  flex-shrink: 0;
  font-size: calc(24px - var(--font-shrink, 0px));
  font-weight: 800;
  color: var(--nk-text);
  line-height: 1.1;
  letter-spacing: -0.01em;
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}

/* ═══════════════════════════════════════════════════════════════════════════
   RESPONSIVE FRAMEWORK — screen-size breakpoints for /home
   ═══════════════════════════════════════════════════════════════════════════
   /home is this 3-column, x.com-style shell. Every column is `position: fixed`
   and anchored to the viewport CENTRE via `calc(50% ± var(--chat-w)/2 …)`, so
   the layout is centre-out, not flow-based. The tiers below degrade that down
   to a single-column mobile build.

   Desktop-first cascade: each `max-width` block layers UNDER the ones above it
   (source order 1360 → 1024 → 688 → 480). A 768px (iPad-portrait) viewport gets
   the 1360 + 1024 rules — the centred 600 column ("tablet"), NOT the mobile build.
   The mobile build only kicks in at ≤688 (x.com's mobile breakpoint, below tablet
   width), so the centre panel never balloons to full width on a tablet.

     ┌────────────┬──────────────┬───────────────────────────────────────────┐
     │ Tier       │ Width        │ Layout                                      │
     ├────────────┼──────────────┼───────────────────────────────────────────┤
     │ Desktop    │ ≥ 1361px     │ 3 cols:  nav │ feed (600) │ right sidebar   │
     │ Laptop     │ 1025–1360px  │ 2 cols:  nav │ feed       (right hidden)    │
     │ Tablet     │  689–1024px  │ 1 col:   centred 600 feed (no nav yet)      │
     │ Mobile     │ ≤  688px     │ full-width feed + top app bar + bottom nav  │
     │ Sm. phone  │ ≤  480px     │ mobile build, tighter gutters               │
     └────────────┴──────────────┴───────────────────────────────────────────┘

   The Desktop + Laptop tiers live ABOVE this block (the `max-width: 1360px`
   query earlier in the file). Everything from here down is new scaffold —
   structure only; refine the visuals later. The side-column content (profile
   card, shop, notebook, radio) is hidden below the tablet break; the intent is
   to resurface it through the bottom-nav tabs as drawers/sheets later.
   ═══════════════════════════════════════════════════════════════════════════ */

/* ── Mobile bottom tab bar — primary nav on phones ──────────────────────────
   `display:none` on desktop; the ≤688px phone build switches it to flex. Markup is the `.nuke-mobile-nav`
   in index.html; the `data-mnav` tabs are wired in initNukeMobileNav (Home / Search / Season / Shop / Profile). */
.nuke-container .nuke-mobile-nav {
  display: none;
  position: fixed;
  left: 0;
  right: 0;
  bottom: 0;
  box-sizing: border-box;
  /* Height includes the iOS home-indicator inset so the icon row clears the
     gesture bar; the matching feed `bottom` below reserves the same amount. */
  height: calc(var(--m-botnav-h) + env(safe-area-inset-bottom, 0px));
  padding-bottom: env(safe-area-inset-bottom, 0px);
  z-index: 40;
  align-items: stretch;
  /* Match the top page header: same dark, saturated take on the logged-in user's profile colour (mirrors
     `.nuke-page-header`), with the header's darker (L × 0.88) variant as the top divider. Colourless viewer
     falls back to the same neutral dark band. */
  --page-header-fill: oklch(from var(--current-user-color) 0.42 0.12 h);
  background: var(--page-header-fill, oklch(0.42 0 0));
  border-top: 1px solid oklch(from var(--page-header-fill, oklch(0.42 0 0)) calc(l * 0.88) c h);
}
.nuke-container .nuke-mobile-nav-item {
  flex: 1 1 0;
  min-width: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  background: transparent;
  border: none;
  cursor: pointer;
  /* White icons over the dark header-coloured bar — inactive dimmed, active full white. */
  color: rgba(255, 255, 255, 0.6);
  font-size: calc(26px - var(--font-shrink, 0px));
  line-height: 1;
  -webkit-appearance: none;
  appearance: none;
}
.nuke-container .nuke-mobile-nav-item.is-active {
  color: #fff;
}

/* ── Tablet (≤1024px): collapse to a single centred feed ───────────────────── */
/* ── Mobile: the mobile build — touch device AND ≤688px ───────────────────────────────────────────────
   Triggers only on a touch device (no hover) that's ALSO ≤688px wide. So: a narrow DESKTOP window never
   goes mobile (it keeps the responsive width tiers, which end at ≤800 above), and a LARGE touch device
   (e.g. a tablet) uses the desktop width layout while it fits — only small touch screens get this
   full-width edge-to-edge build. Self-contained — it hides the nav rail, right panel and search subheader. */
@media (hover: none) and (pointer: coarse) and (max-width: 688px) {
  /* SITE-WIDE font shrink: every `font-size` is wrapped in `calc(<size> - var(--font-shrink, 0px))`, so
     this one variable subtracts exactly 2px from every text (and token) size on phones. Desktop leaves
     --font-shrink unset → the calc falls back to 0px → sizes are unchanged. Set on :root so body-level
     nuke UI (toasts, splash, modals) shrinks too, not just .nuke-container descendants. */
  :root { --font-shrink: 2px; }
  /* No trio centering on phones (the feed is edge-to-edge below). */
  .nuke-container { --trio-shift: 0px; }
  /* Post + reply cards on phones: post gets EQUAL 10px LEFT/RIGHT side padding, the reply a 6px side; both cut
     their top/bottom padding 2px below SKINNY-MODE (post 8→6, reply 6→4) so the cards sit tighter. Everything
     keyed off the post's left/right padding is re-derived below (thread line, footer bleed + inset, vote slot). */
  .nuke-container .nuke-post {
    padding-left: 10px;
    padding-right: 10px;
    padding-top: 6px;
    padding-bottom: 6px;
    /* Flat feed on phones: strip the layered card frame at once, then keep ONLY a 1px bottom divider between
       posts. It's a multi-part system, so clearing it all first is cleanest — the real borders (1px white
       TOP, gray LEFT/RIGHT panel frame) AND the inset side bevels (box-shadow: white inner-left + --card-border
       inner-right) all go; the BOTTOM is re-added as a 1px --card-border line (was 2px). No hover/pulse/arrival
       state re-adds a shadow (they only change background-color), so this is complete. */
    border: none;
    border-bottom: 1px solid var(--card-border);
    box-shadow: none;
  }
  /* Date dividers become full-width bars filled with the SAME setting-2 card fill the post containers use
     (--card-bg-themed), themed to the page/viewer colour so the bar matches the feed's post cards — colourless
     falls to the neutral --nk-hover-1, exactly like a post. Plus the SAME 1px bottom divider your posts have
     (setting-3 --card-border, viewer hue). Dividers + label are unchanged. */
  .nuke-container .nuke-chat-day-sep {
    --card-bg-themed: oklch(from var(--current-user-color) var(--set-2-l) var(--set-2-c) h);
    --card-border-themed: oklch(from var(--current-user-color) var(--set-3-l) var(--set-3-c) h);
    background: var(--card-bg-themed, var(--nk-hover-1));
    border-bottom: 1px solid var(--card-border-themed, var(--nk-border-plain));
  }
  /* Hide older DATE stamps ("Jul 5") in post + reply headers on phones — flagged live by the time ticker
     (nuke-time-date), including each stamp's leading "·" dot. Recent relative stamps ("3h") stay. */
  .nuke-container .nuke-time-date {
    display: none;
  }
  .nuke-container .nuke-post-quote {
    padding-left: 6px;
    padding-right: 6px;
    padding-top: 4px;
    padding-bottom: 4px;
  }
  /* Re-align the comment-chain "reply chain" thread line for the phone geometry: the avatar is 36px here
     (shrunk via --nuke-chat-pfp-size below) with 10px side / 6px top padding. Centre x = 10px padding + 18px
     half-avatar = 28px; below-segment top = 6px padding + 36px avatar + 4px gap = 46px (top padding, unchanged
     by the side padding); the above stub is the 6px top padding so it still reaches the avatar. */
  .nuke-container .nuke-post-thread-above::before,
  .nuke-container .nuke-post-thread-below::after {
    left: 28px;
  }
  .nuke-container .nuke-post-thread-above::before {
    height: 6px;
  }
  .nuke-container .nuke-post-thread-below::after {
    top: 46px;
  }
  /* Shrink avatars + the fixed-size flag on phones. (The cred badge is sized 1em, so it already tracks the
     −2px font shrink automatically — no rule needed.) Post pfp shrinks via the shared token so the cred-pill
     and vote-slot columns, which key off the same token, stay aligned with it. */
  .nuke-container { --nuke-chat-pfp-size: 36px; }                    /* feed/post avatars + aligned cred pill / vote slot (was 40px) */
  .nuke-container .nuke-post-quote-pfp { width: 18px; height: 18px; }  /* reply avatar (was 20px) */
  .cc-pfp-wrapper,
  .msg-pfp-frame,
  .msg-pfp-spacer,
  .cc-pfp,
  .chat-secondary-action-inner { width: 33px; height: 33px; }         /* calling-card + direct-message pfp system (was 37px) */
  /* The country flag is a fixed 16×11 sprite (can't be em), so scale it ~15% to sit right with the smaller
     text — roughly matching the name row's 15→13px reduction. */
  .nuke-container .nuke-flag { transform: scale(0.85); }
  /* Post + reply balance sit at an explicit 13px on phones (they otherwise inherit the name/post-base size). */
  .nuke-container .nuke-post-balance,
  .nuke-container .nuke-post-quote-balance {
    font-size: 13px;
  }
  /* Pull the name/copy column closer to the avatar — the 12px row gap is too wide on the narrow card. */
  .nuke-container .nuke-post-row { gap: 6px; }
  /* SIDEWAYS-SCROLL FIX: the footer band bleeds out with negative side/bottom margins to cancel the post's
     padding and sit edge-to-edge. The bleed MUST match the post padding exactly (an overshoot is what let the
     feed scroll right) — now 10px sides / 6px bottom. Left inset re-bases onto the phone avatar gutter
     (10px pad + 36px pfp + 6px row gap = 52px) so the action row still starts under the message text. */
  .nuke-container .nuke-post-footer {
    margin-left: -10px;
    margin-right: -10px;     /* matches the post's 10px RIGHT padding so the band stays edge-to-edge */
    margin-bottom: -6px;
    padding-left: 52px;
    padding-right: 10px;     /* reactions align to the content right edge (post's 10px right padding) */
  }
  /* The cred badge / vote slot is absolutely pinned to the pfp gutter. With the footer bleeding -10px, its
     border-box left = the post edge, so the pfp x is 10px here — move the slot to match, otherwise it would
     float into the message column and overlap the action bar. */
  .nuke-container .nuke-post-footer .nuke-post-vote-slot {
    left: 10px;
  }
  /* ── Upload sizing on phones ──────────────────────────────────────────────────────────────────
     Music CD: shrink the disc to 150px via the ONE --music-cd-size knob (everything %-based — spindle hole,
     rings, art, masks — scales with it on its own). Only the fixed-px chrome gets its own small overrides:
     the borders (4→3 / 3→2) and the shine blur (2→1.5). To resize again, change just --music-cd-size.
     (zoom/transform were avoided: zoom mis-scales the -webkit-mask on WebKit, and transform collides with the
     CD's hover/tap scale.) */
  .nuke-container .nuke-post-music-tile { --music-cd-size: 150px; border-width: 3px; }
  .nuke-container .nuke-post-music-cd { border-width: 2px; }
  .nuke-container .nuke-post-music-cd::before { border-width: 3px; }
  .nuke-container .nuke-post-music-tile::before { filter: blur(1.5px); }
  /* Image uploads: cap the rendered HEIGHT without cropping — the whole image scales down keeping its aspect
     ratio. Drop the inline fixed-aspect box + portrait width-cap, take the <img> out of its absolute/cover
     fill and size it naturally within a max-height (width follows to preserve the ratio), then shrink-wrap
     the bordered frame around it and centre it. */
  .nuke-container .nuke-post-media-1 .nuke-post-img-wrap {
    aspect-ratio: auto !important;   /* beat the inline aspect-ratio:w/h */
    max-width: 100% !important;      /* beat the inline portrait width-cap */
    width: fit-content;
    margin-left: 0;                  /* left-align the shrink-wrapped frame (posts are left-aligned) */
    margin-right: auto;
  }
  .nuke-container .nuke-post-media-1 .nuke-post-img-wrap img {
    position: static;                /* was absolute inset-0 */
    display: block;                  /* kill the inline-baseline descender gap under the img (was a strip
                                        of empty space between the image and the frame's bottom border) */
    width: auto;
    height: auto;
    max-width: 100%;
    max-height: 55vh;                /* the height limit; aspect ratio preserved, no crop */
    object-fit: contain;
  }
  /* Presale receipts render like an image post; trim their max-width down from 260px on the narrow card. */
  .nuke-container .nuke-receipt { max-width: 200px; }
  /* Hide the desktop chrome — left nav rail, right shop panel, search subheader. The bottom tab bar +
     the Search/Shop DRAWER rules further down re-surface search/shop on demand. */
  .nuke-container .nuke-nav,
  .nuke-container .nuke-side-right,
  .nuke-container .nuke-subheader {
    display: none;
  }
  /* The mobile top app bar was logo-only at this tier (the pill ticker + caps are hidden from 1024px, and
     its Home logo just duplicates the bottom-nav Home) — a useless ~48px strip above the chat header. Drop
     it and zero its height token so the feed + its OWN chat header fill from just below the notch. Every
     `calc(var(--m-topbar-h) + safe-area)` below (stage top, drawer top) collapses to the safe-area inset. */
  /* The main page header (wordmark on the home feed; back-bar + title in subviews) kept its 56px desktop
     height and was NEVER offset from the feed on phones, so it sat OVER the content. Make it a compact mobile
     header: 44px tall (spanning the notch), and offset the feed + drawers to start beneath it by pointing
     --m-topbar-h (which every `+ safe-area` top calc below uses) at the header height. */
  .nuke-container {
    --page-header-h: 46px;   /* match the bottom nav (--m-botnav-h) height */
    --m-topbar-h: var(--page-header-h);
  }
  .nuke-container .nuke-page-topbar {
    display: none;
  }
  .nuke-container .nuke-page-header {
    height: calc(var(--page-header-h) + env(safe-area-inset-top, 0px));
    padding-top: env(safe-area-inset-top, 0px);
    /* Thinner 1px bottom border on phones (2px on desktop) — the inset-shadow "border" + the soft drop. */
    box-shadow:
      inset 0 -1px 0 0 oklch(from var(--page-header-fill, oklch(0.42 0 0)) calc(l * 0.88) c h),
      0 2px 6px rgba(0, 0, 0, 0.14);
  }
  /* Wordmark: pin to the left edge (no nav column on phones — the ≤800 rule left it at left:100px) and shrink
     it to fit the compact header (was 32px in a 60px box). top offset clears the notch. */
  .nuke-container .nuke-page-header .nuke-nav-logo-item {
    left: 12px;
    top: env(safe-area-inset-top, 0px);
  }
  .nuke-container .nuke-page-header .nuke-nav-logo-item .nuke-nav-logo-text {
    height: var(--page-header-h);
    font-size: 18px;
  }
  /* Subview (post / notifications) titles share the wordmark's Marlboro size — shrink them to match. */
  .nuke-container .nuke-page-header .nuke-page-subview-bar[data-subview="post"] .nuke-post-modal-title,
  .nuke-container .nuke-page-header .nuke-page-subview-bar[data-subview="notif"] .nuke-post-modal-title {
    font-size: 18px;
  }
  /* Drop the gray side frame from the radio row on phones — the feed already runs full-width with no side
     columns, so the left/right panel borders just box it in unnecessarily. */
  .nuke-container .nuke-chat-header {
    border-left: none;
    border-right: none;
  }
  /* Radio bar row (the feed's top header) sits at 32px on phones (42px on desktop), with the play + video
     buttons scaled down so they fit inside it (they were 30/26px). */
  .nuke-container .nuke-chat-header-top {
    height: 32px;
  }
  .nuke-container .nuke-radio-bar-btn {
    width: 20px;
    height: 20px;
  }
  .nuke-container .nuke-radio-bar-btn svg { width: 12px; height: 12px; }
  .nuke-container .nuke-radio-bar-video {
    width: 18px;
    height: 18px;
  }
  .nuke-container .nuke-radio-bar-video svg { width: 11px; height: 11px; }
  /* The LCD box is a fixed 42px — it overflows the row and keeps the bar visually 42px tall. Shrink it to the
     row height (border-box). Strip the whole LCD frame on phones EXCEPT the bottom divider: no top border, no
     SIDE frame (white left highlight, profile-coloured right edge, inset-left profile inner box-shadow) — only
     the 1px bottom line to the feed remains. */
  .nuke-container .nuke-radio-bar-info {
    height: 32px;
    box-sizing: border-box;
    border-top: none;
    border-bottom-width: 1px;
    border-left: none;
    border-right: none;
    box-shadow: none;
  }
  /* Stream video: block taps reaching the native <video> so iOS can't fling it into fullscreen or pop its
     PiP/AirPlay controls (the disablepictureinpicture/controlslist attrs cover the rest). The app drives
     play/stop from its own buttons, so the video never needs direct interaction. */
  .nuke-container .nuke-radio-video-el {
    pointer-events: none;
  }
  /* Feed fills the whole viewport between the notch (safe-area) and the bottom
     tab bar — edge to edge, no centring. */
  .nuke-container .nuke-stage {
    left: 0;
    right: 0;
    width: auto;
    transform: none;
    /* Anchor the feed at the very TOP (behind the fixed page header). The radio row is pinned BELOW the page
       header via JS (header.style.top = page-header height) and the scroll's top padding clears BOTH — so the
       page header + radio form one stack that collapses together with pure transforms, no top/layout change. */
    top: 0;
    /* Feed runs to the very screen bottom, BEHIND the fixed bottom nav — so when the nav collapses on scroll
       it just reveals more feed (no empty gap). The scroll's bottom padding (JS) keeps the last post clear of
       the nav while it's shown. Only the home-indicator inset is reserved here. */
    bottom: env(safe-area-inset-bottom, 0px);
    /* The feed's inner header (pfp + name + balance + stake buttons) was sized
       for the 600px desktop column and has a ~485px min-content width that
       won't shrink. `min-width: 0` lets this full-width column honour the
       viewport instead of being forced wider, and `overflow: hidden` clips any
       residual bleed — together they stop the horizontal page scroll that
       would otherwise drag the fixed bottom nav off-screen. The inner header
       still needs to wrap/shrink at this width — that's the refine-later work. */
    min-width: 0;
    overflow: hidden;
  }
  /* Slim top app bar — logo only (the ticker is already hidden at the tablet
     tier). Grows under the notch via the safe-area inset. */
  .nuke-container .nuke-page-topbar {
    height: calc(var(--m-topbar-h) + env(safe-area-inset-top, 0px));
    padding-top: env(safe-area-inset-top, 0px);
  }
  /* Primary bottom tab bar on phones — Home / Search / Season Pass / Shop / Profile (wired in
     initNukeMobileNav). The stage reserves --m-botnav-h above so the feed ends just over it. */
  .nuke-container .nuke-mobile-nav {
    display: flex;
  }

  /* ── Mobile drawers: Search + Shop surfaced as full-screen panes ───────────
     The desktop shop column (.nuke-side-right) and the search subheader
     (.nuke-subheader) are display:none below 1360px. The bottom-nav Search/Shop
     tabs set a body `m-view-*` class (JS setMobileView) that reveals the matching
     panel as a fixed pane filling the gap between the slim top app bar and the
     bottom nav, layered over the feed. NPCs reach the shop + search here too. */
  body.m-view-shop .nuke-container .nuke-side-right,
  body.m-view-search .nuke-container .nuke-subheader {
    display: flex;
    flex-direction: column;
    position: fixed;
    left: 0;
    right: 0;
    top: calc(var(--m-topbar-h) + env(safe-area-inset-top, 0px));
    bottom: calc(var(--m-botnav-h) + env(safe-area-inset-bottom, 0px));
    width: auto;
    margin: 0;
    padding: 8px;
    box-sizing: border-box;
    z-index: 35;
    background: var(--nk-surface);
    overflow-y: auto;
  }
  /* Shop pane shows just the shop list — drop the column's mission notebook. */
  body.m-view-shop .nuke-container .nuke-side-right .nuke-notebook {
    display: none;
  }
  /* Search pane: keep the results list open regardless of input focus. */
  body.m-view-search .nuke-container .nuke-search-results {
    display: block;
  }
  /* Phones shrink the profile pfp + headline balance BELOW the SKINNY-MODE sizes (96px pfp / 32px balance).
     Touch-only + placed after SKINNY-MODE, so it wins on phones while a narrow DESKTOP window keeps 96/32.
     Only width/height/dot + the balance size change — the squared shape, no-shadow and 4px ring fall through
     from SKINNY-MODE. */
  .nuke-container .nuke-profile-page-pfp {
    width: 56px;
    height: 56px;
    --dot-size: 9px;                          /* 56px avatar → proportional presence dot */
    --pfp-outer-border-width: 2px;            /* thinner ring on phones (4px at SKINNY-MODE) */
  }
  .nuke-container .nuke-profile-page-toprow {
    margin-top: -28px;                        /* overlap the banner by ~half the 56px avatar */
  }
  .nuke-container .nuke-profile-page-toprow-actions {
    margin-top: 36px;                         /* keep the buttons at the banner edge with the shallower -28 pull */
  }
  .nuke-container .nuke-profile-page-balance {
    font-size: calc(20px - var(--font-shrink, 0px));                          /* smaller headline balance on phones (32px on desktop / skinny) */
    margin-top: 12px;                         /* 8px lower than the 4px base — drops the balance down on phones */
  }
}

/* ── Small phones (≤480px, touch): tighten the page gutters — device-gated like the mobile build above. */
@media (hover: none) and (pointer: coarse) and (max-width: 480px) {
  .nuke-container {
    --page-edge-x: 8px;
  }
  /* NB: the old "CD fills 100%" rules were removed — they GREW the disc to the phone width and overrode the
     240px phone size set in the ≤688 block above. The 240px disc already fits every phone, so no cap needed here. */
}

/* ============ Edit Profile button + modal (own profile page) ============ */
/* Flat button themed to the VISITED profile: SETTING-2 background + SETTING-3 border + SETTING-7 text. HOVER
   saturates the background + border via --btn-sat (a chroma multiplier). Applied to the Edit button AND
   every friend-button state. */
.nuke-container .nuke-profile-edit-btn,
.nuke-container .nuke-profile-friend-btn {
  --btn-sat: 1;   /* chroma multiplier — hover bumps it to saturate the bg + border */
  flex-shrink: 0;
  box-sizing: border-box;
  height: 28px;   /* FIXED so every button in the action row is the same height regardless of icon size */
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 0 14px;   /* height controls the vertical size now; horizontal padding only */
  /* Border = the profile's SETTING-3 (--nk-border, the post-card border colour), chroma × --btn-sat. */
  border: 1px solid;
  border-color: oklch(from var(--nk-border) calc(l * 0.8) calc(c * var(--btn-sat)) h);
  border-radius: 0;   /* square, matching the top-players player cards */
  /* Background = the profile's SETTING-2 fill (--self-card-fill), chroma × --btn-sat. Colourless → neutral. */
  background: oklch(from var(--self-card-fill) calc(l * 0.9) calc(c * var(--btn-sat)) h);
  /* Text = the profile's SETTING-7 (main copy text); colourless → --nk-text-msg. */
  --edit-btn-fg-raw: oklch(from var(--current-user-color) calc(var(--set-7-l) * 0.8) var(--set-7-c) h);
  color: var(--edit-btn-fg-raw, var(--nk-text-msg));
  font-family: inherit;   /* buttons don't inherit the site font (--font-conduit) by default */
  font-size: calc(13px - var(--font-shrink, 0px));
  font-weight: 700;
  line-height: 1;
  cursor: pointer;
  transition: none;
}
/* Hover: saturate the background + border (bump the chroma multiplier). */
.nuke-container .nuke-profile-edit-btn:hover,
.nuke-container .nuke-profile-edit-btn:focus,
.nuke-container .nuke-profile-friend-btn:hover,
.nuke-container .nuke-profile-friend-btn:focus {
  outline: 0;
  --btn-sat: 2;
}
.nuke-container .nuke-profile-friend-btn i { font-size: calc(14px - var(--font-shrink, 0px)); }
/* Edit-profile button: plain GRAY (same as the Requested button) — not the profile-colour fill. Stays
   clickable, so the --btn-sat hover above is a no-op against these fixed neutral values. */
.nuke-container .nuke-profile-edit-btn {
  background: var(--nk-hover-1);
  border-color: var(--nk-border-plain);
  color: var(--nk-text-2);
}

/* The Post / "Get Personal" CTA is the primary action — styled like the left-nav Post button:
   NO border + the same green fill (not the themed gray of Edit/friend). Keeps the row's size
   (padding/radius/font) so it lines up with the Edit/friend button beside it. */
.nuke-container .nuke-profile-post-btn {
  flex-shrink: 0;
  box-sizing: border-box;
  height: 28px;   /* same fixed height as the Edit/friend + Accept/Decline buttons */
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 6px;
  padding: 0 14px;   /* height controls the vertical size now; horizontal padding only */
  border-radius: 0;   /* square, matching the top-players player cards */
  /* Same fill as .nuke-nav-post-cta: page-header profile colour rotated −100° in hue, with the
     #0E8A53 fallback when --current-user-color is unset (isolate pattern). */
  --post-cta-fill: oklch(from var(--current-user-color) 0.42 0.12 calc(h - 100));
  --btn-bg: var(--post-cta-fill, #0E8A53);
  background-color: var(--btn-bg);
  /* Sign-in button DESIGN (colour kept; 2px border — NOT the doubled 4px): top-highlight + bottom-shade
     gradients + a 2px 3D bevel derived from the fill (lighter top/left, darker bottom/right, same hue). */
  background-image:
    linear-gradient(to bottom, rgba(255, 255, 255, 0.10) 0%, transparent 50%),
    linear-gradient(to top, rgba(0, 0, 0, 0.10) 0%, transparent 12px);
  border: 2px solid;
  border-color:
    oklch(from var(--btn-bg) calc(l * 1.28) c h)
    oklch(from var(--btn-bg) calc(l * 0.72) c h)
    oklch(from var(--btn-bg) calc(l * 0.72) c h)
    oklch(from var(--btn-bg) calc(l * 1.28) c h);
  color: #FFFFFF;
  font-family: inherit;   /* buttons don't inherit the site font (--font-conduit) by default */
  font-size: calc(13px - var(--font-shrink, 0px));
  font-weight: 700;
  line-height: 1;
  cursor: pointer;
  transition: filter 0.15s ease, transform 0.12s ease;
}
/* Hover brightness + scale, press scale — the scale matches the left-panel nav motion. Guarded with
   :not([disabled]) so the disabled cooldown variant doesn't animate. */
.nuke-container .nuke-profile-post-btn:not([disabled]):hover,
.nuke-container .nuke-profile-post-btn:not([disabled]):focus { outline: 0; filter: brightness(1.06); transform: scale(1.005); }
.nuke-container .nuke-profile-post-btn:not([disabled]):active { filter: brightness(0.92); transform: scale(0.985); }
/* Compose pencil / cooldown clock icon (left of the label) — sized to the 13px text line-box so it
   doesn't make the button taller than the (icon-less) Edit / friend button beside it. */
.nuke-container .nuke-profile-post-btn .nuke-profile-post-icon {
  width: 13px;
  height: 13px;
  flex-shrink: 0;
  display: block;
}
/* Cooldown clock — 1px larger than the pencil (both the self "Post" and "Get Personal" timeout states). */
.nuke-container .nuke-profile-post-btn .nuke-profile-post-icon-clock {
  width: 14px;
  height: 14px;
}
/* Self "Post" button: pencil normally, clock swapped in while the posting cooldown is active. (The Get
   Personal cooldown button renders only the clock inline, so it's unaffected by these data-nav rules.) */
.nuke-container .nuke-profile-post-btn[data-nav="post"] .nuke-profile-post-icon-clock { display: none; }
.nuke-container[data-post-cooldown] .nuke-profile-post-btn[data-nav="post"] .nuke-profile-post-icon-pencil { display: none; }
.nuke-container[data-post-cooldown] .nuke-profile-post-btn[data-nav="post"] .nuke-profile-post-icon-clock { display: block; }
/* Settings cog on the Edit-profile button — 14px (a touch larger than the 13px text). */
.nuke-container .nuke-profile-edit-btn .nuke-profile-edit-icon {
  width: 14px;
  height: 14px;
  flex-shrink: 0;
}
/* Cooldown state (disabled) — the SAME plain gray as the Edit-profile button, so timeout reads as
   unavailable rather than a live green CTA. */
.nuke-container .nuke-profile-post-btn-cooldown,
.nuke-container .nuke-profile-post-btn-cooldown:hover {
  background: var(--nk-hover-1);
  color: var(--nk-text-2);
  border-color: var(--nk-border-plain);
  cursor: default;
  filter: none;
}
/* Post + Edit/friend sit in a row (Post on the left). */
.nuke-container .nuke-profile-action-row {
  display: inline-flex;
  align-items: center;
  gap: 14px;
}
/* Requested (pending): inert + plain GRAY (not the profile colour), and no hover saturation. */
.nuke-container .nuke-profile-friend-btn[data-friend-state='outgoing'] {
  cursor: default;
  background: var(--nk-hover-1);
  border-color: var(--nk-border-plain);
  color: var(--nk-text-2);
}
.nuke-container .nuke-profile-friend-btn[data-friend-state='outgoing']:hover {
  background: var(--nk-hover-1);
  border-color: var(--nk-border-plain);
}
/* Nav-style hover/press scale on the Send-request (+ Friends) button; Requested (outgoing) is
   excluded via :not() so it stays fully inert (no hover or click feedback). */
.nuke-container .nuke-profile-friend-btn:not([data-friend-state='outgoing']) { transition: transform 0.12s ease; }
.nuke-container .nuke-profile-friend-btn:not([data-friend-state='outgoing']):hover { transform: scale(1.005); }
.nuke-container .nuke-profile-friend-btn:not([data-friend-state='outgoing']):active { transform: scale(0.985); }
/* Friend area wrapper — holds the single button, or the Accept + Decline pair (incoming request). */
.nuke-container .nuke-profile-friend-slot {
  display: inline-flex;
  align-items: center;
  gap: 8px;
}
/* Friends: swap "Friends" ↔ "Unadd" on hover (text only — the button chrome stays the plain button). */
.nuke-container .nuke-profile-friend-btn[data-friend-state='friends'] .friend-btn-alt { display: none; }
.nuke-container .nuke-profile-friend-btn[data-friend-state='friends'] .friend-btn-default { display: inline-flex; align-items: center; gap: 6px; }
.nuke-container .nuke-profile-friend-btn[data-friend-state='friends']:hover .friend-btn-default { display: none; }
.nuke-container .nuke-profile-friend-btn[data-friend-state='friends']:hover .friend-btn-alt { display: inline-flex; align-items: center; gap: 6px; }

/* ── Friend-request notification: inline Accept / Decline ── */
.notif-friend-actions {
  display: flex;
  align-items: center;
  gap: 8px;
  margin-top: 8px;
}
.notif-friend-btn {
  --btn-sat: 1;   /* chroma multiplier — hover saturates the bg + border */
  display: inline-flex;
  align-items: center;
  padding: 5px 14px;
  border-radius: 0;   /* square, matching the top-players player cards */
  border: 1px solid;
  font-family: inherit;   /* buttons don't inherit the site font (--font-conduit) by default */
  font-size: calc(13px - var(--font-shrink, 0px));
  font-weight: 700;
  line-height: 1;
  cursor: pointer;
  transition: none;
}
/* In the profile action row (Accept/Decline on someone's profile), match the fixed height of the Post /
   Edit / friend buttons so every button in that section is the same height. (Notification-LIST copies keep
   their own padding-based size.) */
.nuke-container .nuke-profile-friend-slot .notif-friend-btn {
  box-sizing: border-box;
  height: 28px;
  padding: 0 14px;
}
.notif-friend-btn:hover { --btn-sat: 2; }   /* hover saturates bg + border (same as the profile button) */
/* Accept = GREEN, Decline = RED — same flat themed treatment as the profile Edit button (SETTING-2 fill +
   SETTING-3 border + SETTING-7 text), keyed to a green / red base; both saturate on hover via --btn-sat. */
.notif-friend-accept { --fr-base: #1a9e5e; }
.notif-friend-decline { --fr-base: #d32f2f; }
.notif-friend-accept,
.notif-friend-decline {
  /* Background = the SAME colour as the border (SETTING-3), not the lighter SETTING-2 fill. */
  background: oklch(from var(--fr-base) var(--set-3-l) calc(var(--set-3-c) * var(--btn-sat)) h);
  border-color: oklch(from var(--fr-base) var(--set-3-l) calc(var(--set-3-c) * var(--btn-sat)) h);
  color: oklch(from var(--fr-base) var(--set-7-l) var(--set-7-c) h);
  transition: transform 0.12s ease;   /* animates the hover / press scale below */
}
/* Hover scale + press feedback — same motion as the left-panel nav items. */
.notif-friend-accept:hover,
.notif-friend-decline:hover { transform: scale(1.005); }
.notif-friend-accept:active,
.notif-friend-decline:active { transform: scale(0.985); }
.notif-friend-resolved { font-size: calc(13px - var(--font-shrink, 0px)); font-weight: 600; color: var(--nk-text-2); }

/* ── Action-row buttons: a hard offset "border shadow" that animates on click, like the header Back
   button — 4px at rest, grows to 6px on hover, sucks into 1px on press. Only ACTIVE buttons animate;
   the cooldown (Post / Get Personal) + Requested states get no shadow at all. Placed after all the button
   rules so the base box-shadow + shared transition win. */
.nuke-container .nuke-profile-post-btn,
.nuke-container .nuke-profile-edit-btn,
.nuke-container .nuke-profile-friend-btn,
.nuke-container .nuke-profile-friend-slot .notif-friend-btn {
  box-shadow: 2px 2px 0 0 rgba(0, 0, 0, 0.2);   /* = the top-players player-card shadow (shorter offset) */
  transition: transform 0.12s ease, box-shadow 0.12s ease, filter 0.15s ease;
}
.nuke-container .nuke-profile-post-btn:not([disabled]):hover,
.nuke-container .nuke-profile-edit-btn:hover,
.nuke-container .nuke-profile-friend-btn:not([data-friend-state='outgoing']):hover,
.nuke-container .nuke-profile-friend-slot .notif-friend-btn:hover {
  box-shadow: 3px 3px 0 0 rgba(0, 0, 0, 0.2);
}
.nuke-container .nuke-profile-post-btn:not([disabled]):active,
.nuke-container .nuke-profile-edit-btn:active,
.nuke-container .nuke-profile-friend-btn:not([data-friend-state='outgoing']):active,
.nuke-container .nuke-profile-friend-slot .notif-friend-btn:active {
  box-shadow: 1px 1px 0 0 rgba(0, 0, 0, 0.2);
}
/* Edit-profile button — the same hover/press SCALE the other action buttons have (its box-shadow already
   grows/shrinks above; this adds the matching motion so it animates fully like the Back button). */
.nuke-container .nuke-profile-edit-btn:hover { transform: scale(1.005); }
.nuke-container .nuke-profile-edit-btn:active { transform: scale(0.985); }
/* Requested (pending) is INERT — it keeps the resting 2px shadow (all buttons in the row carry one) but
   never animates: it's excluded from the hover/press grow above, and its :hover/:active are frozen here. */
.nuke-container .nuke-profile-friend-btn[data-friend-state='outgoing']:hover,
.nuke-container .nuke-profile-friend-btn[data-friend-state='outgoing']:active {
  box-shadow: 2px 2px 0 0 rgba(0, 0, 0, 0.2);
  transform: none;
}
/* Timed-out (cooldown) buttons KEEP the resting 2px shadow but DON'T animate — freeze it at rest on
   hover/press (the Get Personal cooldown is `disabled`, so its hover/press shadow is already skipped). */
.nuke-container[data-post-cooldown] .nuke-profile-post-btn[data-nav="post"]:hover,
.nuke-container[data-post-cooldown] .nuke-profile-post-btn[data-nav="post"]:focus,
.nuke-container[data-post-cooldown] .nuke-profile-post-btn[data-nav="post"]:active {
  box-shadow: 2px 2px 0 0 rgba(0, 0, 0, 0.2);
}

/* Overlay scrim — covers the viewport, centers the modal. Above the headers
   (z 5) and the chat lightbox (z 20). */
.nuke-container .nuke-edit-overlay {
  position: fixed;
  inset: 0;
  z-index: 60;
  display: flex;
  /* Sit the card higher up the page (matches the compose modals) instead of vertically centered. */
  align-items: flex-start;
  justify-content: center;
  padding: 6vh 16px 16px;
  background: rgba(0, 0, 0, 0.4);
}

.nuke-container .nuke-edit-modal {
  display: flex;
  flex-direction: column;
  width: 100%;
  max-width: 600px;
  max-height: 90vh;
  background: var(--nk-surface);
  /* Squared chrome to match the app's other modals (.cred-compose-modal): 2px soft border, no radius. */
  border: 2px solid var(--nk-border-soft);
  border-radius: 0;
  box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.02), 0 16px 48px rgba(0, 0, 0, 0.22);
  overflow: hidden;
}
/* Reuse the compose modal's open / close animation (same global keyframes): backdrop fades while the
   card rises + scales in. Close is driven by the .is-closing class (JS adds it, waits ~170ms, then
   removes the node). */
.nuke-container .nuke-edit-overlay { animation: nuke-compose-overlay-in 0.16s ease both; }
.nuke-container .nuke-edit-modal { animation: nuke-compose-card-in 0.2s cubic-bezier(0.2, 0.8, 0.2, 1) both; }
.nuke-container .nuke-edit-overlay.is-closing { animation: nuke-compose-overlay-out 0.16s ease both; pointer-events: none; }
.nuke-container .nuke-edit-overlay.is-closing .nuke-edit-modal { animation: nuke-compose-card-out 0.16s ease both; }
@media (prefers-reduced-motion: reduce) {
  .nuke-container .nuke-edit-overlay,
  .nuke-container .nuke-edit-modal,
  .nuke-container .nuke-edit-overlay.is-closing,
  .nuke-container .nuke-edit-overlay.is-closing .nuke-edit-modal { animation-duration: 0.01ms; }
}

/* Top bar: close (X) · "Edit profile" · Save (right), like X.com. */
.nuke-container .nuke-edit-header {
  display: flex;
  align-items: center;
  gap: 20px;
  flex-shrink: 0;
  height: 53px;
  padding: 0 16px;
}
.nuke-container .nuke-edit-close {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 34px;
  height: 34px;
  margin-left: -6px;
  border: none;
  border-radius: 0;
  background: transparent;
  color: var(--nk-text);
  font-size: calc(20px - var(--font-shrink, 0px));
  cursor: pointer;
  transition: background-color 0.15s ease;
}
.nuke-container .nuke-edit-close:hover { background: rgba(15, 20, 25, 0.1); }
.nuke-container .nuke-edit-title {
  flex: 1;
  font-size: calc(20px - var(--font-shrink, 0px));
  font-weight: 800;
  color: var(--nk-text);
}
.nuke-container .nuke-edit-save {
  padding: 0 16px;
  height: 32px;
  border: none;
  border-radius: 0;
  background: var(--nk-text);       /* dark button */
  color: var(--nk-surface);
  font-size: calc(14px - var(--font-shrink, 0px));
  font-weight: 700;
  cursor: pointer;
}
.nuke-container .nuke-edit-save:disabled { opacity: 0.5; cursor: default; }

.nuke-container .nuke-edit-body { overflow-y: auto; }

/* ── Hero: banner (with camera) + avatar overlapping its bottom-left ─────── */
.nuke-container .nuke-edit-hero {
  position: relative;
  margin-bottom: 64px;   /* room for the avatar overhang */
}
.nuke-container .nuke-edit-hero-banner {
  position: relative;
  aspect-ratio: 3 / 1;
  background: #333639;
  overflow: hidden;
}
.nuke-container .nuke-edit-hero-banner-img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
}
/* Dim scrim so the camera button reads over any banner. */
.nuke-container .nuke-edit-hero-banner::after {
  content: '';
  position: absolute;
  inset: 0;
  background: rgba(0, 0, 0, 0.25);
  pointer-events: none;
}
.nuke-container .nuke-edit-hero-cam {
  position: absolute;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  z-index: 1;
  display: flex;
  align-items: center;
  justify-content: center;
  width: 44px;
  height: 44px;
  border: none;
  border-radius: 0;
  background: rgba(15, 20, 25, 0.75);
  color: #FFFFFF;
  font-size: calc(20px - var(--font-shrink, 0px));
  cursor: pointer;
  transition: background-color 0.15s ease;
}
.nuke-container .nuke-edit-hero-cam:hover { background: rgba(39, 44, 48, 0.85); }
.nuke-container .nuke-edit-hero-file { display: none; }
.nuke-container .nuke-edit-hero-avatar {
  position: absolute;
  left: 16px;
  bottom: -56px;    /* half of the 112px avatar hangs below the banner */
  width: 112px;
  height: 112px;
  border-radius: 0;
  border: 4px solid var(--nk-surface);
  background: var(--nk-surface);
  overflow: hidden;
}
.nuke-container .nuke-edit-hero-avatar .cc-pfp {
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
  border-radius: inherit;
}
.nuke-container .nuke-edit-hero-status { padding: 4px 16px 0; }

/* ── Fields: bordered, labelled boxes (X.com style) ─────────────────────── */
.nuke-container .nuke-edit-fields {
  display: flex;
  flex-direction: column;
  gap: 12px;
  padding: 4px 16px 20px;
}
.nuke-container .nuke-edit-field {
  border: 1px solid var(--nk-border);
  border-radius: 0;
  padding: 6px 8px 8px 12px;
}
.nuke-container .nuke-edit-field:focus-within { border-color: var(--nk-accent); }
.nuke-container .nuke-edit-field-head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 8px;
}
.nuke-container .nuke-edit-field-label {
  font-size: calc(13px - var(--font-shrink, 0px));
  color: var(--nk-text-2);
}
.nuke-container .nuke-edit-field:focus-within .nuke-edit-field-label { color: var(--nk-accent); }
.nuke-container .nuke-edit-field-counter {
  font-size: calc(13px - var(--font-shrink, 0px));
  color: var(--nk-text-2);
}
.nuke-container .nuke-edit-field-body {
  display: flex;
  flex-direction: column;
  gap: 8px;
  margin-top: 2px;
}
/* HIDDEN: a field flagged hidden for now (e.g. the font picker) — kept in the DOM but not shown. */
.nuke-container .nuke-edit-hidden { display: none; }

.nuke-container .nuke-edit-row {
  display: flex;
  align-items: center;
  gap: 10px;
}
.nuke-container .nuke-edit-swatch {
  width: 28px;
  height: 28px;
  flex-shrink: 0;
  border-radius: 0;
  border: 1px solid var(--nk-border);
}
/* "No colour" swatch — a red diagonal slash over a neutral tile. */
.nuke-container .nuke-edit-swatch.is-none {
  background-color: var(--nk-surface, #FFFFFF);
  background-image: linear-gradient(135deg, transparent 44%, #E0245E 44%, #E0245E 56%, transparent 56%);
}
/* Secondary "Remove color" button under the hue picker. */
.nuke-container .nuke-edit-remove-color {
  align-self: flex-start;
  margin-top: 10px;
  padding: 4px 12px;
  border: 1px solid var(--nk-border);
  border-radius: 0;
  background: transparent;
  color: var(--nk-text-2);
  font-size: calc(12px - var(--font-shrink, 0px));
  font-weight: 600;
  cursor: pointer;
}
.nuke-container .nuke-edit-remove-color:hover {
  border-color: var(--nk-text-2);
  color: var(--nk-text);
}
/* Hue slider — full-spectrum track at the picker's fixed S/L (85% / 50%). */
.nuke-container .nuke-edit-hue {
  flex: 1;
  height: 12px;
  border-radius: 0;
  appearance: none;
  -webkit-appearance: none;
  cursor: pointer;
  background: linear-gradient(to right,
    hsl(0, 85%, 50%), hsl(60, 85%, 50%), hsl(120, 85%, 50%), hsl(180, 85%, 50%),
    hsl(240, 85%, 50%), hsl(300, 85%, 50%), hsl(360, 85%, 50%));
}
.nuke-container .nuke-edit-hue::-webkit-slider-thumb {
  -webkit-appearance: none;
  width: 16px;
  height: 16px;
  border-radius: 0;
  background: var(--nk-surface);
  border: 2px solid #0F1419;
  cursor: pointer;
}
.nuke-container .nuke-edit-hue::-moz-range-thumb {
  width: 16px;
  height: 16px;
  border-radius: 0;
  background: var(--nk-surface);
  border: 2px solid #0F1419;
  cursor: pointer;
}

/* Text / select / textarea — borderless (the field box provides the frame). */
.nuke-container .nuke-edit-select,
.nuke-container .nuke-edit-input,
.nuke-container .nuke-edit-textarea {
  width: 100%;
  padding: 0;
  border: none;
  background: transparent;
  color: var(--nk-text);
  font-size: calc(17px - var(--font-shrink, 0px));
  font-family: inherit;
  box-sizing: border-box;
}
.nuke-container .nuke-edit-select:focus,
.nuke-container .nuke-edit-input:focus,
.nuke-container .nuke-edit-textarea:focus { outline: none; }
.nuke-container .nuke-edit-textarea {
  resize: none;
  min-height: 46px;
  line-height: 1.4;
}
.nuke-container .nuke-edit-select {
  cursor: pointer;
  appearance: none;
  -webkit-appearance: none;
}
.nuke-container .nuke-edit-font-preview {
  font-size: calc(20px - var(--font-shrink, 0px));
  font-weight: 700;
  color: var(--nk-text);
  min-height: 26px;
}

.nuke-container .nuke-edit-status {
  font-size: calc(12px - var(--font-shrink, 0px));
  color: var(--nk-text-2);
}
.nuke-container .nuke-edit-status:empty { display: none; }
.nuke-container .nuke-edit-status[data-state="ok"] { color: #1A9E5E; }
.nuke-container .nuke-edit-status[data-state="err"] { color: #D32F2F; }

/* ═════════════════════ Lights Out theme ═════════════════════
   Toggled by `.theme-lightsout` on <html> (no-flash script in index.html + the
   Settings toggle). Flips the theme vars to pure-black; the vars cascade to every
   rule that was migrated to them. A handful of dark elements are flipped explicitly. */
/* ╔══════════════════════════════════════════════════════════════════════════╗
   ║  LIGHTS OUT TOKENS — dark overrides (mirror of the light block above)       ║
   ║  Same token order as `body.page-nuke`; only the VALUES differ.             ║
   ╚══════════════════════════════════════════════════════════════════════════╝ */
/* Light/Dark flip is INSTANT — no transition animation. settings.ts wraps the theme-class toggle in
   .theme-switching for one flushed frame; this kills every transition while it is on so nothing fades the
   colour change. Motion resumes the instant the class drops, so hover/press are unaffected. */
html.theme-switching *,
html.theme-switching *::before,
html.theme-switching *::after {
  transition: none !important;
  animation: none !important;
}

/* X.com-style hover tooltip for message-footer icons — a small dark pill that fades in just
   below the icon (positioned in tooltip.ts). Same dark/light palette as .nuke-toast.
   pointer-events:none so it never blocks the icon it labels. */
.nuke-container .nuke-tooltip {
  position: fixed;
  z-index: 1200;
  pointer-events: none;
  padding: 2px 6px;
  border-radius: 4px;   /* match the search bar's rounded corners (was a full pill) */
  background: #0F1419;
  color: #FFFFFF;
  font-size: calc(11px - var(--font-shrink, 0px));
  font-weight: 400;
  line-height: 1.4;
  white-space: nowrap;
  box-shadow: 0 4px 14px rgba(15, 20, 25, 0.22);
  opacity: 0;
  /* Grows out of the icon: scale from the TOP edge (nearest the icon) + a small drop, on a
     crisp ease-out (expo) so it snaps in and settles — keeps the quick feel, reads cleaner
     than the plain slide. Opacity fades a touch faster than the transform settles. */
  transform-origin: top center;
  transform: translateY(-2px) scale(0.94);
  transition: opacity 0.11s ease-out, transform 0.16s cubic-bezier(0.16, 1, 0.3, 1);
}
.nuke-container .nuke-tooltip.is-visible {
  opacity: 1;
  transform: translateY(0) scale(1);
}
/* Emojis in a tooltip (the Like/Dislike reaction breakdown) render larger + sit off the text
   baseline — scale them down toward the cap height and nudge them to optical centre so they
   line up cleanly with the label + counts. */
.nuke-container .nuke-tooltip .nuke-tt-emoji {
  display: inline-block;
  font-size: 0.85em;
  line-height: 1;
  vertical-align: middle;
  font-family: 'Noto Color Emoji', 'Apple Color Emoji', 'Segoe UI Emoji', sans-serif;
}

/* ───────────────────────── Settings modal ─────────────────────────
   Opened from the left-nav "More" button. Themed via the --nk-* vars so it goes
   dark in Lights Out too. */
.nuke-settings-overlay {
  position: fixed;
  inset: 0;
  z-index: 1200;
  display: flex;
  align-items: center;
  justify-content: center;
  background: rgba(15, 20, 25, 0.4);
}
.nuke-settings-modal {
  width: 360px;
  box-sizing: border-box;
  background: var(--nk-surface);
  border: 1px solid var(--nk-border);
  border-radius: 16px;
  box-shadow: 0 12px 40px rgba(15, 20, 25, 0.18);
  overflow: hidden;
}
.nuke-settings-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 16px 18px;
  border-bottom: 1px solid var(--nk-border-soft);
  font-size: calc(17px - var(--font-shrink, 0px));
  font-weight: 800;
  color: var(--nk-text);
}
.nuke-settings-close {
  width: 30px;
  height: 30px;
  display: flex;
  align-items: center;
  justify-content: center;
  border: none;
  border-radius: 9999px;
  background: transparent;
  color: var(--nk-text-2);
  font-size: calc(16px - var(--font-shrink, 0px));
  cursor: pointer;
  transition: background 0.15s ease, color 0.15s ease;
}
.nuke-settings-close:hover { background: var(--nk-border-soft); color: var(--nk-text); }
.nuke-settings-body { padding: 6px 0 10px 0; }
.nuke-settings-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 14px;
  padding: 14px 18px;
}
/* Appearance row stacks its label over the System/Light/Dark segmented control. */
.nuke-settings-row-stack {
  flex-direction: column;
  align-items: stretch;
}
.nuke-theme-seg {
  display: flex;
  gap: 4px;
  margin-top: 12px;
  padding: 4px;
  background: var(--nk-surface-2);
  border: 1px solid var(--nk-border-soft);
  border-radius: 10px;
}
.nuke-theme-opt {
  flex: 1;
  padding: 8px 10px;
  border: none;
  border-radius: 7px;
  background: transparent;
  color: var(--nk-text-2);
  font-size: calc(13px - var(--font-shrink, 0px));
  font-weight: 600;
  cursor: pointer;
  transition: background 0.15s ease, color 0.15s ease;
}
.nuke-theme-opt:hover { color: var(--nk-text); }
.nuke-theme-opt[aria-checked="true"] {
  background: var(--nk-accent);
  color: #FFFFFF;
}
.nuke-settings-row-label { font-size: calc(15px - var(--font-shrink, 0px)); font-weight: 700; color: var(--nk-text); }
.nuke-settings-row-sub { font-size: calc(12px - var(--font-shrink, 0px)); color: var(--nk-text-2); margin-top: 2px; }
/* iOS-style toggle switch. */
.nuke-toggle {
  flex-shrink: 0;
  width: 46px;
  height: 26px;
  border: none;
  border-radius: 9999px;
  background: var(--nk-border);
  position: relative;
  cursor: pointer;
  transition: background 0.18s ease;
}
.nuke-toggle[aria-checked="true"] { background: var(--nk-accent); }
.nuke-toggle::after {
  content: '';
  position: absolute;
  top: 3px;
  left: 3px;
  width: 20px;
  height: 20px;
  border-radius: 50%;
  background: #FFFFFF;
  transition: transform 0.18s ease;
}
.nuke-toggle[aria-checked="true"]::after { transform: translateX(20px); }

/* ── Music / video upload composer (header Music + Video buttons) ─────────── */
/* The modal reuses the cred-compose card (.cred-compose-modal) + .bio-modal-textarea +
   .bio-char-count + .action-btn; only the link input + status/quota lines are new, and
   they're styled with the theme tokens so they match in light + dark. */
.cred-compose-modal .music-upload-link {
  width: 100%;
  height: 40px;
  padding: 0 12px;
  border: 1px solid var(--nk-border);
  border-radius: 10px;
  background: var(--nk-surface-2);
  color: var(--nk-text);
  font-family: var(--font-conduit);
  font-size: calc(15px - var(--font-shrink, 0px));
}
.cred-compose-modal .music-upload-link::placeholder { color: var(--nk-text-2); }
.cred-compose-modal .music-upload-link:focus {
  outline: none;
  border-color: var(--nk-accent);
  box-shadow: 0 0 0 3px rgba(var(--nk-accent-rgb), 0.18);
}
.cred-compose-modal .music-upload-status {
  min-height: 16px;
  font-size: calc(13px - var(--font-shrink, 0px));
  color: var(--nk-text-2);
}
.cred-compose-modal .music-upload-status.music-status-err { color: #e0245e; }
.cred-compose-modal .music-quota { margin-top: 4px; font-size: calc(12px - var(--font-shrink, 0px)); color: var(--nk-text-2); }

/* ═══ Contribution RECEIPT — plain purchase receipt in a presale-contribution post ════════════════════
   A small monochrome receipt (Merchant Copy type + dashed rules) — no paper skeuomorphism. Theme-
   independent black-on-white, like a real receipt. Markup: contributionReceiptHtml() in chat-render.ts. */
@font-face {
  font-family: 'Merchant Copy';
  src: url('/fonts/MerchantCopy-GOXq.ttf') format('truetype');
  font-weight: 400;
  font-display: swap;
}
.nuke-container .nuke-post-receipt-media {
  display: flex;
  justify-content: flex-start;
  width: 100%;
  margin: 8px 0 2px;
}
.nuke-container .nuke-receipt {
  width: 100%;
  max-width: 260px;
  box-sizing: border-box;
  padding: 12px 14px;
  background: #fff;
  border: 1px solid hsl(0, 0%, 80%);
  border-radius: 2px;
  font-family: 'Merchant Copy', monospace;
  font-size: calc(18px - var(--font-shrink, 0px));
  line-height: 1.05;
  color: hsl(0, 0%, 0%);
  cursor: pointer;
  transition: transform 0.09s ease, box-shadow 0.18s ease;
}
/* Display like a regular image post: NO hover scale/lift — feed images dropped theirs (see
   .nuke-post-media-1 .nuke-post-img-wrap: "no hover scale/shadow"). Only the press shadow on :active,
   which images also keep. Clicking still opens the receipt in the lightbox like an image. */
.nuke-container .nuke-receipt:active { box-shadow: var(--card-lift-press); }
.nuke-container .nuke-receipt-title {
  text-align: center;
  padding-bottom: 6px;
  margin-bottom: 6px;
  border-bottom: 1px dashed hsl(0, 0%, 0%);
  overflow-wrap: anywhere;
}
.nuke-container .nuke-receipt-table-header,
.nuke-container .nuke-receipt-table-row,
.nuke-container .nuke-receipt-total {
  display: flex;
}
.nuke-container .nuke-receipt-table-header {
  border-bottom: 1px dashed hsl(0, 0%, 0%);
  padding-bottom: 4px;
}
.nuke-container .nuke-receipt-table-row { padding-top: 6px; }
.nuke-container .nuke-receipt-total {
  border-top: 1px dashed hsl(0, 0%, 0%);
  padding-top: 4px;
  margin-top: 4px;
}
.nuke-container .nuke-receipt-col-item { flex: 1; text-align: left; }
.nuke-container .nuke-receipt-col-qty { width: 40px; text-align: right; margin-right: 12px; }
.nuke-container .nuke-receipt-col-price { width: 64px; text-align: right; }
.nuke-container .nuke-receipt-thankyou { text-align: center; font-size: calc(15px - var(--font-shrink, 0px)); margin-top: 10px; }
/* The receipt opened in the image lightbox — a wider clone, centered; no feed hover/press affordance. */
.nuke-container .nuke-chat-lightbox .nuke-receipt-lightbox { width: min(380px, 90vw); cursor: default; }
.nuke-container .nuke-chat-lightbox .nuke-receipt-lightbox:hover { transform: none; box-shadow: var(--card-lift-strong); }

/* ═══════════════════════════ DARK MODE (Lights Out) ═══════════════════════════
   NOT an invert — a DIMMED light mode. Same structure as light (dark text on lighter
   containers, hover still DARKENS, dark paper dots), just tinted much darker / softer so
   it's easy on the eyes. The seven --set-N are dimmed (backgrounds pulled down, text kept
   dark) and the base tokens go to muted-dark greys; everything themed recomputes via
   oklch(from <hue> --set-N h). Toggled by `.theme-lightsout` on <html>. Darkness is one
   knob — nudge --set-1-l / --set-2-l lighter or darker to taste. */
.theme-lightsout body.page-nuke,
.theme-lightsout body.page-onboarding {
  /* Plain PURE BLACK page background (dark mode) — no gradient/texture. */
  background-color: #000000;
  background-image: none;
  --nk-surface: #5B5B5B;            /* PURE gray (R=G=B, no tint) — panels/modals + colourless fallback */
  --nk-surface-2: #535353;          /* recessed surface */
  --nk-surface-rgb: 91, 91, 91;

  --nk-text: #141414;               /* primary text — PURE dark gray (stays dark → more contrast) */
  --nk-text-2: #3A3A3A;             /* muted text — pure neutral gray */
  --nk-hover-quote: #535353;        /* reply-hover bg — pure gray */

  /* Hover DIRECTION flips to WHITE in dark mode (as the base-token comment promises): a hover / pulse /
     highlight LIGHTENS the surface instead of darkening it, so it reads as a lit highlight on the dim UI.
     Drives --nk-hover-1/2/3 + --nk-hl-soft (and the plain --nk-hover for notif rows / menus / buttons). */
  --nk-hover-tint: #FFFFFF;
  --nk-hover: rgba(255, 255, 255, 0.045);

  /* Dividers / frame / panel lines = PURE gray (R=G=B — NOT the user's hue, no warm/blue tint) —
     the "gray/white in light" chrome. (Author CARD borders still themed via --card-border-themed.) */
  --nk-border: #525252;
  --nk-border-plain: #525252;   /* fixed neutral (dark --nk-border is already un-themed, so same value) */
  --nk-border-soft: #585858;
  --nk-border-2: #525252;

  --nk-accent: #1D9BF0;

  --page-grad-top: oklch(from var(--current-user-color) 0.505 0.038 h);   /* darker + more saturated than the page (L.590/C.020), mirroring light's fade relationship */

  /* THEME COLOR SETTINGS — DIMMED (light was 1=.962 2=.930 3=.819 4=.55 6=.240 7=.400).
     Backgrounds pulled WAY down; text stays dark (relative to the containers) — NOT inverted. */
  --set-1-l: 0.590; --set-1-c: 0.020;   /* 1 · page — profile tinted; lightened a notch for readability */
  --set-2-l: 0.560; --set-2-c: 0.030;   /* 2 · card — lighter → dark text reads easier; keeps author tint */
  --set-3-l: 0.480; --set-3-c: 0.032;   /* 3 · card border (author) */
  --set-4-l: 0.350; --set-4-c: 0.016;   /* 4 · muted / timestamps — desaturated */
  --set-5-l: 0.320; --set-5-c: 0.080;   /* 5 · link */
  --set-6-l: 0.180; --set-6-c: 0.050;   /* 6 · heading / usernames / icons — desaturated */
  --set-7-l: 0.270; --set-7-c: 0.030;   /* 7 · body text — desaturated */
}
/* Card top/left bevel edge: light-mode pure #FFF is too bright on a dimmed card → a soft lit edge.
   The chat profile header (header-2) is styled as a post card, so it shares the same softened bevel. */
.theme-lightsout .nuke-container .nuke-chat-header-2,
.theme-lightsout .nuke-container .nuke-chat-day-sep {
  border-top-color: rgba(255, 255, 255, 0.22);
  border-left-color: rgba(255, 255, 255, 0.22);
}
/* Posts: the LEFT bevel is now an INSET shadow (real left border carries the panel frame), so soften that
   via --post-side-bevel — NOT border-left-color, which would recolour the frame. Top stays a real border. */
.theme-lightsout .nuke-container .nuke-post {
  border-top-color: rgba(255, 255, 255, 0.22);
  --post-side-bevel: rgba(255, 255, 255, 0.22);
}
/* The post left highlight in dark is translucent white over its profile-tinted card fill, so it READS as a
   light profile tint. The radio/stream sit on pure black — a translucent white there reads as flat gray — so
   match the post COMPOSITED look with a SOLID colour = 22% white over the self card fill (SETTING 2). */
.theme-lightsout .nuke-container .nuke-radio-bar-info,
.theme-lightsout .nuke-container .nuke-radio-video-screen {
  border-left-color: color-mix(in srgb, #ffffff 22%, var(--self-card-fill));
}
/* Inner bevel (reply/pfp/image) softened for the dimmed cards. */
.theme-lightsout .nuke-container { --card-bevel: rgba(255, 255, 255, 0.18); }
/* HOVER + PULSE LIGHTEN in dark mode (opposite of light) — a lit highlight ABOVE the card (L.560):
   hover L.590 is a subtle lift, the arrival-flash / scroll-to PULSE L.635 a slightly clearer flash. */
.theme-lightsout .nuke-container .nuke-post {
  --card-hover-themed: oklch(from var(--nuke-cmd-color) 0.590 0.030 h);
  --card-darken-themed: oklch(from var(--nuke-cmd-color) 0.635 0.038 h);
}
.theme-lightsout .nuke-container .nuke-post-quote {
  --card-hover-themed: oklch(from var(--nuke-quote-raw) 0.590 0.030 h);
}
.theme-lightsout .nuke-container .nuke-row {
  --card-hover-themed: oklch(from var(--nuke-cmd-color) 0.590 0.030 h);
  --card-darken-themed: oklch(from var(--nuke-cmd-color) 0.635 0.038 h);
}
/* Notifications list — light hovers/unread/glyph → dimmed. */
.theme-lightsout .nuke-container .nuke-notif-list .notif-item.unread { background: rgba(var(--nk-accent-rgb), 0.10); }
.theme-lightsout .nuke-container .nuke-notif-list .notif-item-glyph { background: rgba(var(--nk-accent-rgb), 0.18); }
/* Chat disclaimer (pale-yellow note → dimmed). */
.theme-lightsout .nuke-container #nuke-chat-disclaimer { background: rgba(0, 0, 0, 0.06); color: var(--nk-text-2); }
/* Links / @mentions, the /word tokens, and the @handle pill's text START from their LIGHT-mode colour
   formula, then for DARK mode ONLY are lightly DESATURATED + brightened a LOT (much higher OKLCH
   lightness) so they read soft but clearly lit on the dimmed cards. */
.theme-lightsout .nuke-container .nuke-chat-link,
.theme-lightsout .nuke-container .nuke-chat-mention {
  color: oklch(from var(--nk-accent) 0.67 calc(c * 0.58) h);
}
.theme-lightsout .nuke-container .nuke-chat-command {
  color: oklch(from hsl(from var(--nuke-cmd-color, #808080) calc(h + 15) s l) clamp(0.69, l, 0.82) calc(c * 0.48) h);
}
/* Handle pill: only its TEXT is desaturated + brightened here; the pill background keeps the light formula. */
.theme-lightsout .nuke-container .nuke-post-handle,
.theme-lightsout .nuke-container .nuke-post-quote-handle {
  color: oklch(from var(--nuke-quote-raw, #808080) clamp(0.37, l, 0.49) calc(c * 0.35) calc(h + 180));
}
