/* Danmakus' own styles, alongside the Twind utilities the templates use.
 *
 * Twind's runtime sheet is injected AFTER this file, so a rule that ties with a
 * Twind utility on specificity loses. Rules that must win carry a `body` prefix
 * to break the tie; elements styled here otherwise avoid utility classes.
 */

/* ---------------------------------------------------------------------------
 * Type
 *
 * Outfit, self-hosted, latin subset only (32 KB); the unicode-range falls
 * through to the system face for 中文. It's here for the numbers — Outfit ships
 * `tnum`, so tabular-nums genuinely aligns the money/#id/timestamp columns.
 * ------------------------------------------------------------------------- */
@font-face {
  font-family: 'Outfit';
  font-style: normal;
  font-weight: 400 700;
  font-display: swap;
  src: url('/static/outfit-latin.woff2?v=1787616000000') format('woff2');
  unicode-range:
    U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329,
    U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}

/* `body`, not `html`: Twind's preflight sets font-family on html, and an
   element's own rule beats an inherited value regardless of sheet order. */
body {
  font-family:
    'Outfit',
    -apple-system,
    BlinkMacSystemFont,
    'Segoe UI',
    'PingFang SC',
    'Hiragino Sans GB',
    'Microsoft YaHei',
    sans-serif;
}

/* Column numbers only: tabular figures are too stiff for running prose. */
.danmakus-price,
.dm-time,
.dm-id,
.dm-num,
.dm-page-input,
.dm-price-input,
#unseen-count {
  font-variant-numeric: tabular-nums;
}

/* ---------------------------------------------------------------------------
 * Tokens
 * ------------------------------------------------------------------------- */
