/* === Post card === */
.post {
  border: var(--card-border-width) var(--card-border-style) var(--border);
  border-radius: var(--card-radius);
  background: var(--card-bg);
  padding: 1rem 1.1rem 1.1rem 1.1rem;
  position: relative;
  transition: border-color 0.2s;
  /* User-content can include zalgo / fullwidth / RTL exploits / unbroken
     emoji ZWJ chains that try to escape the post column. `min-width: 0`
     lets the card actually shrink inside its grid/flex parent on phones
     instead of forcing horizontal overflow on the whole feed. */
  min-width: 0;
  max-width: 100%;
}
.post:hover { border-color: var(--fg-dim); }

.post .post-meta {
  display: flex;
  justify-content: space-between;
  align-items: baseline;
  margin-bottom: 0.6rem;
  font-size: 0.85rem;
  letter-spacing: 0.04em;
  /* If the author byline carries a 200-codepoint zalgo handle, the
     no-wrap default would shove the #shortid right off-screen on a
     phone. Allow the meta row to break onto two lines and let each
     half shrink. */
  flex-wrap: wrap;
  gap: 0.3rem 0.6rem;
}
.post .post-meta-left {
  min-width: 0;
  flex: 1 1 auto;
  overflow-wrap: anywhere;
  word-break: break-word;
}
.post .post-time {
  color: var(--cool);
  text-shadow: 0 0 4px rgba(var(--cool-rgb), 0.35);
  border-bottom: none;
}
.post .post-time:hover {
  color: var(--cool);
  text-shadow: 0 0 8px rgba(var(--cool-rgb), 0.6);
}
.post .post-id {
  color: var(--fg-faint);
  font-size: 0.75rem;
  letter-spacing: 0.08em;
  user-select: all;
  flex: 0 0 auto;
}

.post .post-title {
  font-family: var(--font-display);
  font-size: 1.6rem;
  color: var(--hot);
  margin-bottom: 0.4rem;
  text-shadow: 0 0 6px rgba(var(--hot-rgb), 0.35);
  letter-spacing: 0.04em;
  /* Horizontal-only overflow guard: a fullwidth headline like
     "ＨＥＬＬＯ ＷＯＲＬＤ" or a zalgo'd title would otherwise push
     the post column off the right edge of a phone. Vertical bleed
     from combining-mark stacks is intentionally left uncontained —
     it reads as a cool glitch. */
  overflow-wrap: anywhere;
  word-break: break-word;
}

.post .post-body {
  color: var(--fg);
  /* `anywhere` is more aggressive than `break-word`: it lets the browser
     break inside *any* sequence of characters when a line would
     otherwise overflow, which matters for fullwidth runs and for
     non-CJK no-break sequences crafted to escape the column. We
     deliberately don't clip vertically — zalgo combining-mark stacks
     that bleed above/below adjacent lines are part of the aesthetic,
     not a bug. */
  overflow-wrap: anywhere;
  word-break: break-word;
}
.post .post-body a {
  color: var(--cool);
  border-bottom: 1px dotted var(--cool);
}
.post .post-body a:hover {
  color: var(--hot);
  border-bottom-color: var(--hot);
  text-shadow: 0 0 6px rgba(var(--hot-rgb), 0.45);
}

/* ---- markdown constructs (render.js renderBlock / applyInline) -------- */
.post .post-body .md-h {
  color: var(--fg);
  font-family: var(--font-display, var(--font-body));
  line-height: 1.25;
  margin: 0.8em 0 0.4em;
}
.post .post-body .md-h1 { font-size: 1.5rem; }
.post .post-body .md-h2 { font-size: 1.3rem; }
.post .post-body .md-h3 { font-size: 1.15rem; }
.post .post-body .md-h4,
.post .post-body .md-h5,
.post .post-body .md-h6 { font-size: 1rem; color: var(--fg-dim); }
.post .post-body .md-hr {
  border: 0;
  border-top: 1px dashed var(--border);
  margin: 1em 0;
}
.post .post-body .md-quote {
  margin: 0.6em 0;
  padding: 0.2em 0 0.2em 0.8em;
  border-left: 3px solid var(--cool);
  color: var(--fg-dim);
}
.post .post-body .md-list {
  margin: 0.5em 0;
  padding-left: 1.5em;
}
.post .post-body .md-list li { margin: 0.15em 0; }
.post .post-body strong { color: var(--fg); font-weight: 700; }
.post .post-body em { font-style: italic; }
.post .post-body del { opacity: 0.6; }