:root {
  --dm-ink: #374151;
  --dm-ink-strong: #1f2937;
  --dm-ink-soft: #6b7280;
  --dm-ink-faint: #9ca3af;
  --dm-line: #e5e7eb;
  --dm-line-soft: #f3f4f6;

  /* Brand pink at 97% lightness. The headroom is what keeps the price-tier
     pastels reading as distinct tints on top of it. */
  --dm-surface: #fdf2f8;
  /* Darkened surface hue, not black at low alpha, so the card reads as lit by
     the page. Derived, so it tracks --dm-surface. */
  --dm-shadow: color-mix(in srgb, #000 78%, var(--dm-surface));

  /* The one interactive accent; all other chrome is neutral. */
  --dm-accent: #1d4ed8;
  --dm-accent-hover: #1e3a8a;

  /* Not the link blue: #2563eb is 83% saturation and shouts against pink. Same
     hue family, desaturated. 6.4:1 on white. */
  --dm-focus: #4a5d8f;
  --dm-focus-glow: color-mix(in srgb, var(--dm-focus) 18%, transparent);

  /* The brand pink, deep enough to set type in. No status hue can do this job:
     blue is the link, and amber or red over a tint reads as an alert. */
  --dm-brand: #831843;
  --dm-brand-line: color-mix(in srgb, var(--dm-brand) 24%, #fff);

  --dm-danger: #b91c1c;
  --dm-danger-bg: #fef2f2;
  --dm-danger-line: #fecaca;

  /* "Running", the one state the palette had no colour for. Emerald rather than
     a grass green: it sits with the pink instead of fighting it. 5.5:1 on white. */
  --dm-live: #047857;

  /* Long enough to read as motion, short enough not to lag fast tabbing. */
  --dm-dur: 260ms;
  --dm-ease: cubic-bezier(0.2, 0.8, 0.3, 1);
}

@media (prefers-reduced-motion: reduce) {
  :root {
    --dm-dur: 0ms;
  }
}

/* One flat tint, not a gradient: the rows carry meaning through colour, and a
 * fade put the same price tier on a different backdrop down the page. */
body.dm-body {
  min-height: 100dvh;
  background-color: var(--dm-surface);
  color: var(--dm-ink);
}

/* Follows Twind's `.container`: a max-width that STEPS per breakpoint. A flat
   1536px cap widens the page between 1280 and 1536, where it held at 1280. */
.dm-shell {
  width: 100%;
  margin: 0 auto;
  min-height: 100dvh;
  background: rgba(255, 255, 255, 0.9);
  box-shadow: 0 12px 32px -14px color-mix(in srgb, var(--dm-shadow) 22%, transparent);
}
@media (min-width: 640px) {
  .dm-shell {
    max-width: 640px;
  }
}
@media (min-width: 768px) {
  .dm-shell {
    max-width: 768px;
  }
}
/* Not one of Twind's steps. At 768 the toolbar row has 18.8px of slack, so a
   3-digit page number or a storage warning tips it onto a second line; 812
   takes that to 62.8px for the rest of the sub-1024 range. */
@media (min-width: 812px) {
  .dm-shell {
    max-width: 812px;
  }
}
@media (min-width: 1024px) {
  .dm-shell {
    max-width: 1024px;
  }
}
@media (min-width: 1280px) {
  .dm-shell {
    max-width: 1280px;
  }
}
@media (min-width: 1536px) {
  .dm-shell {
    max-width: 1536px;
  }
}

/* Keyboard users get out of the header without tabbing every filter control. */
.dm-skip {
  position: absolute;
  left: 0.5rem;
  top: -3rem;
  z-index: 60;
  background: var(--dm-accent);
  color: #fff;
  padding: 0.5rem 0.875rem;
  border-radius: 0.375rem;
  font-size: 0.875rem;
  text-decoration: none;
  transition: transform var(--dm-dur) var(--dm-ease);
}
.dm-skip:focus {
  transform: translateY(3.5rem);
}

/* ---------------------------------------------------------------------------
 * Loading — a top bar, not a scrim. It decorates the OUTGOING page, which is
 * about to be discarded, so dimming the list buys nothing. Animates transform,
 * not width, to avoid a per-frame relayout.
 * ------------------------------------------------------------------------- */
.dm-progress {
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  height: 2px;
  z-index: 50;
  background: var(--dm-accent);
  transform: scaleX(0);
  transform-origin: 0 50%;
  opacity: 0;
  pointer-events: none;
}
.dm-progress[data-loading='1'] {
  opacity: 1;
  transform: scaleX(0.9);
  transition:
    transform 1.8s cubic-bezier(0.1, 0.6, 0.2, 1),
    opacity 100ms;
}

/* ---------------------------------------------------------------------------
 * Header
 * ------------------------------------------------------------------------- */
.dm-header {
  background: #fff;
  border-bottom: 1px solid var(--dm-line);
  padding: 1rem;
  position: sticky;
  top: 0;
  z-index: 10;
}

.dm-titlebar {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: 0.5rem;
  margin-bottom: 0.75rem;
}

/* Tightened tracking: at default spacing this read as a label, not a masthead. */
.dm-title {
  font-size: 1.5rem;
  font-weight: 700;
  letter-spacing: -0.02em;
  color: var(--dm-ink-strong);
  margin: 0;
  white-space: nowrap;
}

/* Auto-refresh switch. No fill and no border: beside a 1.5rem masthead this has
 * to read as status, not as a third button competing with 礼物模式 and 重置. The
 * dot carries the state, the ping carries the polling.
 * margin-right: auto — .dm-titlebar is space-between, and auto margins eat the
 * free space first, which packs this against the title instead of letting it
 * drift to the centre of the bar. */
.dm-live {
  display: inline-flex;
  align-items: center;
  gap: 0.375rem;
  margin-right: auto;
  /* Optical, not geometric. align-items centres the ink BOX, but "Danmakus" is
     one cap over a run of x-height letters, so the eye reads the word as centred
     on its x-band — 2.1px lower. Centred by the box, this sits visibly high. */
  position: relative;
  top: 1.5px;
  font: inherit;
  font-size: 0.75rem;
  padding: 0.25rem 0.5rem;
  border: 0;
  border-radius: 999px;
  background: transparent;
  color: var(--dm-ink-faint);
  cursor: pointer;
  outline: 2px solid transparent;
  outline-offset: -1px;
  transition:
    background-color var(--dm-dur) var(--dm-ease),
    color var(--dm-dur) var(--dm-ease),
    outline-color var(--dm-dur) var(--dm-ease);
}
.dm-live:hover {
  background: var(--dm-line-soft);
}
.dm-live:focus-visible {
  outline-color: var(--dm-focus);
}
.dm-live[aria-checked='true'] {
  color: var(--dm-live);
}

.dm-live-dot {
  position: relative;
  flex: none;
  width: 7px;
  height: 7px;
  border-radius: 999px;
  background: currentColor;
}
/* Hollow when paused: solid-and-pulsing against ring-and-still stays legible at
   7px without leaning on colour alone. */
.dm-live[aria-checked='false'] .dm-live-dot {
  background: transparent;
  box-shadow: inset 0 0 0 1.5px currentColor;
}
/* Its own layer, so the 7px dot stays crisp while this scales past it. */
.dm-live-dot::after {
  content: '';
  position: absolute;
  inset: 0;
  border-radius: inherit;
  background: currentColor;
  opacity: 0;
}
.dm-live[aria-checked='true'] .dm-live-dot::after {
  animation: dm-ping 1.8s cubic-bezier(0, 0, 0.2, 1) infinite;
}
@keyframes dm-ping {
  0% {
    transform: scale(1);
    opacity: 0.55;
  }
  80%,
  100% {
    transform: scale(2.6);
    opacity: 0;
  }
}
/* An indefinite pulse is the archetype this setting exists for. */
@media (prefers-reduced-motion: reduce) {
  .dm-live[aria-checked='true'] .dm-live-dot::after {
    animation: none;
  }
}

.dm-meta {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.5rem 1rem;
  font-size: 0.875rem;
}
.dm-count {
  color: var(--dm-ink-soft);
  white-space: nowrap;
}

/* ---------------------------------------------------------------------------
 * Toolbar — one hairline container per functional group, buttons plain until
 * hover. No filled "primary": the primary action here is reading the list.
 * ------------------------------------------------------------------------- */
/* Grid, not flex-wrap: flex breaks the line before shrinking anything, which
 * dropped 只看未读 to its own line and stranded it against the right edge. The
 * left zone wraps internally instead; on a phone the right track collapses to
 * zero when #read-controls is display:none. */
.dm-tools {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  /* `start`: once the left zone wraps, centring leaves 只看未读 floating
     between its two lines, level with neither. */
  align-items: start;
  column-gap: 1rem;
  row-gap: 0.5rem;
  font-size: 0.875rem;
}

.dm-tools-main {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.5rem;
  min-width: 0;
}

/* Keeps the two on one line when the row wraps. Same gap as the row itself:
   a tighter one leaves the button with uneven space either side. */
.dm-pair {
  display: inline-flex;
  align-items: center;
  gap: 0.5rem;
}

.dm-group {
  display: inline-flex;
  align-items: center;
  gap: 2px;
  background: #fff;
  border: 1px solid #e6e4e1;
  border-radius: 0.5rem;
  padding: 2px;
}

.dm-btn {
  font: inherit;
  font-size: 0.8125rem;
  line-height: 1.4;
  padding: 0.3125rem 0.625rem;
  border: 0;
  background: transparent;
  color: #3f3f46;
  border-radius: 0.375rem;
  cursor: pointer;
  outline: 2px solid transparent;
  outline-offset: -1px;
  transition:
    background-color var(--dm-dur) var(--dm-ease),
    color var(--dm-dur) var(--dm-ease),
    box-shadow var(--dm-dur) var(--dm-ease),
    outline-color var(--dm-dur) var(--dm-ease);
}
.dm-btn:hover:not(:disabled) {
  background: #f4f4f2;
  color: #18181b;
}
.dm-btn:active:not(:disabled) {
  background: #e9e9e6;
}
.dm-btn:focus-visible {
  outline-color: var(--dm-focus);
}
.dm-btn:disabled {
  opacity: 0.4;
  cursor: not-allowed;
}
.dm-btn--icon {
  display: inline-flex;
  align-items: center;
  justify-content: center;
}
/* Carries its own chrome instead of sitting in a .dm-group, where any emphasis
   reads as the selected half of a toggle. Fill is --dm-surface: the chip is a
   piece of the page's own pink, lifted onto the white card.
   0.4375rem = .dm-group's 2px padding + .dm-btn's 0.3125rem, so a bordered
   button standing alone matches the pills' height. */
.dm-btn--primary,
.dm-btn--primary:hover:not(:disabled) {
  color: var(--dm-brand);
}
.dm-btn--primary {
  padding: 0.4375rem 0.75rem;
  border: 1px solid var(--dm-brand-line);
  border-radius: 0.5rem;
  background: var(--dm-surface);
}
.dm-btn--primary:hover:not(:disabled) {
  background: color-mix(in srgb, var(--dm-brand) 10%, #fff);
}
.dm-btn--primary:active:not(:disabled) {
  background: color-mix(in srgb, var(--dm-brand) 16%, #fff);
}
/* Mode is on. Same 3px halo a focused filter segment grows, in the button's own
   brand pink rather than the focus blue. No inset edge to go with it: the
   segment needs one because it has no border of its own, whereas this button
   already carries --dm-brand-line, and doubling that up reads as pressed. */
.dm-btn--primary[data-on='1'] {
  box-shadow: 0 0 0 3px color-mix(in srgb, var(--dm-brand) 14%, transparent);
}

/* Red is reserved for the one destructive action, and only on hover. */
.dm-btn--danger:hover:not(:disabled) {
  background: var(--dm-danger-bg);
  color: var(--dm-danger);
}

.dm-sep {
  width: 1px;
  align-self: stretch;
  margin: 0.25rem 2px;
  background: #ecebe8;
}

.dm-page-input {
  font: inherit;
  font-size: 0.8125rem;
  width: 3rem;
  text-align: center;
  border: 0;
  background: transparent;
  color: var(--dm-ink-strong);
  padding: 0.3125rem 0;
  border-radius: 0.375rem;
  outline: none;
  -moz-appearance: textfield;
}
.dm-page-input::-webkit-outer-spin-button,
.dm-page-input::-webkit-inner-spin-button {
  -webkit-appearance: none;
  margin: 0;
}

.dm-select {
  font: inherit;
  font-size: 0.8125rem;
  border: 0;
  background: transparent;
  color: var(--dm-ink-strong);
  padding: 0.3125rem 0.5rem;
  border-radius: 0.375rem;
  cursor: pointer;
  outline: none;
}

.dm-label {
  color: var(--dm-ink-soft);
  font-size: 0.8125rem;
  white-space: nowrap;
}

/* Responsive visibility here, not via Twind's `md:hidden`: those utilities are
 * single-class selectors like `.dm-group`, so sheet order alone would decide the
 * winner. `.hidden` is redeclared because emote-filter.js toggles that exact
 * class on #lowinfo-filtered-note. */
.hidden {
  display: none;
}
.dm-mobile-only {
  display: inline-flex;
}
#filter-controls {
  display: none;
}
@media (min-width: 768px) {
  .dm-mobile-only {
    display: none;
  }
  #filter-controls {
    display: block;
  }
}

/* Not wrapped in .dm-group: that's the button-toolbar shell, and a hairline
 * frame would make these read as a control group they aren't. */
.dm-checks {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  flex-wrap: wrap;
  white-space: nowrap;
}
/* No leading separator: once the row wraps it lands at the start of a line and
   divides nothing, and its 15px pushed the cluster out of line with the buttons
   above. The bordered .dm-group boxes already separate the two. */
.dm-checks label {
  display: inline-flex;
  align-items: center;
  gap: 0.25rem;
  cursor: pointer;
  font-size: 0.8125rem;
}
.dm-checks input[type='checkbox'],
.danmakus-read-toggle,
#read-controls input[type='checkbox'] {
  accent-color: var(--dm-focus);
}
.dm-checks input[type='checkbox']:focus-visible,
#read-controls input[type='checkbox']:focus-visible {
  outline: 2px solid var(--dm-focus);
  outline-offset: 2px;
}

/* ---------------------------------------------------------------------------
 * Filter panel — two segmented bars: query on the left, refinements on the
 * right. Neither absorbs surplus width; a keyword field given `1fr` becomes a
 * 1400px box for a two-character query, so the surplus stays between them.
 *
 * Grid, not flex-wrap, for the reason given at .dm-tools. Dividers are the
 * grid's own 1px gaps, so no cell needs a border.
 * ------------------------------------------------------------------------- */
#filter-controls {
  container: dmfilters / inline-size;
  margin-bottom: 0.75rem;
}

.dm-filters {
  display: grid;
  grid-template-columns: minmax(0, auto) max-content;
  justify-content: space-between;
  align-items: center;
  gap: 0.75rem 1.5rem;
}

.dm-bar {
  display: grid;
  gap: 1px;
  background: #e7e5e2;
  border: 1px solid #e0ddd9;
  border-radius: 0.625rem;
  min-width: 0;
}
.dm-bar > * {
  background: #fff;
  display: flex;
  align-items: center;
  min-width: 0;
  transition:
    background-color var(--dm-dur) var(--dm-ease),
    box-shadow var(--dm-dur) var(--dm-ease);
}
/* Edge cells carry their own radii, so the bar needs no overflow clip — that's
   what lets a focused segment cast a glow past its edge. */