/* ---- code (render.js reinsertCodePlaceholders) ----------------------- */
.post .post-body .code-inline,
.post .post-body .code-block {
  font-family: var(--font-mono, ui-monospace, "Cascadia Code", "Fira Code", Consolas, monospace);
  font-size: 0.9em;
}
.post .post-body .code-inline {
  padding: 0.1em 0.35em;
  background: rgba(var(--fg-rgb), 0.06);
  border: 1px solid var(--border);
  border-radius: var(--radius, 2px);
  white-space: pre-wrap;
}
.post .post-body .code-block {
  position: relative;
  margin: 0.7em 0;
  padding: 0.7em 0.85em;
  background: rgba(var(--fg-rgb), 0.04);
  border: 1px solid var(--border);
  border-left: 3px solid var(--hot);
  border-radius: var(--radius, 2px);
  overflow-x: auto;
  line-height: 1.45;
}
.post .post-body .code-block code {
  display: block;
  white-space: pre;
}
/* Language label chip (data-lang), drawn in the top-right corner. */
.post .post-body .code-block[data-lang]::before {
  content: attr(data-lang);
  position: absolute;
  top: 0;
  right: 0;
  padding: 0.1em 0.5em;
  font-size: 0.7rem;
  color: var(--bg, #000);
  background: var(--hot);
  letter-spacing: 0.05em;
}
/* Minimal highlighter tokens (render.js highlightCode). */
.post .post-body .code-block .tok-str { color: var(--ok, #6c6); }
.post .post-body .code-block .tok-com { color: var(--fg-dim); font-style: italic; }
.post .post-body .code-block .tok-num { color: var(--cool); }
.post .post-body .code-block .tok-kw  { color: var(--hot); font-weight: 600; }

.post .post-actions {
  margin-top: 0.9rem;
  display: flex;
  gap: 0.5rem;
  justify-content: flex-end;
}

/* "Copy share link" button. Reuses the .ghost look, just smaller, and
   flips to an ok/err colour for ~1.5s after a copy so the click feels
   acknowledged even though we don't pop a toast. */
.post-actions .share-btn,
.share-btn {
  font-size: 0.75rem;
  padding: 0.25em 0.7em;
  letter-spacing: 0.08em;
}
.share-btn.ok {
  color: var(--ok);
  border-color: rgba(var(--ok-rgb), 0.55);
  background: rgba(var(--ok-rgb), 0.05);
  text-shadow: 0 0 6px rgba(var(--ok-rgb), 0.45);
}
.share-btn.ok:hover {
  color: var(--ok);
  border-color: rgba(var(--ok-rgb), 0.7);
  background: rgba(var(--ok-rgb), 0.08);
}
.share-btn.err {
  color: var(--err);
  border-color: rgba(var(--err-rgb), 0.55);
  background: rgba(var(--err-rgb), 0.05);
}

/* Anonymous like button. Mirrors the .share-btn footprint so the
   post-actions row stays visually balanced. `.liked` flips to the hot
   pink palette + glow so a tapped state reads at a glance. */
.like-btn {
  font-size: 0.75rem;
  padding: 0.25em 0.7em;
  letter-spacing: 0.08em;
}

/* Author/admin delete button on a post-actions row. Same compact
   footprint as .share-btn / .like-btn so the row stays a balanced
   strip of three same-sized buttons. .danger already paints it red;
   here we just match the small size since the generic `button.small`
   rule is scoped to `.comment .comment-actions`. */
.post-actions .post-delete-btn,
.post-actions .post-edit-btn {
  font-size: 0.75rem;
  padding: 0.25em 0.7em;
  letter-spacing: 0.08em;
}
.like-btn.liked {
  color: var(--hot);
  border-color: rgba(var(--hot-rgb), 0.55);
  background: rgba(var(--hot-rgb), 0.06);
  text-shadow: 0 0 6px rgba(var(--hot-rgb), 0.5);
}
.like-btn.liked:hover {
  color: var(--hot);
  border-color: rgba(var(--hot-rgb), 0.8);
  background: rgba(var(--hot-rgb), 0.1);
}
.like-btn.pending {
  opacity: 0.6;
}
.like-btn.err {
  color: var(--err);
  border-color: rgba(var(--err-rgb), 0.55);
  background: rgba(var(--err-rgb), 0.05);
}

/* corner ticks. `--corner-tick-display` lets themes hide them (paper,
   brutalist) or themes redefine `content` on these pseudo-elements to
   swap the L-bracket for a different glyph (sakura uses ✿). */
.post::before, .post::after {
  content: "";
  position: absolute;
  display: var(--corner-tick-display);
  width: 10px; height: 10px;
  border: 1px solid var(--border-hot);
  pointer-events: none;
}
.post::before { top: -1px; left: -1px; border-right: none; border-bottom: none; }
.post::after  { bottom: -1px; right: -1px; border-left: none; border-top: none; }

/* === CRT YouTube embed === */
.crt-embed {
  margin: 1rem 0;
  padding: 0;
  position: relative;
}
.crt-bezel {
  position: relative;
  background: #000;
  border: 1px solid var(--border-hot);
  box-shadow:
    inset 0 0 0 1px #000,
    inset 0 0 18px rgba(var(--hot-rgb), 0.18),
    0 0 14px rgba(var(--hot-rgb), 0.20);
  padding: 8px;
  border-radius: 2px;
}
.crt-bezel::before,
.crt-bezel::after {
  content: "";
  position: absolute;
  display: var(--corner-tick-display);
  width: 12px; height: 12px;
  border: 1px solid var(--cool);
  pointer-events: none;
  opacity: 0.7;
}
.crt-bezel::before { top: 2px; left: 2px;  border-right: none; border-bottom: none; }
.crt-bezel::after  { bottom: 2px; right: 2px; border-left: none;  border-top: none; }

.crt-bezel .crt-frame {
  position: relative;
  width: 100%;
  aspect-ratio: 16 / 9;
  background: #000;
  overflow: hidden;
}
.crt-bezel iframe {
  position: absolute; inset: 0;
  width: 100%; height: 100%;
  border: 0;
  background: #000;
}
.crt-scanlines {
  position: absolute; inset: 0;
  pointer-events: none;
  background: repeating-linear-gradient(
    to bottom,
    rgba(0,0,0,0) 0px,
    rgba(0,0,0,0) 2px,
    rgba(0,0,0,0.30) 3px,
    rgba(0,0,0,0) 4px
  );
  mix-blend-mode: multiply;
  z-index: 3;
}
.crt-rec {
  position: absolute;
  top: 14px; right: 14px;
  font-family: var(--font-display);
  font-size: 0.9rem;
  letter-spacing: 0.15em;
  color: var(--err);
  text-shadow: 0 0 8px rgba(var(--err-rgb), 0.7);
  background: rgba(0,0,0,0.55);
  padding: 1px 6px;
  border: 1px solid rgba(var(--err-rgb), 0.5);
  z-index: 4;
  pointer-events: none;
}
.crt-rec::before {
  content: "\25CF";
  margin-right: 0.35em;
  animation: rec-blink 1.1s steps(2, end) infinite;
}
@keyframes rec-blink {
  0%, 49%  { opacity: 1; }
  50%, 100%{ opacity: 0.15; }
}

/* === Inline image embed (pasted clipboard images / manual ![](uploads/...)) ===
   Constrained to the post column so a 4000x3000 phone photo doesn't blow out
   the layout. `max-height` keeps tall portraits from dominating the viewport;
   `object-fit: contain` preserves aspect ratio while letting us cap both
   dimensions. Matched to the CRT-bezel aesthetic with a thin cool border and
   a soft glow so it sits next to YouTube embeds without looking out of place. */
.img-embed {
  margin: 1rem 0;
  display: block;
}
/* Specificity bumped to `.post .post-body .img-embed a` (and the comment
   equivalent) so this wins over the generic body-link rules elsewhere.
   Without it every pasted image would inherit a dotted underline along its
   bottom edge from the link-style rule. */
.post .post-body .img-embed a,
.comment .comment-body .img-embed a {
  display: block;
  border: 1px solid var(--border);
  background: #000;
  padding: 4px;
  box-shadow:
    inset 0 0 14px rgba(var(--cool-rgb), 0.10),
    0 0 10px rgba(var(--cool-rgb), 0.12);
  transition: box-shadow 0.15s ease, border-color 0.15s ease;
  color: inherit;
}
.post .post-body .img-embed a:hover,
.comment .comment-body .img-embed a:hover {
  border-color: var(--border-hot);
  box-shadow:
    inset 0 0 14px rgba(var(--hot-rgb), 0.14),
    0 0 14px rgba(var(--hot-rgb), 0.22);
  text-shadow: none;
}
.img-embed img {
  display: block;
  max-width: 100%;
  max-height: 70vh;
  width: auto;
  height: auto;
  margin: 0 auto;
  object-fit: contain;
}
/* In a comment we cap embedded images shorter than in a post body so a
   single comment doesn't dominate a full viewport — comments are
   side-conversation, not the headline. */
.comment .comment-body .img-embed img {
  max-height: 40vh;
}

/* === Photo gallery (2+ consecutive ![](uploads/...) on single-newline lines) ===
   Square-thumb grid that scales smoothly from 2 cols on phones up to 4 on
   desktop. Same CRT-bezel framing as .img-embed so a gallery sits next to
   a YouTube embed without looking like a foreign component. Clicks are
   intercepted by gallery.js for a lightbox; if that script is missing
   each tile is still a plain link to the raw image, so nothing breaks. */
.img-gallery {
  margin: 1rem 0;
  display: grid;
  grid-template-columns: repeat(2, 1fr);
  gap: 6px;
}
@media (min-width: 520px) {
  .img-gallery { grid-template-columns: repeat(3, 1fr); }
}
.img-gallery[data-count="2"] { grid-template-columns: repeat(2, 1fr); }
.img-gallery[data-count="4"] { grid-template-columns: repeat(2, 1fr); }
@media (min-width: 760px) {
  .img-gallery[data-count="4"]  { grid-template-columns: repeat(2, 1fr); }
  .img-gallery[data-count="5"],
  .img-gallery[data-count="6"]  { grid-template-columns: repeat(3, 1fr); }
}
.img-gallery .img-gallery-item {
  display: block;
  position: relative;
  aspect-ratio: 1 / 1;
  overflow: hidden;
  border: 1px solid var(--border);
  background: #000;
  box-shadow:
    inset 0 0 14px rgba(var(--cool-rgb), 0.10),
    0 0 8px rgba(var(--cool-rgb), 0.10);
  transition: box-shadow 0.15s ease, border-color 0.15s ease, transform 0.15s ease;
  color: inherit;
  cursor: zoom-in;
}
.img-gallery .img-gallery-item:hover {
  border-color: var(--border-hot);
  box-shadow:
    inset 0 0 14px rgba(var(--hot-rgb), 0.16),
    0 0 14px rgba(var(--hot-rgb), 0.24);
  transform: translateY(-1px);
  text-shadow: none;
}
.img-gallery .img-gallery-item img {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
  /* Bias toward the center top so portraits don't crop heads off as
     aggressively as a centred crop does. */
  object-position: center 30%;
}

/* === Lightbox (full-viewport image viewer with prev/next) ===
   Mounted lazily by gallery.js when the visitor clicks a gallery tile.
   Keyboard: ←/→ navigate, Esc closes. Click outside the image closes too. */
.lightbox {
  position: fixed;
  inset: 0;
  z-index: 200;
  background: rgba(0, 0, 0, 0.92);
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 1.5rem;
  cursor: zoom-out;
  /* sit above scanlines + vignette overlays (z-index: 49/50) */
}
.lightbox[hidden] { display: none; }
.lightbox-stage {
  position: relative;
  max-width: min(100%, 1400px);
  max-height: 100%;
  display: flex;
  align-items: center;
  justify-content: center;
  cursor: default;
}
.lightbox-img {
  display: block;
  max-width: 100%;
  max-height: calc(100vh - 3rem);
  object-fit: contain;
  border: 1px solid var(--border-hot);
  background: #000;
  box-shadow:
    inset 0 0 14px rgba(var(--cool-rgb), 0.12),
    0 0 30px rgba(var(--hot-rgb), 0.30);
}
.lightbox-btn {
  position: absolute;
  top: 50%;
  transform: translateY(-50%);
  font-family: var(--font-display);
  font-size: 2.2rem;
  line-height: 1;
  color: var(--fg);
  background: rgba(0, 0, 0, 0.55);
  border: 1px solid var(--border-cool);
  padding: 0.35em 0.55em;
  cursor: pointer;
  letter-spacing: 0;
  text-shadow: 0 0 6px rgba(var(--cool-rgb), 0.45);
  transition: color 0.15s, border-color 0.15s, background 0.15s, text-shadow 0.15s;
}
.lightbox-btn:hover {
  color: var(--hot);
  border-color: var(--hot);
  background: rgba(var(--hot-rgb), 0.10);
  text-shadow: 0 0 8px rgba(var(--hot-rgb), 0.6);
}
.lightbox-prev { left: -3.5rem; }
.lightbox-next { right: -3.5rem; }
.lightbox-close {
  position: absolute;
  top: -2.5rem;
  right: 0;
  font-family: var(--font-display);
  font-size: 1.4rem;
  color: var(--fg-dim);
  background: transparent;
  border: 1px solid var(--border);
  padding: 0.15em 0.6em;
  cursor: pointer;
  letter-spacing: 0.1em;
}
.lightbox-close:hover {
  color: var(--err);
  border-color: var(--err);
  text-shadow: 0 0 6px rgba(var(--err-rgb), 0.5);
}
.lightbox-counter {
  position: absolute;
  bottom: -2rem;
  left: 50%;
  transform: translateX(-50%);
  font-family: var(--font-display);
  color: var(--cool);
  letter-spacing: 0.15em;
  font-size: 1rem;
  text-shadow: 0 0 6px rgba(var(--cool-rgb), 0.45);
  user-select: none;
}

/* Original-filename caption beneath the lightbox image. The text comes
   from each tile's `<img alt>`, which `render.js` populates from the
   markdown `![alt](url)` alt slot, which `upload.js` fills with the
   server-returned `original_filename`. Old posts (with empty alt) get
   the element hidden via the `hidden` HTML attribute. */
.lightbox-filename {
  position: absolute;
  bottom: -2rem;
  left: 50%;
  transform: translateX(-50%);
  font-family: var(--font-mono, monospace);
  color: var(--fg-dim);
  font-size: 0.85rem;
  letter-spacing: 0.04em;
  text-shadow: 0 0 4px rgba(var(--cool-rgb), 0.25);
  user-select: text;
  /* Truncate absurd filenames with an ellipsis so the layout never
     stretches past the viewport on phones. */
  max-width: min(90vw, 1200px);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  pointer-events: auto;
}
.lightbox-filename[hidden] { display: none; }
/* When BOTH the filename and the counter are visible, the filename sits
   directly under the image and the counter drops one row lower. When
   only the counter is visible (no filename, e.g. old empty-alt posts),
   it keeps its original position via the rule above. */
.lightbox-stage:has(.lightbox-filename:not([hidden])) .lightbox-counter {
  bottom: -3.6rem;
}
/* On small screens the side buttons would clip off-canvas, so float them
   inside the stage and let them overlay the image edges. */
@media (max-width: 760px) {
  .lightbox { padding: 0.75rem; }
  .lightbox-prev { left: 0.25rem; }
  .lightbox-next { right: 0.25rem; }
  .lightbox-btn  { font-size: 1.8rem; background: rgba(0, 0, 0, 0.7); }
  .lightbox-img  { max-height: calc(100vh - 4.5rem); }
}

/* === Composer / admin === */
.composer-card {
  border: var(--card-border-width) solid var(--border);
  border-radius: var(--card-radius);
  background: var(--card-bg);
  padding: 1.1rem 1.2rem 1.2rem 1.2rem;
  margin-bottom: 1.5rem;
  position: relative;
}
.card-title {
  font-family: var(--font-display);
  font-size: 1.5rem;
  color: var(--cool);
  margin-bottom: 0.8rem;
  letter-spacing: 0.08em;
  text-shadow: 0 0 8px rgba(var(--cool-rgb), 0.35);
}

#token-form, #post-form {
  display: flex;
  flex-direction: column;
  gap: 0.6rem;
}
#token-form { flex-direction: row; gap: 0.6rem; }
#token-form input { flex: 1; }

#post-form label {
  margin-top: 0.2rem;
}
#post-form label:first-of-type { margin-top: 0; }

.composer-actions {
  display: flex;
  align-items: center;
  gap: 1rem;
  margin-top: 0.5rem;
}

.preview-wrap {
  margin-top: 1.2rem;
  border-top: 1px dashed var(--border);
  padding-top: 0.8rem;
}
.preview-wrap summary {
  cursor: pointer;
  margin-bottom: 0.6rem;
  user-select: none;
}
.preview-wrap .preview {
  margin-top: 0.6rem;
}

/* Markdown/FX cheat-sheet under the composer. Reuses .preview-wrap for the
   dashed frame; this just lays out the reference list + inline code chips. */
.md-cheat-body { line-height: 1.6; }
.md-cheat-body ul {
  list-style: none;
  margin: 0.4rem 0;
  padding: 0;
}
.md-cheat-body li { margin: 0.25em 0; }
.md-cheat-body code {
  color: var(--fg);
  background: rgba(var(--fg-rgb, 200, 200, 200), 0.08);
  border: 1px solid var(--border);
  border-radius: 3px;
  padding: 0 0.3em;
}

.msg.ok    { color: var(--ok);  text-shadow: 0 0 4px rgba(var(--ok-rgb), 0.45); }
.msg.err   { color: var(--err); text-shadow: 0 0 4px rgba(var(--err-rgb), 0.45); }

.status {
  color: var(--fg-dim);
  font-size: 0.9rem;
  text-align: center;
  padding: 1.5rem 0;
  letter-spacing: 0.12em;
}

/* === Identity banner (admin/guest console) === */
.who-banner {
  color: var(--cool);
  font-size: 0.85rem;
  letter-spacing: 0.08em;
  padding: 0.35rem 0.6rem;
  margin-bottom: 0.9rem;
  border: 1px dashed var(--border-cool);
  background: rgba(var(--cool-rgb), 0.04);
  text-shadow: 0 0 4px rgba(var(--cool-rgb), 0.35);
}

/* === Author byline on posts === */
.post-author {
  color: var(--hot);
  font-size: 0.75rem;
  letter-spacing: 0.08em;
  margin-left: 0.6rem;
  text-shadow: 0 0 4px rgba(var(--hot-rgb), 0.35);
  /* A glitched/zalgo'd guest handle would otherwise force horizontal
     overflow on a phone-width post-meta row. */
  overflow-wrap: anywhere;
  word-break: break-word;
}

/* === Command palette (search) === */
.cmdp-overlay {
  display: none;
  position: fixed;
  inset: 0;
  z-index: 100;
  background: rgba(0, 0, 0, 0.55);
  padding-top: 12vh;
  justify-content: center;
  align-items: flex-start;
}
.cmdp-overlay.cmdp-open { display: flex; }
.cmdp-panel {
  width: min(620px, 92vw);
  max-height: 70vh;
  display: flex;
  flex-direction: column;
  background: var(--bg, #000);
  border: 1px solid var(--hot);
  border-radius: var(--radius, 2px);
  box-shadow: 0 8px 40px rgba(0, 0, 0, 0.5),
              0 0 18px rgba(var(--hot-rgb), 0.25);
  overflow: hidden;
}
.cmdp-input {
  width: 100%;
  box-sizing: border-box;
  padding: 0.7em 0.9em;
  font-family: var(--font-body);
  font-size: 1rem;
  color: var(--fg);
  background: rgba(var(--fg-rgb), 0.03);
  border: 0;
  border-bottom: 1px solid var(--border);
  outline: none;
}
.cmdp-status {
  padding: 0.3em 0.9em;
  color: var(--fg-dim);
  border-bottom: 1px dashed var(--border);
}
.cmdp-list {
  list-style: none;
  margin: 0;
  padding: 0;
  overflow-y: auto;
}
.cmdp-item {
  display: flex;
  flex-direction: column;
  gap: 0.15em;
  padding: 0.5em 0.9em;
  cursor: pointer;
  border-bottom: 1px solid rgba(var(--fg-rgb), 0.05);
}
.cmdp-item.cmdp-active { background: rgba(var(--hot-rgb), 0.12); }
.cmdp-title { color: var(--fg); font-weight: 600; }
.cmdp-room { color: var(--cool); font-size: 0.72rem; letter-spacing: 0.05em; }
.cmdp-snip {
  color: var(--fg-dim);
  font-size: 0.82rem;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* === "edited" marker === */
.post-edited {
  color: var(--fg-dim);
  font-size: 0.72rem;
  font-style: italic;
  letter-spacing: 0.06em;
  margin-left: 0.6rem;
  white-space: nowrap;
}

/* === Reading time + view count meta === */
.post-readtime,
.post-views {
  color: var(--fg-dim);
  font-size: 0.72rem;
  letter-spacing: 0.06em;
  margin-left: 0.6rem;
  white-space: nowrap;
}

/* === Comments-count link on feed === */
.post-comments-link {
  display: inline-block;
  margin-top: 0.7rem;
  font-size: 0.8rem;
  color: var(--fg-dim);
  border-bottom: 1px dotted var(--fg-faint);
  letter-spacing: 0.06em;
}
.post-comments-link:hover {
  color: var(--cool);
  border-bottom-color: var(--cool);
}

/* === Comments section ===
   Used twice: standalone under the single post on post.html, and nested
   inside each .post card on the feed. When nested, the post card already
   provides the frame, so we drop the border + background and just use a
   dashed top divider to visually separate it from the post body. */
.comments-section {
  border: var(--card-border-width) var(--card-border-style) var(--border);
  border-radius: var(--card-radius);
  background: var(--card-bg);
  padding: 1rem 1.1rem 1.2rem 1.1rem;
  margin-top: 1.5rem;
}
.post > .comments-section {
  border: 0;
  background: none;
  padding: 0.9rem 0 0 0;
  margin-top: 1rem;
  border-top: 1px dashed var(--border);
}
.comments-title {
  font-family: var(--font-display);
  font-size: 1.2rem;
  color: var(--cool);
  letter-spacing: 0.08em;
  text-shadow: 0 0 6px rgba(var(--cool-rgb), 0.30);
  margin: 0 0 0.7rem 0;
}

.comment-list {
  display: flex;
  flex-direction: column;
  gap: 0.7rem;
  margin-bottom: 1.2rem;
}

.comment {
  border-left: 2px solid var(--border-cool);
  padding: 0.45rem 0.6rem 0.55rem 0.7rem;
  background: var(--card-inset-bg);
  min-width: 0;
  max-width: 100%;
}
.comment .comment-meta {
  display: flex;
  justify-content: space-between;
  align-items: baseline;
  font-size: 0.8rem;
  margin-bottom: 0.25rem;
  flex-wrap: wrap;
  gap: 0.2rem 0.6rem;
}
.comment .comment-author {
  color: var(--hot);
  letter-spacing: 0.06em;
  text-shadow: 0 0 4px rgba(var(--hot-rgb), 0.30);
  /* Anon handles aren't validated server-side beyond a length cap, so
     a zalgo'd 40-codepoint name would otherwise blow the meta line off
     the right edge on a phone. */
  overflow-wrap: anywhere;
  word-break: break-word;
  min-width: 0;
}
.comment .comment-time {
  color: var(--fg-faint);
  font-size: 0.75rem;
  letter-spacing: 0.06em;
  flex: 0 0 auto;
}
.comment .comment-body {
  color: var(--fg);
  /* Horizontal-only guard, same as `.post .post-body`: let any
     character break so a fullwidth/no-break run can't escape the
     comment box width-wise. Zalgo combining marks are allowed to
     bleed onto neighbouring lines on purpose. */
  overflow-wrap: anywhere;
  word-break: break-word;
  font-size: 0.95rem;
}
/* Bare URLs in a comment body get linkified by R.toHtml — match the
   post body's cool/hot link palette so the two read consistently. The
   `.img-embed a` rule above overrides this for embedded-image links. */
.comment .comment-body a {
  color: var(--cool);
  border-bottom: 1px dotted var(--cool);
}
.comment .comment-body a:hover {
  color: var(--hot);
  border-bottom-color: var(--hot);
  text-shadow: 0 0 6px rgba(var(--hot-rgb), 0.45);
}
/* R.toHtml wraps text in <p>; comments are short so we tighten the
   default paragraph margins to avoid awkward whitespace inside a
   one-line comment. */
.comment .comment-body p {
  margin: 0;
}
.comment .comment-body p + p {
  margin-top: 0.5rem;
}
.comment .comment-actions {
  margin-top: 0.5rem;
  display: flex;
  justify-content: flex-end;
  gap: 0.5rem;
}
.comment .comment-actions button.small {
  font-size: 0.75rem;
  padding: 0.2em 0.6em;
}

/* === Comment compose form === */
.comment-form {
  display: flex;
  flex-direction: column;
  gap: 0.45rem;
  border-top: 1px dashed var(--border);
  padding-top: 1rem;
}
.comment-form label { margin-top: 0.2rem; }
.comment-form label:first-of-type { margin-top: 0; }
.comment-form textarea { min-height: 90px; }
.comment-actions {
  display: flex;
  align-items: center;
  gap: 1rem;
  margin-top: 0.4rem;
}

/* Locked handle field when the visitor is signed in as admin/guest.
   The input is readonly + dimmed so it's obvious the value is fixed,
   plus the identity badge highlights the actual handle. */
.comment-author.locked {
  opacity: 0.7;
  cursor: not-allowed;
  border-style: dashed;
}
.comment-identity {
  color: var(--hot);
  letter-spacing: 0.06em;
  text-shadow: 0 0 4px rgba(var(--hot-rgb), 0.35);
  margin: 0 0.35em;
}

/* Tiny inline hint next to "your transmission" — only shown after the
   visitor authenticates as admin/guest and clipboard paste-to-upload
   is actually wired up on the textarea. Anon visitors never see it. */
.comment-paste-hint {
  font-style: italic;
  margin-left: 0.4em;
}

/* Honeypot field: invisible to humans, present in the DOM for bots.
   We use absolute off-screen positioning rather than display:none so
   dumb crawlers that skip hidden inputs still fill it in. */
.honeypot {
  position: absolute;
  left: -10000px;
  top: auto;
  width: 1px;
  height: 1px;
  overflow: hidden;
}

/* === Live chat panel ===
   Writers-only ephemeral chat docked above the feed on index.html. The
   whole `<section>` starts hidden via the `hidden` HTML attribute and is
   only revealed by chat.js once a writer token is found in localStorage,
   so anon visitors literally don't see it. Inside, `.collapsed` flips
   between a thin one-line toggle bar and the full chat body. */
.chat-panel {
  border: var(--card-border-width) solid var(--border);
  border-radius: var(--card-radius);
  background: var(--card-bg);
  margin-bottom: 1.5rem;
  position: relative;
}
.chat-panel::before, .chat-panel::after {
  content: "";
  position: absolute;
  display: var(--corner-tick-display);
  width: 10px; height: 10px;
  border: 1px solid var(--border-cool);
  pointer-events: none;
}
.chat-panel::before { top: -1px; left: -1px; border-right: none; border-bottom: none; }
.chat-panel::after  { bottom: -1px; right: -1px; border-left: none; border-top: none; }

.chat-toggle {
  width: 100%;
  text-align: left;
  border: 0;
  border-bottom: 1px dashed var(--border);
  padding: 0.55rem 0.9rem;
  font-family: var(--font-display);
  font-size: 1.1rem;
  color: var(--cool);
  letter-spacing: 0.08em;
  text-shadow: 0 0 6px rgba(var(--cool-rgb), 0.30);
  text-transform: none;
  background: rgba(var(--cool-rgb), 0.03);
  cursor: pointer;
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 0.6rem;
}
.chat-toggle:hover {
  color: var(--hot);
  border-color: var(--border-hot);
  background: rgba(var(--hot-rgb), 0.05);
  text-shadow: 0 0 6px rgba(var(--hot-rgb), 0.45);
}
.chat-toggle .chat-presence {
  font-family: var(--font-body);
  font-size: 0.8rem;
  letter-spacing: 0.08em;
  color: var(--fg-dim);
}
.chat-toggle .chat-presence.live {
  color: var(--ok);
  text-shadow: 0 0 4px rgba(var(--ok-rgb), 0.4);
}
.chat-toggle .chat-presence.dead {
  color: var(--err);
  text-shadow: 0 0 4px rgba(var(--err-rgb), 0.45);
}
.chat-panel.collapsed .chat-toggle {
  border-bottom: 0;
}

/* The chat-panel container (`<div id="chat-body" class="chat-body">` in
   index.html). Do NOT reuse this class for per-message body wrappers
   inside .chat-log .chat-line.msg -- the column flex + padding here
   would cascade into the message span and turn every inline-block
   .fx child (notably .fx-gradient, which then renders 100% width
   with the gradient washing out into invisible pink at the left edge)
   into a full-width flex item. Per-message body text lives inside
   `<span class="chat-msg-body">` instead. */
/* .chat-body is the chat-panel layout container (`<div id="chat-body">`
   in index.html) -- the column-flex that stacks roster / log /
   composer. The per-message text wrapper inside each <li class="msg">
   uses `.chat-msg-body` instead so this display:flex doesn't leak into
   it and turn inline FX wrappers (like .fx-gradient) into full-width
   flex items. Don't re-use `.chat-body` for per-message content. */
.chat-body {
  display: flex;
  flex-direction: column;
  gap: 0.5rem;
  padding: 0.7rem 0.9rem 0.9rem 0.9rem;
}
.chat-panel.collapsed .chat-body {
  display: none;
}

.chat-roster {
  font-size: 0.78rem;
  letter-spacing: 0.06em;
  min-height: 1.2em;
}
.chat-roster .chat-roster-pip {
  display: inline-block;
  margin-right: 0.5em;
  color: var(--cool);
  text-shadow: 0 0 4px rgba(var(--cool-rgb), 0.35);
}
.chat-roster .chat-roster-pip::before {
  content: "\25CF\00a0";
  color: var(--ok);
  text-shadow: 0 0 4px rgba(var(--ok-rgb), 0.5);
}

.chat-log {
  list-style: none;
  margin: 0;
  padding: 0.4rem 0.5rem;
  border: 1px dashed var(--border);
  background: var(--card-inset-bg);
  height: 280px;
  overflow-y: auto;
  /* Pin the x-axis to clip. Without this, `overflow-y: auto` + the default
     `overflow-x: visible` makes the browser compute x to `auto` too, so an
     animated/wide FX message (shake, marquee, a long gradient run) toggles
     a horizontal scrollbar -- which steals height and flickers the vertical
     one in a feedback loop. Clipping x stops both. */
  overflow-x: hidden;
  font-size: 0.92rem;
  line-height: 1.45;
  scrollbar-width: thin;
  scrollbar-color: var(--border-cool) transparent;
}
.chat-log::-webkit-scrollbar { width: 8px; }
.chat-log::-webkit-scrollbar-thumb {
  background: var(--border-cool);
  border-radius: 0;
}
.chat-log .chat-line {
  margin: 0;
  padding: 0.05em 0;
  /* Horizontal-only guard: break-anywhere keeps each line within the
     chat panel width-wise, even on a fullwidth/no-break run. Vertical
     bleed from zalgo combining marks is fine — it adds to the chaos
     and the scrollback already absorbs the extra height. */
  overflow-wrap: anywhere;
  word-break: break-word;
}
.chat-log .chat-line.msg .chat-from {
  color: var(--hot);
  letter-spacing: 0.04em;
  text-shadow: 0 0 4px rgba(var(--hot-rgb), 0.30);
  margin-right: 0.4em;
  overflow-wrap: anywhere;
  word-break: break-word;
}
.chat-log .chat-line.msg .chat-from::before { content: "<"; color: var(--fg-dim); }
.chat-log .chat-line.msg .chat-from::after  { content: ">"; color: var(--fg-dim); }
.chat-log .chat-line.msg .chat-time {
  color: var(--fg-faint);
  font-size: 0.72rem;
  letter-spacing: 0.06em;
  margin-right: 0.4em;
}
.chat-log .chat-line.msg .chat-msg-body a {
  color: var(--cool);
  border-bottom: 1px dotted var(--cool);
}
.chat-log .chat-line.msg .chat-msg-body a:hover {
  color: var(--hot);
  border-bottom-color: var(--hot);
  text-shadow: 0 0 6px rgba(var(--hot-rgb), 0.45);
}
.chat-log .chat-line.sys {
  color: var(--fg-dim);
  font-style: italic;
  font-size: 0.8rem;
  letter-spacing: 0.06em;
}
.chat-log .chat-line.sys.err { color: var(--err); }

.chat-form {
  display: flex;
  gap: 0.5rem;
  align-items: stretch;
}
.chat-form input[type=text] {
  flex: 1;
  font-size: 0.95rem;
}
.chat-form button {
  font-size: 0.85rem;
  padding: 0.4em 1em;
  letter-spacing: 0.1em;
}
.chat-status {
  min-height: 1.1em;
  font-size: 0.78rem;
  letter-spacing: 0.06em;
}
.chat-status.ok  { color: var(--ok); }
.chat-status.err { color: var(--err); }

/* Tokenless gate: replaces the body with a single CTA line. */
.chat-body.gate {
  text-align: center;
  font-size: 0.9rem;
  color: var(--fg-dim);
  padding: 1rem 0.9rem;
}
.chat-body.gate a {
  color: var(--cool);
  border-bottom: 1px dotted var(--cool);
}

/* === Notifications: top-right bell, badge, dropdown ===
   Mounted as floating siblings of <main> by notifications.js so we
   don't have to touch any page markup. The bell sits above the
   scanlines/vignette overlays (z-index 49/50) so it's always tappable.
*/
.notif-bell {
  position: fixed;
  top: 0.75rem;
  right: 0.75rem;
  z-index: 60;
  width: 44px;
  height: 44px;
  padding: 4px;
  border: 1px solid var(--border-cool);
  background: rgba(0, 0, 0, 0.55);
  text-transform: none;
  letter-spacing: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  cursor: pointer;
  box-shadow:
    inset 0 0 12px rgba(var(--cool-rgb), 0.10),
    0 0 8px rgba(var(--cool-rgb), 0.10);
  transition: border-color 0.15s, box-shadow 0.15s, background 0.15s;
}
.notif-bell:hover,
.notif-bell.open {
  border-color: var(--border-hot);
  background: rgba(var(--hot-rgb), 0.06);
  box-shadow:
    inset 0 0 14px rgba(var(--hot-rgb), 0.18),
    0 0 14px rgba(var(--hot-rgb), 0.30);
  text-shadow: none;
}
.notif-bell.has-unread {
  border-color: var(--border-hot);
  box-shadow:
    inset 0 0 14px rgba(var(--hot-rgb), 0.16),
    0 0 14px rgba(var(--hot-rgb), 0.25);
}

.notif-bell-icon {
  display: block;
  width: 32px;
  height: 32px;
  /* Pixel art shouldn't be smoothed; preserve the chunky CRT edges. */
  image-rendering: pixelated;
  image-rendering: crisp-edges;
  pointer-events: none;
}

/* The unread count. Sits as a small hot-pink chip in the top-right of
   the bell. Hidden when count is 0 via the `hidden` HTML attribute. */
.notif-badge {
  position: absolute;
  top: -6px;
  right: -6px;
  min-width: 18px;
  height: 18px;
  padding: 0 4px;
  border-radius: 9px;
  background: var(--hot);
  color: var(--bg-deeper);
  font-family: var(--font-display);
  font-size: 0.85rem;
  line-height: 18px;
  text-align: center;
  letter-spacing: 0;
  box-shadow:
    0 0 6px rgba(var(--hot-rgb), 0.7),
    inset 0 0 4px rgba(255, 255, 255, 0.18);
  pointer-events: none;
  user-select: none;
}

/* Brief glow pulse on the bell when a new notif arrives over WS. */
.notif-bell.pulse {
  animation: notif-pulse 0.8s ease-out 1;
}
@keyframes notif-pulse {
  0%   { box-shadow: 0 0 0 0 rgba(var(--hot-rgb), 0.55); }
  60%  { box-shadow: 0 0 0 12px rgba(var(--hot-rgb), 0); }
  100% { box-shadow: 0 0 0 0 rgba(var(--hot-rgb), 0); }
}

/* The dropdown drops out from under the bell. Sized to fit ~5
   notifications in the visible area; longer lists scroll. */
.notif-dropdown {
  position: fixed;
  top: 4.25rem;
  right: 0.75rem;
  z-index: 60;
  width: min(380px, calc(100vw - 1.5rem));
  max-height: min(70vh, 540px);
  border: 1px solid var(--border-cool);
  background: linear-gradient(180deg, rgba(6, 0, 10, 0.95), rgba(6, 0, 10, 0.85));
  box-shadow:
    inset 0 0 14px rgba(var(--cool-rgb), 0.10),
    0 0 18px rgba(var(--hot-rgb), 0.20);
  display: flex;
  flex-direction: column;
}
.notif-dropdown[hidden] { display: none; }

.notif-dropdown-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 0.5rem 0.7rem;
  border-bottom: 1px dashed var(--border);
  background: rgba(var(--cool-rgb), 0.04);
}
.notif-dropdown-title {
  font-family: var(--font-display);
  font-size: 1.1rem;
  color: var(--cool);
  letter-spacing: 0.08em;
  text-shadow: 0 0 6px rgba(var(--cool-rgb), 0.35);
}
.notif-mark-all {
  font-size: 0.72rem;
  padding: 0.2em 0.6em;
  letter-spacing: 0.08em;
}

.notif-list {
  list-style: none;
  margin: 0;
  padding: 0;
  overflow-y: auto;
  scrollbar-width: thin;
  scrollbar-color: var(--border-cool) transparent;
}
.notif-list::-webkit-scrollbar { width: 8px; }
.notif-list::-webkit-scrollbar-thumb {
  background: var(--border-cool);
  border-radius: 0;
}

.notif-item {
  border-bottom: 1px dashed var(--border);
}
.notif-item:last-child { border-bottom: 0; }

/* Whole row is the link (when there's somewhere to navigate to);
   styling lives on the inner anchor so the focus ring is square and
   the row hover reads uniform. */
.notif-link {
  display: block;
  padding: 0.55rem 0.75rem 0.6rem 0.75rem;
  border-bottom: 0;
  color: var(--fg);
  transition: background 0.15s, color 0.15s, text-shadow 0.15s;
}
.notif-link:hover {
  background: rgba(var(--hot-rgb), 0.06);
  color: var(--fg);
  border-bottom: 0;
  text-shadow: none;
}
.notif-item.unread .notif-link {
  /* Faint pink left rail signals "new since you last opened this". */
  border-left: 2px solid var(--hot);
  padding-left: calc(0.75rem - 2px);
  background: rgba(var(--hot-rgb), 0.04);
}
.notif-item.read .notif-link {
  opacity: 0.7;
}

.notif-row {
  font-size: 0.92rem;
  /* Horizontal-only guard: a zalgo'd post title or fullwidth comment
     snippet would otherwise push the dropdown past the viewport on a
     phone. Vertical bleed from combining marks is intentional. */
  overflow-wrap: anywhere;
  word-break: break-word;
}
.notif-actor {
  color: var(--hot);
  letter-spacing: 0.04em;
  text-shadow: 0 0 4px rgba(var(--hot-rgb), 0.30);
  overflow-wrap: anywhere;
  word-break: break-word;
}
.notif-verb {
  color: var(--fg-dim);
}
.notif-target {
  color: var(--cool);
  text-shadow: 0 0 4px rgba(var(--cool-rgb), 0.30);
  overflow-wrap: anywhere;
  word-break: break-word;
}
.notif-snippet {
  margin-top: 0.2rem;
  font-size: 0.85rem;
  color: var(--fg-dim);
  border-left: 1px dashed var(--border);
  padding-left: 0.5em;
  overflow-wrap: anywhere;
  word-break: break-word;
}
.notif-meta {
  margin-top: 0.25rem;
  font-size: 0.72rem;
  letter-spacing: 0.06em;
}

.notif-empty {
  padding: 1.4rem 0.8rem;
  text-align: center;
  letter-spacing: 0.1em;
}

/* Comment flash highlight when a notification deep-link scrolls to it. */
.comment.flash {
  animation: comment-flash 1.6s ease-out 1;
}
@keyframes comment-flash {
  0%   { box-shadow: 0 0 0 0 rgba(var(--cool-rgb), 0.55), inset 0 0 18px rgba(var(--cool-rgb), 0.18); }
  100% { box-shadow: 0 0 0 0 rgba(var(--cool-rgb), 0),    inset 0 0 0 rgba(var(--cool-rgb), 0); }
}

@media (max-width: 600px) {
  .notif-bell {
    top: 0.5rem;
    right: 0.5rem;
    width: 40px;
    height: 40px;
  }
  .notif-bell-icon { width: 28px; height: 28px; }
  .notif-dropdown {
    top: 3.6rem;
    right: 0.5rem;
    left: 0.5rem;
    width: auto;
  }
}

/* === CRT dialog (replaces native confirm/alert) ===
   The backdrop sits above EVERYTHING — scanlines (50), vignette (49),
   notifications bell+dropdown (60), lightbox (200) — so a delete
   confirmation can't get hidden behind any of them. */
.os-dialog-backdrop {
  position: fixed;
  inset: 0;
  z-index: 300;
  background: rgba(6, 0, 10, 0.78);
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 1rem;
  /* Subtle blur softens whatever's behind the modal so the eye lands
     on the card; falls back to the dim backdrop on engines without
     backdrop-filter support. */
  backdrop-filter: blur(2px);
  -webkit-backdrop-filter: blur(2px);
  animation: os-dialog-backdrop-in 0.16s ease-out;
}
@keyframes os-dialog-backdrop-in {
  from { opacity: 0; }
  to   { opacity: 1; }
}

.os-dialog {
  position: relative;
  max-width: min(100%, 480px);
  width: 100%;
  border: 1px solid var(--border-cool);
  background: linear-gradient(180deg, rgba(6, 0, 10, 0.98), rgba(6, 0, 10, 0.92));
  padding: 1.1rem 1.2rem 1.1rem 1.2rem;
  box-shadow:
    inset 0 0 16px rgba(var(--cool-rgb), 0.10),
    0 0 24px rgba(var(--hot-rgb), 0.30);
  animation: os-dialog-in 0.18s ease-out;
}
@keyframes os-dialog-in {
  from { opacity: 0; transform: scale(0.96); }
  to   { opacity: 1; transform: scale(1); }
}

/* Same hot-pink corner-tick treatment as .post / .chat-panel so the
   modal feels like part of the same console UI. */
.os-dialog::before, .os-dialog::after {
  content: "";
  position: absolute;
  display: var(--corner-tick-display);
  width: 12px; height: 12px;
  border: 1px solid var(--border-hot);
  pointer-events: none;
}
.os-dialog::before { top: -1px; left: -1px; border-right: none; border-bottom: none; }
.os-dialog::after  { bottom: -1px; right: -1px; border-left: none; border-top: none; }

/* The danger variant flares red so destructive prompts read as serious.
   We still keep the hot-pink corner ticks so it's recognisably an
   overshare card; only the box shadow + title colour shift. */
.os-dialog.danger {
  border-color: rgba(var(--err-rgb), 0.5);
  box-shadow:
    inset 0 0 18px rgba(var(--err-rgb), 0.16),
    0 0 28px rgba(var(--hot-rgb), 0.36);
}

.os-dialog-title {
  font-family: var(--font-display);
  font-size: 1.4rem;
  color: var(--cool);
  letter-spacing: 0.08em;
  text-shadow: 0 0 8px rgba(var(--cool-rgb), 0.35);
  margin: 0 0 0.6rem 0;
  overflow-wrap: anywhere;
  word-break: break-word;
}
.os-dialog.danger .os-dialog-title {
  color: var(--err);
  text-shadow: 0 0 8px rgba(var(--err-rgb), 0.45);
}

.os-dialog-message {
  color: var(--fg);
  margin-bottom: 1rem;
  line-height: 1.5;
  /* Preserve newlines from JS without needing to insert <br>s,
     and break weird-unicode runs the same way the post body does. */
  white-space: pre-wrap;
  overflow-wrap: anywhere;
  word-break: break-word;
}

.os-dialog-actions {
  display: flex;
  justify-content: flex-end;
  gap: 0.6rem;
  flex-wrap: wrap;
}
.os-dialog-actions button {
  min-width: 96px;
  font-size: 0.9rem;
}

@media (max-width: 600px) {
  .os-dialog-backdrop { padding: 0.6rem; }
  .os-dialog { padding: 0.9rem 1rem 0.95rem 1rem; }
  .os-dialog-title { font-size: 1.2rem; }
  .os-dialog-actions button { min-width: 88px; }
}

/* === Rooms ===
   The rooms index + per-room page share a colour-accent system: each
   room has a deterministic hue derived from its slug (see
   backend/slugs.py :: hue_for_slug) and the card / header uses
   `--room-accent` for the left edge + glow. Cards fall back to a
   neutral border when the JS hasn't set the variable yet so the
   first paint isn't naked. */
:root {
  --room-accent: var(--cool);
  --room-accent-soft: rgba(var(--cool-rgb), 0.18);
}

/* Toolbar (sort + search + new-room button) above the grid. */
.rooms-toolbar {
  display: flex;
  flex-wrap: wrap;
  gap: 0.6rem 1rem;
  align-items: center;
  justify-content: space-between;
  padding: 0.4rem 0 1rem 0;
  margin-bottom: 0.4rem;
  border-bottom: 1px dashed var(--border);
}
.rooms-toolbar-left,
.rooms-toolbar-right {
  display: flex;
  gap: 0.5rem;
  align-items: center;
  flex-wrap: wrap;
}
.rooms-toolbar input[type=text],
.rooms-toolbar select {
  font-family: var(--font-body);
  background: rgba(var(--fg-rgb), 0.03);
  color: var(--fg);
  border: 1px solid var(--border);
  padding: 0.4em 0.6em;
  min-width: 12rem;
}
.rooms-toolbar input[type=text]:focus {
  border-color: var(--cool);
  outline: none;
}

/* CRT-themed <select>. Pulled out of `.rooms-toolbar` so the
   per-room sort dropdown (`#room-sort`) gets the same look without
   needing the toolbar wrapper. Both the rooms-index sort and the
   in-room sort use the `.rooms-sort` class. */
select.rooms-sort {
  font-family: var(--font-body);
  background: rgba(var(--fg-rgb), 0.03);
  color: var(--fg);
  border: 1px solid var(--border);
  padding: 0.4em 0.6em;
  min-width: 9rem;
  cursor: pointer;
  /* Suppress the native chrome (rounded box + colored arrow) so the
     control matches the rest of the dashed-CRT inputs. We paint our
     own arrow with an inline SVG background so we don't ship a tiny
     asset just for this. */
  appearance: none;
  -webkit-appearance: none;
  -moz-appearance: none;
  padding-right: 1.8rem;
  background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 7'><path d='M1 1 L6 6 L11 1' fill='none' stroke='%2300e5ff' stroke-width='1.6' stroke-linecap='square'/></svg>");
  background-repeat: no-repeat;
  background-position: right 0.6rem center;
  background-size: 0.7rem auto;
}
select.rooms-sort:focus {
  border-color: var(--cool);
  outline: none;
}
/* The native popup menu is OS-rendered, so most CSS doesn't reach it
   — but `background-color` + `color` on the <option> ARE respected
   in Chromium and Firefox on Win/Linux, which is the realistic
   deploy target here. Safari/macOS will fall back to its system
   styling; the text remains readable either way. */
select.rooms-sort option {
  background: var(--bg-deeper);
  color: var(--fg);
  font-family: var(--font-body);
}
select.rooms-sort option:checked,
select.rooms-sort option:hover {
  background: var(--hot);
  color: var(--bg-deeper);
}

/* In-room sort toolbar — sits above the post list, after the pinned
   card (if any). Single row, label + select, with a subtle divider
   so it reads as a feed-level control rather than another post. */
.room-feed-toolbar {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  padding: 0.4rem 0 0.8rem 0;
  margin-bottom: 0.6rem;
  border-bottom: 1px dashed var(--border);
}
.rooms-toolbar button.primary {
  color: var(--hot);
  border-color: var(--border-hot);
  background: rgba(var(--hot-rgb), 0.04);
}
.rooms-toolbar button.primary:hover {
  color: var(--hot);
  border-color: var(--hot);
  background: rgba(var(--hot-rgb), 0.1);
  text-shadow: 0 0 6px rgba(var(--hot-rgb), 0.5);
}

/* "Post to Main Feed" CTA above the feed. Only shown (via JS in
   auth-nav.js) once a valid admin/guest token is present, so guests
   discover that posting is open to them. Mirrors the .primary accent
   look so it themes correctly across all palettes. */
.feed-compose {
  display: flex;
  justify-content: center;
  margin: 0 0 1rem 0;
}
.feed-compose-btn {
  display: inline-block;
  font-family: var(--font-body);
  font-size: 0.95rem;
  text-decoration: none;
  padding: 0.5em 1.1em;
  border: 1px solid var(--border-hot);
  border-radius: var(--button-radius);
  color: var(--hot);
  background: rgba(var(--hot-rgb), 0.04);
  text-transform: var(--button-text-transform);
  letter-spacing: var(--button-letter-spacing);
  transition: color 0.15s, border-color 0.15s, background 0.15s, text-shadow 0.15s;
}
.feed-compose-btn:hover {
  border-color: var(--hot);
  background: rgba(var(--hot-rgb), 0.1);
  text-shadow: 0 0 6px rgba(var(--hot-rgb), 0.5);
}

/* Grid of cards. One column on phones, two from ~720px so the cards
   read like channels in a tuner. */
.rooms-grid {
  display: grid;
  grid-template-columns: 1fr;
  gap: 1.1rem;
  min-width: 0;
}
@media (min-width: 720px) {
  .rooms-grid { grid-template-columns: repeat(2, 1fr); }
}

.room-card {
  position: relative;
  border: 1px dashed var(--border);
  border-left: 3px solid var(--room-accent);
  background:
    linear-gradient(180deg, var(--room-accent-soft), rgba(0,0,0,0.15)),
    rgba(0, 0, 0, 0.35);
  padding: 0.9rem 1rem 0.9rem 1rem;
  display: flex;
  flex-direction: column;
  gap: 0.65rem;
  min-width: 0;
  transition: border-color 0.2s, box-shadow 0.2s;
}
.room-card:hover {
  border-color: var(--fg-dim);
  box-shadow: 0 0 18px var(--room-accent-soft);
}

/* The whole upper region is one big link to /rooms/<slug>. Keep the
   text underline-free so it doesn't compete with the dashed border. */
.room-card-main {
  display: flex;
  gap: 0.85rem;
  align-items: flex-start;
  border-bottom: none;
  color: inherit;
  min-width: 0;
}
.room-card-main:hover { border-bottom: none; }
.room-card-main:hover .room-card-name {
  color: var(--room-accent);
  text-shadow: 0 0 8px var(--room-accent-soft);
}

.room-card-icon {
  flex: 0 0 auto;
  width: 64px;
  height: 64px;
  object-fit: cover;
  border: 1px solid var(--room-accent);
  background: #000;
  box-shadow: 0 0 10px var(--room-accent-soft);
}
.room-card-icon-placeholder {
  display: flex;
  align-items: center;
  justify-content: center;
  font-family: var(--font-display);
  font-size: 1.6rem;
  color: var(--room-accent);
  letter-spacing: 0.05em;
}

.room-card-text {
  flex: 1 1 auto;
  min-width: 0;
}
.room-card-name {
  font-family: var(--font-display);
  font-size: 1.5rem;
  color: var(--fg);
  margin: 0 0 0.25rem 0;
  overflow-wrap: anywhere;
  word-break: break-word;
  transition: color 0.15s, text-shadow 0.15s;
}
.room-card-meta {
  margin-bottom: 0.25rem;
  overflow-wrap: anywhere;
  word-break: break-word;
}
.room-card-meta code {
  font-family: var(--font-body);
  font-size: 0.85em;
  color: var(--fg-dim);
  background: rgba(255, 255, 255, 0.02);
  padding: 0 0.2em;
  border: 1px solid var(--border);
}
.room-card-stats { overflow-wrap: anywhere; }

.room-card-actions {
  display: flex;
  align-items: center;
  gap: 0.6rem;
  flex-wrap: wrap;
  padding-top: 0.45rem;
  border-top: 1px dashed var(--border);
}
.room-card-subs { flex: 1 1 auto; }
.room-sub-btn {
  font-size: 0.85rem;
  letter-spacing: 0.05em;
  border-color: var(--room-accent);
  color: var(--room-accent);
}
.room-sub-btn:hover {
  background: var(--room-accent-soft);
  color: var(--room-accent);
  text-shadow: 0 0 6px var(--room-accent-soft);
}
.room-sub-btn.tuned {
  color: var(--ok);
  border-color: rgba(var(--ok-rgb), 0.5);
}
.room-card-delete { margin-left: 0.2rem; }

/* Create-room dialog. Reuses .os-dialog* scaffolding from the global
   dialog component but stacks form labels vertically with breathing room
   and gives the icon preview its own row. */
.room-create-dialog .os-dialog-message {
  display: flex;
  flex-direction: column;
  gap: 0.7rem;
  white-space: normal;
}
.room-create-dialog label {
  display: flex;
  flex-direction: column;
  gap: 0.3rem;
}
.room-create-dialog input[type=text],
.room-create-dialog input[type=file] {
  font-family: var(--font-body);
  background: rgba(var(--fg-rgb), 0.03);
  color: var(--fg);
  border: 1px solid var(--border);
  padding: 0.4em 0.6em;
}
.room-create-dialog input[type=text]:focus {
  border-color: var(--cool);
  outline: none;
}
.room-create-dialog .rc-preview {
  display: flex;
  align-items: center;
  gap: 0.6rem;
  padding: 0.4rem;
  border: 1px solid var(--border);
}
.room-create-dialog .rc-preview-img {
  width: 56px;
  height: 56px;
  object-fit: cover;
  border: 1px solid var(--cool);
}
.rc-msg {
  min-height: 1.2em;
  font-family: var(--font-body);
}

/* === Per-room page === */
.room-header {
  border: 1px dashed var(--border);
  border-left: 3px solid var(--room-accent);
  padding: 1.1rem 1.2rem 1rem 1.2rem;
  margin-bottom: 1.5rem;
  background:
    linear-gradient(180deg, var(--room-accent-soft), rgba(0,0,0,0.15)),
    rgba(0, 0, 0, 0.35);
  display: flex;
  gap: 1.1rem;
  align-items: flex-start;
  flex-wrap: wrap;
}
.room-header-icon {
  width: 96px;
  height: 96px;
  object-fit: cover;
  border: 1px solid var(--room-accent);
  background: #000;
  box-shadow: 0 0 14px var(--room-accent-soft);
  flex: 0 0 auto;
}
.room-header-icon.placeholder {
  display: flex;
  align-items: center;
  justify-content: center;
  font-family: var(--font-display);
  font-size: 2.4rem;
  color: var(--room-accent);
  letter-spacing: 0.06em;
}
.room-header-main {
  flex: 1 1 18rem;
  min-width: 0;
}
.room-header-title {
  font-family: var(--font-display);
  font-size: 2rem;
  color: var(--fg);
  margin: 0 0 0.2rem 0;
  text-shadow: 0 0 8px var(--room-accent-soft);
  overflow-wrap: anywhere;
}
.room-header-meta { color: var(--fg-dim); }
.room-header-meta code {
  font-family: var(--font-body);
  font-size: 0.85em;
  color: var(--fg-dim);
  background: rgba(255, 255, 255, 0.02);
  padding: 0 0.2em;
  border: 1px solid var(--border);
}
.room-header-stats { color: var(--fg-dim); margin-top: 0.4rem; }
.room-header-actions {
  margin-top: 0.7rem;
  display: flex;
  gap: 0.5rem;
  flex-wrap: wrap;
}

/* Inline composer on a room page. Hidden by default; the writer
   opens it from a "// new transmission" toggle button in the room
   header so the page leads with the pinned post + feed rather than
   a tall empty textarea. Lighter chrome than admin.html's composer
   card so it docks under the header without stealing the eye. */
.room-composer {
  border: 1px dashed var(--border);
  padding: 0.9rem 1rem 0.9rem 1rem;
  margin-bottom: 1.4rem;
  background: rgba(0, 0, 0, 0.25);
}
.room-composer-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 0.5rem;
  margin-bottom: 0.5rem;
}
.room-composer h3 {
  font-family: var(--font-display);
  font-size: 1.2rem;
  color: var(--fg-dim);
  margin: 0;
  letter-spacing: 0.05em;
}
.room-composer .rc-close-btn { flex: 0 0 auto; }
.room-composer textarea { min-height: 110px; }
.room-composer .composer-actions {
  display: flex;
  gap: 0.5rem;
  align-items: center;
  margin-top: 0.5rem;
  flex-wrap: wrap;
}

/* Header-level "// new transmission" toggle. Picks up the room
   accent so it visually anchors to this specific room — clicking it
   reveals the composer below. The data-open attribute flips on
   click so we can re-style without rebuilding the element. */
.room-compose-toggle {
  border-color: var(--room-accent);
  color: var(--room-accent);
  letter-spacing: 0.05em;
}
.room-compose-toggle:hover {
  background: var(--room-accent-soft);
  color: var(--room-accent);
  text-shadow: 0 0 6px var(--room-accent-soft);
}
.room-compose-toggle[data-open="true"] {
  background: var(--room-accent-soft);
  color: var(--fg);
}

/* Pinned post: same `.post` skeleton plus a leading badge so the
   reader knows it isn't chronologically the newest one. */
.pinned-wrap { margin-bottom: 1.2rem; }
.pinned-badge {
  display: inline-block;
  font-family: var(--font-display);
  font-size: 0.95rem;
  color: var(--room-accent);
  letter-spacing: 0.08em;
  margin-bottom: 0.4rem;
  text-shadow: 0 0 6px var(--room-accent-soft);
}
.post .pin-toggle-btn { margin-left: auto; }

/* The post.html chip that announces a post's room scope. Small, sits
   in the meta row next to the timestamp. */
.post-room-chip {
  display: inline-block;
  font-size: 0.78rem;
  letter-spacing: 0.05em;
  color: var(--room-accent);
  border: 1px solid var(--room-accent);
  background: var(--room-accent-soft);
  padding: 0 0.4em;
  margin-left: 0.4em;
  border-bottom: none;
}
.post-room-chip:hover {
  color: var(--room-accent);
  text-shadow: 0 0 6px var(--room-accent-soft);
  border-bottom: 1px solid var(--room-accent);
}