.dm-bar > *:first-child {
  border-radius: 9px 0 0 9px;
}
.dm-bar > *:last-child {
  border-radius: 0 9px 9px 0;
}

.dm-bar input,
.dm-bar select,
.dm-bar button {
  font: inherit;
  font-size: 0.875rem;
  border: 0;
  background: transparent;
  padding: 0.5625rem 0.75rem;
  width: 100%;
  min-width: 0;
  color: var(--dm-ink);
  outline: none;
  border-radius: inherit;
}
.dm-bar select,
.dm-bar button {
  cursor: pointer;
}
/* The bar's only control that acts rather than sets, so it's the only one that
   needs a hover affordance. */
.dm-bar button:hover {
  background: #faf9f8;
  color: var(--dm-ink-strong);
}

.dm-query {
  grid-template-columns: minmax(9rem, 16rem) minmax(8rem, 12rem) auto;
}
.dm-refine {
  grid-template-columns: auto auto auto;
}

.dm-seg-search {
  padding-left: 0.75rem;
  gap: 0.375rem;
  color: var(--dm-ink-faint);
}
.dm-seg-search input {
  padding-left: 0;
}
.dm-seg-search svg {
  flex-shrink: 0;
}

/* One time range, so one segment joined by an arrow — not two controls. */
.dm-seg-range {
  padding-right: 0.25rem;
}
.dm-seg-range input {
  width: auto;
  flex: 0 1 auto;
}
.dm-seg-range .dm-arrow {
  color: var(--dm-ink-faint);
  font-size: 0.8125rem;
  flex-shrink: 0;
}

.dm-seg-price {
  padding-right: 0.75rem;
  font-size: 0.875rem;
  white-space: nowrap;
  color: var(--dm-ink);
}
.dm-seg-price > span:first-child {
  padding-left: 0.75rem;
}
/* Scoped to out-specify `.dm-bar input` (0,1,1), which sets width:100%; a bare
 * `.dm-price-input` is (0,1,0) and loses, stretching the refinement bar wide. */
.dm-seg-price .dm-price-input {
  width: 4.25rem;
  flex: 0 0 auto;
  text-align: center;
  padding-left: 0.25rem;
  padding-right: 0.25rem;
}

/* Let the text fields give up more before anything stacks. */
@container dmfilters (max-width: 1340px) {
  .dm-query {
    grid-template-columns: minmax(7rem, 1fr) minmax(7rem, 1fr) auto;
  }
}
/* Once both bars no longer fit a line, stack them left-aligned: a wrapped
   right-aligned bar is the stranded orphan again. */
@container dmfilters (max-width: 1120px) {
  .dm-filters {
    grid-template-columns: minmax(0, 1fr);
  }
  .dm-refine {
    justify-self: start;
  }
}
@container dmfilters (max-width: 720px) {
  .dm-query,
  .dm-refine {
    grid-template-columns: 1fr;
  }
  .dm-bar > *:first-child {
    border-radius: 9px 9px 0 0;
  }
  .dm-bar > *:last-child {
    border-radius: 0 0 9px 9px;
  }
}

/* ---------------------------------------------------------------------------
 * Focus. Per Selectors L4 a text input always matches :focus-visible while
 * focused, even on a mouse click, so the question is what it looks like, not
 * whether. The z-index is required: without it the glow paints under the
 * adjacent cells. One indicator per control, never a ring plus a highlight.
 * ------------------------------------------------------------------------- */
.dm-bar > *:focus-within {
  position: relative;
  z-index: 1;
  border-radius: 9px;
  box-shadow:
    inset 0 0 0 1px var(--dm-focus),
    0 0 0 3px var(--dm-focus-glow);
}

/* ---------------------------------------------------------------------------
 * Rows
 * ------------------------------------------------------------------------- */
.dm-list {
  background: transparent;
}
/* 1rem left padding on EVERY row: the extra 0.25rem is the gutter the 4px tier
   rail sits in, and applying it only to priced rows shifts their avatars 4px. */
body [data-message-row] {
  display: flex;
  align-items: flex-start;
  gap: 0.75rem;
  padding: 0.75rem 0.75rem 0.75rem 1rem;
  transition: background-color var(--dm-dur) var(--dm-ease);
}
.dm-list > [data-message-row] + [data-message-row] {
  border-top: 1px solid var(--dm-line-soft);
}
body [data-message-row]:not([data-price-tier]):hover {
  background-color: rgba(255, 255, 255, 0.45);
}

/* The fill is a per-username hue from avatarColor(); seen only without an image. */
.dm-avatar {
  width: 2rem;
  height: 2rem;
  border-radius: 9999px;
  overflow: hidden;
  flex-shrink: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  color: #fff;
  font-size: 0.875rem;
  font-weight: 600;
  line-height: 1;
}
.dm-avatar img {
  width: 100%;
  height: 100%;
  object-fit: cover;
}

.dm-body {
  flex: 1 1 0%;
  min-width: 0;
}

.dm-rowmeta {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  margin-bottom: 0.25rem;
  flex-wrap: wrap;
}

.dm-name {
  font-weight: 600;
  color: var(--dm-accent);
  text-decoration: none;
  max-width: 12rem;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.dm-name:hover {
  color: var(--dm-accent-hover);
  text-decoration: underline;
}
.dm-name--plain {
  color: var(--dm-ink-strong);
  font-weight: 500;
}
@media (min-width: 640px) {
  .dm-name {
    max-width: none;
  }
}

.dm-time {
  font-size: 0.75rem;
  color: var(--dm-ink-soft);
  white-space: nowrap;
}

.dm-id {
  font-size: 0.75rem;
  color: var(--dm-ink-faint);
}

/* No type badge: the message's emoji prefix already states the type. `data-type`
   stays on the row — emote-filter.js reads it. */

.dm-text {
  color: var(--dm-ink);
  overflow-wrap: break-word;
  text-wrap: pretty;
  margin: 0;
}
.dm-text a {
  color: var(--dm-accent);
  text-decoration: none;
}
.dm-text a:hover {
  color: var(--dm-accent-hover);
  text-decoration: underline;
}
.dm-emote {
  display: inline-block;
  height: 1.5rem;
  width: auto;
  margin: 0 0.25rem;
  vertical-align: middle;
}
.dm-area {
  color: var(--dm-accent);
}
.dm-suffix {
  color: var(--dm-ink-soft);
}

.danmakus-read-toggle {
  width: 1rem;
  height: 1rem;
  flex-shrink: 0;
  cursor: pointer;
  /* Down onto the username line, off the row's top edge. */
  margin-top: 0.375rem;
}

/* Empty state: a zero-result query used to render nothing, which reads as a
 * broken page. */
.dm-empty {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  text-align: center;
  padding: 4rem 1.5rem;
  gap: 0.375rem;
  background: rgba(255, 255, 255, 0.6);
}
.dm-empty-mark {
  width: 2.75rem;
  height: 2.75rem;
  border-radius: 0.75rem;
  background: #f4f4f2;
  display: flex;
  align-items: center;
  justify-content: center;
  color: var(--dm-ink-soft);
  margin-bottom: 0.375rem;
}
.dm-empty h2 {
  margin: 0;
  font-size: 1rem;
  font-weight: 600;
  color: var(--dm-ink-strong);
}
.dm-empty p {
  margin: 0;
  font-size: 0.875rem;
  color: var(--dm-ink-soft);
  max-width: 38ch;
  line-height: 1.6;
  text-wrap: pretty;
}
.dm-empty .dm-group {
  margin-top: 0.875rem;
}

/* Inline export errors, in place of window.alert(). */
.dm-error {
  display: none;
  font-size: 0.8125rem;
  color: var(--dm-danger);
  background: var(--dm-danger-bg);
  border: 1px solid var(--dm-danger-line);
  border-radius: 0.375rem;
  padding: 0.5rem 0.75rem;
  margin-top: 0.75rem;
  max-width: 52ch;
  line-height: 1.5;
  text-wrap: pretty;
}
.dm-error[data-shown='1'] {
  display: flex;
  align-items: flex-start;
  gap: 0.5rem;
}
.dm-error > span {
  flex: 1 1 auto;
  min-width: 0;
}
/* Dismissible without a navigation. Escape also closes it — see export.js. */
.dm-error-close {
  flex: 0 0 auto;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 1.25rem;
  height: 1.25rem;
  margin-top: 0.0625rem;
  padding: 0;
  border: 0;
  border-radius: 0.25rem;
  background: transparent;
  color: inherit;
  cursor: pointer;
  opacity: 0.65;
  outline: 2px solid transparent;
  transition:
    opacity var(--dm-dur) var(--dm-ease),
    background-color var(--dm-dur) var(--dm-ease),
    outline-color var(--dm-dur) var(--dm-ease);
}
.dm-error-close:hover {
  opacity: 1;
  background: rgba(185, 28, 28, 0.1);
}
.dm-error-close:focus-visible {
  opacity: 1;
  outline-color: var(--dm-danger);
  outline-offset: 1px;
}

.dm-note {
  padding: 1rem;
  text-align: center;
  font-size: 0.875rem;
  color: var(--dm-ink-soft);
  border-top: 1px solid var(--dm-line-soft);
}

/* ---------------------------------------------------------------------------
 * Price tiers — laplace.live's palette, tinted much weaker: a filtered page here
 * is *all* paid events and mostly ¥138/¥168 舰长, so full strength makes a wall
 * of amber a ¥2000 superchat can't stand out against. The rail and pill keep the
 * saturated colour; pill text is white or near-black, whichever clears AA.
 *
 * The rail is a gradient, not a left border: a border shorthand would fight the
 * row divider's border-top.
 * ------------------------------------------------------------------------- */
body [data-message-row][data-price-tier] {
  background-repeat: no-repeat;
}
body [data-message-row][data-price-tier='0'] {
  background-color: #fff;
  background-image: linear-gradient(to right, #d1d5db 4px, transparent 4px);
}
body [data-message-row][data-price-tier='1'] {
  background-color: #edf6ff;
  background-image: linear-gradient(to right, #006fe6 4px, transparent 4px);
}
body [data-message-row][data-price-tier='2'] {
  background-color: #effbf4;
  background-image: linear-gradient(to right, #1db45c 4px, transparent 4px);
}
body [data-message-row][data-price-tier='3'] {
  background-color: #fff8eb;
  background-image: linear-gradient(to right, #eb9c00 4px, transparent 4px);
}
body [data-message-row][data-price-tier='4'] {
  background-color: #ffeada;
  background-image: linear-gradient(to right, #de6502 4px, transparent 4px);
}
body [data-message-row][data-price-tier='5'] {
  background-color: #fbd1d8;
  background-image: linear-gradient(to right, #e9163c 4px, transparent 4px);
}
body [data-message-row][data-price-tier='6'] {
  background-color: #f9c0ca;
  background-image: linear-gradient(to right, #a30f2a 4px, transparent 4px);
}

.danmakus-price {
  display: inline-block;
  padding: 0 0.4em;
  border-radius: 9999px;
  font-weight: 600;
  font-size: 0.9em;
  white-space: nowrap;
}
[data-price-tier='0'] .danmakus-price {
  background-color: #e5e7eb;
  color: #374151;
}
[data-price-tier='1'] .danmakus-price {
  background-color: #006fe6;
  color: #fff;
}
[data-price-tier='2'] .danmakus-price {
  background-color: #1db45c;
  color: #111827;
}
[data-price-tier='3'] .danmakus-price {
  background-color: #eb9c00;
  color: #111827;
}
[data-price-tier='4'] .danmakus-price {
  background-color: #de6502;
  color: #111827;
}
[data-price-tier='5'] .danmakus-price {
  background-color: #e9163c;
  color: #fff;
}
[data-price-tier='6'] .danmakus-price {
  background-color: #a30f2a;
  color: #fff;
}

/* ---------------------------------------------------------------------------
 * Read marks — a white veil, not desaturation, so the price tier stays
 * recognisable. An ::after overlay rather than `opacity`: opacity composites
 * against the page's pink tint, giving read rows a cast instead of lifting them
 * toward white. The veil clears on hover; the checkbox sits above it.
 * ------------------------------------------------------------------------- */
[data-readable] {
  cursor: pointer;
  position: relative;
}
[data-message-row][data-read='1']::after {
  content: '';
  position: absolute;
  inset: 0;
  pointer-events: none;
  background-color: rgba(255, 255, 255, 0.6);
  transition: background-color 0.12s;
}
[data-message-row][data-read='1']:hover::after {
  background-color: rgba(255, 255, 255, 0);
}
[data-message-row][data-read='1'] .danmakus-read-toggle {
  position: relative;
  z-index: 1;
}
body [data-message-row][data-readable]:hover {
  box-shadow: inset 0 0 0 9999px rgba(0, 0, 0, 0.035);
}
[data-message-row][data-read-hidden] {
  display: none;
}

/* ---------------------------------------------------------------------------
 * Header control visibility
 * ------------------------------------------------------------------------- */
/* These fold away with the filter panel on a phone. They can't sit inside
   #filter-controls (that costs a line on desktop), so they track its open state
   via <body>. Only the closed state is declared. */
@media (max-width: 767px) {
  /* #read-controls listed explicitly: its id-level `[data-has-rows]` rule would
     outrank a class selector here and keep showing 只看未读. */
  body[data-filters-open='0'] .danmakus-optional,
  body[data-filters-open='0'] #read-controls {
    display: none;
  }
}

/* Same token as the .dm-count beside it, so the two can't drift apart. */
#read-unread-count {
  color: var(--dm-ink-soft);
  font-size: 0.875rem;
  white-space: nowrap;
}
#read-unread-count[data-unread='0'] {
  color: var(--dm-ink-faint);
}
/* Empty unless marks aren't reaching disk; the detail is in its title. */
#read-storage-warning {
  color: #b45309;
  font-weight: 600;
  font-size: 0.875rem;
  white-space: nowrap;
  cursor: help;
}
/* Empty most of the time, and an empty flex item still claims the gap. */
#read-unread-count:empty,
#read-storage-warning:empty {
  display: none;
}
/* 0.5rem = .dm-group's 1px border + 2px padding + .dm-btn's 0.3125rem, which
   puts this label's text on the button text's line. */
#read-controls {
  display: none;
  align-items: center;
  gap: 0.5rem;
  font-size: 0.8125rem;
  padding-top: 0.5rem;
}
#read-controls[data-has-rows='1'] {
  display: flex;
}
#read-controls label {
  display: inline-flex;
  align-items: center;
  gap: 0.25rem;
  cursor: pointer;
}
/* No auto margin — the grid track places it, and that margin is what stranded
   it when the row wrapped. The negative margin cancels the header padding so
   the checkbox lines up with the per-row toggles. */
#read-controls {
  justify-self: end;
}
@media (min-width: 768px) {
  #read-controls {
    margin-right: -0.25rem;
  }
}

/* Unseen-event count, inside the 刷新 button. A piece of the page's own pink
   lifted onto the button, as .dm-btn--primary does — the count is news, not a
   second action. */
#unseen-count {
  margin-left: 0.375rem;
  padding: 0 0.3125rem;
  border-radius: 999px;
  background: var(--dm-surface);
  color: var(--dm-brand);
  font-size: 0.75rem;
  font-weight: 600;
}
/* Zero width at zero count, its margin included. */
#unseen-count:empty {
  display: none;
}

/* ---------------------------------------------------------------------------
 * 404
 * ------------------------------------------------------------------------- */
.dm-404 {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  text-align: center;
  min-height: 100dvh;
  gap: 0.5rem;
  padding: 2rem;
}
.dm-404 h1 {
  font-size: 3rem;
  font-weight: 700;
  letter-spacing: -0.03em;
  margin: 0;
  color: var(--dm-ink-strong);
}
.dm-404 p {
  margin: 0 0 1rem;
  color: var(--dm-ink-soft);
  font-size: 0.9375rem;
  max-width: 40ch;
  text-wrap: pretty;
}
