* { box-sizing: border-box; margin: 0; padding: 0; }

:root {
  --bg: #0f1117;
  --panel: #181c26;
  --panel-2: #1f2431;
  --border: #2a3040;
  --text: #e6e9f0;
  --muted: #8b93a7;
  --accent: #f0567a;
  --accent-2: #ffb86b;
  --good: #4ade80;
  --gem: #7dd3fc;
  /* Flags. Three levels, and the colour has to carry the difference at a
     glance in a table full of pink accents — so the top level is the only
     red on the page, and "watch" is deliberately quiet: most flags are
     notes, and if every one of them shouted none of them would. */
  --flag-watch: #a5b4fc;
  --flag-warn: #ffb86b;
  --flag-block: #f87171;
}

body {
  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
  background: var(--bg);
  color: var(--text);
  min-height: 100vh;
}

.hidden { display: none !important; }
.muted { color: var(--muted); }
.small { font-size: 0.8rem; font-weight: normal; }
/* icon licence credit — required by Font Awesome Free (CC BY 4.0) */
.attrib { text-align: center; font-size: 0.75rem; color: var(--muted); padding: 1.5rem 0 0.5rem; }
.attrib a { color: var(--muted); }
.attrib a:hover { color: var(--text); }
.error { color: #f87171; margin-top: 0.5rem; }

/* login */
#login-view { display: flex; align-items: center; justify-content: center; min-height: 100vh; }
.login-card {
  background: var(--panel);
  border: 1px solid var(--border);
  border-radius: 12px;
  padding: 2rem;
  width: min(360px, 90vw);
  display: flex; flex-direction: column; gap: 0.9rem;
}
.login-card h1 { font-size: 1.4rem; }

/* header */
header {
  display: flex; align-items: center; gap: 1.5rem;
  padding: 0.9rem 1.5rem;
  background: var(--panel);
  border-bottom: 1px solid var(--border);
  flex-wrap: wrap;
}
header h1 { font-size: 1.15rem; margin-right: auto; }
nav { display: flex; gap: 0.4rem; }

button {
  background: var(--accent);
  color: #fff;
  border: none;
  border-radius: 8px;
  padding: 0.55rem 1rem;
  font-size: 0.9rem;
  cursor: pointer;
}
button:hover { filter: brightness(1.1); }
button.ghost, button.tab {
  background: transparent;
  color: var(--muted);
  border: 1px solid var(--border);
}
button.tab.active { color: var(--text); background: var(--panel-2); border-color: var(--accent); }
button.tiny { padding: 0.25rem 0.6rem; font-size: 0.78rem; }
button.danger { background: transparent; border: 1px solid #7f1d1d; color: #f87171; }

/* live banner */
#live-banner {
  margin: 1rem 1.5rem 0;
  padding: 0.6rem 1rem;
  background: linear-gradient(90deg, rgba(240,86,122,0.18), transparent);
  border: 1px solid var(--accent);
  border-radius: 10px;
  font-size: 0.9rem;
}
#live-banner .dot {
  display: inline-block; width: 8px; height: 8px; border-radius: 50%;
  background: var(--accent); margin-right: 0.5rem;
  animation: pulse 1.4s infinite;
}
@keyframes pulse { 50% { opacity: 0.3; } }
#live-banner.quota {
  background: linear-gradient(90deg, rgba(245,158,11,0.18), transparent);
  border-color: #f59e0b;
}
#live-banner.quota .dot { background: #f59e0b; animation: none; }

/* toasts
   A popover, so it shares the top layer with the #overlay <dialog> instead of
   rendering behind its backdrop. `inset: auto` undoes the centring a popover
   gets by default. */
#toasts {
  position: fixed;
  inset: auto 1rem 1rem auto;
  margin: 0; padding: 0; border: 0; background: none; overflow: visible;
  display: flex; flex-direction: column; gap: 0.5rem;
  width: min(24rem, calc(100vw - 2rem));
}
#toasts:not(:popover-open) { display: none; }
.toast {
  display: flex; align-items: baseline; gap: 0.6rem;
  background: var(--panel-2);
  border: 1px solid var(--border);
  border-left: 3px solid var(--accent);
  border-radius: 10px;
  padding: 0.6rem 0.9rem;
  font-size: 0.88rem;
  box-shadow: 0 6px 20px rgba(0,0,0,0.45);
}
.toast .toast-msg { flex: 1; min-width: 0; overflow-wrap: anywhere; }
.toast.error { border-left-color: #f87171; }
.toast.live { border-left-color: var(--good); }
.toast.gift { border-left-color: var(--accent-2); }
.toast.tracked { border-left-color: var(--muted); }
.toast.flag { border-left-color: var(--flag-block); }
.toast-go { color: var(--accent); text-decoration: none; white-space: nowrap; }
.toast-x {
  background: none; border: 0; color: var(--muted);
  cursor: pointer; font-size: 1.1rem; line-height: 1; padding: 0;
}
.toast-x:hover { color: var(--text); }

/* per-cell filter affordance.
   Hidden until the cell is hovered so it does not clutter a dense table, but
   :focus-visible is not optional — without it the control is unreachable by
   keyboard — and on touch, where nothing ever hovers, it is always faint. */
.cell-filter { opacity: 0; color: var(--muted); text-decoration: none; padding: 0 0.2rem; }
td:hover .cell-filter { opacity: 1; }
.cell-filter:focus-visible { opacity: 1; color: var(--text); }
.cell-filter:hover { color: var(--accent); }
@media (hover: none) { .cell-filter { opacity: 0.55; } }

/* a badge that is also a filter link */
a.badge { text-decoration: none; }
a.badge:hover { border-color: var(--accent); }

/* charts whose points open something */
.chart-group.drillable svg.chart { cursor: pointer; }
.drill-hint { margin: 0 0 0.5rem; }

/* composer — the one control that writes back to TikTok */
.composer { display: flex; gap: 0.5rem; margin-top: 0.75rem; }
.composer input {
  flex: 1; min-width: 0;
  background: var(--panel-2);
  border: 1px solid var(--border);
  border-radius: 8px;
  padding: 0.5rem 0.75rem;
  color: var(--text);
}
.composer input:disabled { opacity: 0.6; }

/* settings page */
.settings-block {
  background: var(--panel);
  border: 1px solid var(--border);
  border-radius: 12px;
  padding: 1rem 1.25rem 1.25rem;
  margin-bottom: 1.25rem;
  max-width: 62rem;
}
.settings-block h3 { margin: 0 0 0.3rem; font-size: 1.05rem; }
.settings-block > p { margin: 0 0 0.9rem; }
.settings-block code {
  background: var(--panel-2); border-radius: 4px; padding: 0.05rem 0.3rem; font-size: 0.85em;
}
.settings-block .checkbox { display: block; margin-bottom: 0.5rem; }
.settings-block .field { display: block; margin-top: 0.75rem; font-size: 0.9rem; }
.settings-block .field input { width: 7rem; margin: 0 0.35rem; }

/* Two columns where there is room, one on a phone. Each setting keeps its
   help text with its input rather than in a legend elsewhere. */
.settings-grid {
  display: grid; gap: 1rem 2rem;
  grid-template-columns: repeat(auto-fit, minmax(19rem, 1fr));
}
.setting { display: block; }
.setting-label { display: block; font-size: 0.92rem; margin-bottom: 0.3rem; }
.setting-input { display: flex; align-items: center; gap: 0.45rem; }
.setting-input input {
  width: 7rem;
  background: var(--panel-2);
  border: 1px solid var(--border);
  border-radius: 8px;
  padding: 0.4rem 0.6rem;
  color: var(--text);
}
.setting-help { display: block; margin-top: 0.35rem; line-height: 1.45; }
/* The env var name, so what the page writes is never a mystery. */
.setting-env {
  display: inline-block; margin-left: 0.35rem;
  font-family: ui-monospace, monospace; font-size: 0.9em; opacity: 0.75;
}
.settings-actions { display: flex; align-items: center; gap: 0.75rem; margin-top: 1.1rem; }

.settings-status {
  display: grid; grid-template-columns: max-content 1fr; gap: 0.35rem 1.25rem;
  margin: 0; font-size: 0.9rem;
}
.settings-status dt { color: var(--muted); }
.settings-status .warn { color: var(--accent-2); font-weight: 600; }
.settings-status .bad { color: #f87171; font-weight: 600; }
.settings-status dd { margin: 0; }

/* alerts panel */
#prefs-panel {
  position: fixed;
  inset: auto 1rem auto auto; top: 3.5rem;
  background: var(--panel-2);
  border: 1px solid var(--border);
  border-radius: 12px;
  padding: 1rem 1.1rem;
  width: min(20rem, calc(100vw - 2rem));
  color: var(--text);
  box-shadow: 0 6px 24px rgba(0,0,0,0.5);
}
#prefs-panel h3 { margin: 0 0 0.2rem; font-size: 1rem; }
#prefs-panel p { margin: 0 0 0.75rem; }
#prefs-panel .checkbox { display: block; margin-bottom: 0.5rem; }
#prefs-panel .field { display: block; margin-top: 0.75rem; font-size: 0.88rem; color: var(--muted); }
#prefs-panel .field input { width: 6rem; margin: 0 0.35rem; }

main { padding: 1.5rem; max-width: 1100px; margin: 0 auto; }
section h2 { font-size: 1.05rem; margin-bottom: 1rem; }

/* tables */
table { width: 100%; border-collapse: collapse; background: var(--panel); border-radius: 10px; overflow: hidden; }
.table-wrap { overflow-x: auto; border: 1px solid var(--border); border-radius: 10px; }
th, td { padding: 0.6rem 0.9rem; text-align: left; font-size: 0.88rem; white-space: nowrap; }
th { background: var(--panel-2); color: var(--muted); font-weight: 600; font-size: 0.78rem; text-transform: uppercase; letter-spacing: 0.04em; }
tr { border-top: 1px solid var(--border); }
tbody tr:hover { background: var(--panel-2); }
tr.clickable { cursor: pointer; }
td.num { text-align: right; font-variant-numeric: tabular-nums; }
th.num { text-align: right; }

.badge {
  display: inline-block; padding: 0.1rem 0.5rem; border-radius: 999px;
  font-size: 0.72rem; border: 1px solid var(--border); color: var(--muted);
}
.badge.mine { border-color: var(--good); color: var(--good); }
.badge.live { border-color: var(--accent); color: var(--accent); }
.badge.elsewhere { border-color: var(--accent-2); color: var(--accent-2); }
/* Verified is a fact about the account, not a status of its own — it reads as
   information rather than competing with the live and tracked badges. */
.badge.verified { border-color: var(--gem); color: var(--gem); }
/* A flag is the one badge that changes what the reader does next, so it is the
   only one that fills rather than outlines. */
.badge.flag { border-color: var(--flag-watch); color: var(--flag-watch); }
.badge.flag-warn { border-color: var(--flag-warn); color: var(--flag-warn); }
.badge.flag-block {
  border-color: var(--flag-block); color: var(--bg); background: var(--flag-block); font-weight: 600;
}
/* Presence, when the person standing in the room is one of them. */
.badge.now.flag { border-color: var(--flag-block); color: var(--flag-block); }
.badge.now .dot {
  display: inline-block; width: 6px; height: 6px; border-radius: 50%;
  background: currentColor; margin-right: 0.35rem;
  animation: pulse 1.4s infinite;
}

/* icons — Font Awesome sprite symbols, see index.html */
.icon {
  width: 1em; height: 1em; fill: currentColor;
  vertical-align: -0.125em; /* sit on the text baseline */
  flex: none;
}
/* keep the colour cues the emoji used to carry */
.icon-gem { color: var(--gem); }
.icon-coins { color: var(--accent-2); }
.icon-gift { color: var(--accent); }
/* inside a badge the badge's own colour wins */
.badge .icon { color: inherit; }

/* avatars */
.avatar {
  display: inline-flex; align-items: center; justify-content: center;
  width: 28px; height: 28px; border-radius: 50%;
  background: var(--panel-2); border: 1px solid var(--border);
  color: var(--muted); font-size: 0.75rem; font-weight: 600;
  vertical-align: middle; margin-right: 0.5rem;
  overflow: hidden; position: relative;
}
.avatar::before { content: attr(data-initial); position: absolute; }
.avatar img { width: 100%; height: 100%; object-fit: cover; position: relative; z-index: 1; }

/* forms */
input {
  background: var(--panel-2);
  border: 1px solid var(--border);
  color: var(--text);
  border-radius: 8px;
  padding: 0.55rem 0.8rem;
  font-size: 0.9rem;
}
input:focus { outline: none; border-color: var(--accent); }
.checkbox { display: flex; align-items: center; gap: 0.4rem; font-size: 0.88rem; color: var(--muted); }
.checkbox input { width: auto; }


/* Airtime bar in the per-day history. Inline width is the value; the minimum
   keeps a day that was on air at all from rendering as nothing. */
.bar {
  display: block; height: 4px; min-width: 2px; margin-bottom: 3px;
  background: var(--accent); border-radius: 2px;
}

/* The same bar under a figure in a numeric cell — the likers tables draw one
   per row for "how big is this against the biggest". A numeric cell is
   right-aligned, and a block element in it would otherwise grow leftward from
   the wrong edge and leave the bar detached from its own number. */
td.num .bar { margin: 2px 0 0 auto; background: var(--series-likes); }

/* --- charts ---
   One measure per chart, so each plot has a single series and needs no legend
   box; the caption's colour key and heading carry the identity. --series-*
   are validated against the panel surface for lightness, chroma, contrast and
   colour-vision separation — they are not the UI accents reused as data ink,
   which is why --gem is not what the diamonds line is drawn in. */
:root {
  --series-likes: #f0567a;
  --series-diamonds: #2b91d8;
  --series-comments: #e8a13c;
  /* A fourth series had to fit a palette already checked all-pairs. This green
     is the best available: its worst pairing under simulated deuteranopia is
     ΔE 10.6, above the 10.3 that likes/comments already binds at, so adding it
     costs the set nothing. It is deliberately not --good (#4ade80), which
     scores the same but is a status accent, and status is not a measure. */
  --series-viewers: #57d68f;

  /* --- diagram palette ---
     Channel identity is NOT carried by hue. Nine categorical hues cannot be
     told apart on this surface — checked all-pairs against the panel colour,
     the worst pair lands at ΔE 1.6 for deuteranopia against a floor of 15 —
     and the flow and network views are all-pairs forms, where any node can end
     up beside any other. So every mark is direct-labelled and hue is spent on
     the two things that really are categorical and small: which way the
     audience drifted, and whether a channel is yours. Both are intrinsic
     properties of the row, so adding or deleting a channel repaints nothing;
     there is no hash and no array index anywhere in the colour choice.
     These three pass all-pairs here: worst CVD ΔE 9.4, worst normal 20.9. */
  --flow-out: #d95926;   /* seen in this channel first */
  --flow-in: #3987e5;    /* seen in the other one first */
  --flow-tied: #199e70;  /* first seen in both at once */

  /* Sequential, one hue monotonic in lightness — the engagement stages are an
     ordered magnitude, not categories. */
  --stage-1: #2e6ba8; --stage-2: #3987e5; --stage-3: #69a6ec; --stage-4: #a8cef7;

  /* How often a gift is sent, against what it is worth — two meanings, two
     slots, reused wherever that pair appears so the reader learns it once. */
  --mix-sends: #d95926;
  --grid: #232a38;
}

.filter-row { display: flex; gap: 0.4rem; margin-bottom: 0.8rem; }
.filter-row .active { border-color: var(--accent); color: var(--text); }

.chart-group { position: relative; display: grid; gap: 1rem; margin-bottom: 1.2rem; }
@media (min-width: 900px) { .chart-group { grid-template-columns: 1fr 1fr; } }

.chart-card {
  margin: 0; padding: 0.6rem 0.7rem;
  background: var(--panel-2); border: 1px solid var(--border); border-radius: 8px;
}
.chart-card figcaption {
  display: flex; align-items: center; gap: 0.4rem;
  font-size: 0.8rem; margin-bottom: 0.3rem;
}
/* Lines get a line key, not a filled box — a box is data-weight ink doing a
   label's job at this size. */
.series-key {
  display: inline-block; width: 12px; height: 2px; border-radius: 1px; flex: none;
}

svg.chart { display: block; width: 100%; height: auto; touch-action: none; }
svg.chart:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
/* Hairline and solid: dashed grid reads as noise and competes with the data. */
.grid line { stroke: var(--grid); stroke-width: 1; }
.chart .tick { fill: var(--muted); font-size: 9px; font-variant-numeric: tabular-nums; }
.chart-line { fill: none; stroke-width: 2; stroke-linejoin: round; stroke-linecap: round; }
.chart-area { opacity: 0.1; stroke: none; }
/* The 2px ring in the surface colour is what keeps a dot legible where it sits
   on its own line. */
.chart-peak, .chart-cursor { stroke: var(--panel-2); stroke-width: 2; }
.peak-label { fill: var(--text); font-size: 10px; font-variant-numeric: tabular-nums; }
.crosshair { stroke: var(--muted); stroke-width: 1; }

.chart-tip {
  position: absolute; top: -0.4rem; pointer-events: none; z-index: 5;
  padding: 0.45rem 0.6rem; font-size: 0.78rem; line-height: 1.5;
  background: var(--panel); border: 1px solid var(--border); border-radius: 6px;
  box-shadow: 0 4px 14px rgba(0, 0, 0, 0.45); white-space: nowrap;
}
.chart-tip strong { font-variant-numeric: tabular-nums; }
.chart-tip .series-key { margin-right: 0.35rem; vertical-align: middle; }
.empty { padding: 1.5rem; text-align: center; color: var(--muted); font-size: 0.9rem; }

/* --- the broadcast grid ---
   One card per stream, live or finished. This began as the Live tab, which had
   been a table: a table answers the wrong question on a narrow screen, because
   the [data-priority] rules at the foot of this file drop columns tier by tier,
   so the one view whose entire job was "what is happening right now" hid the
   viewer count on a phone. A card reflows instead, and nothing in this block
   hides at any width.

   Live and recorded cards share every rule here — they are the same object at
   two moments — and differ only where .stream-card overrides below. */

.stream-grid {
  display: grid;
  gap: 0.9rem;
  /* The whole responsive strategy, and no media query: auto-fill gives three
     columns at the 1400px main, two around 900px and one below ~700px, while
     min(...,100%) stops a 320px viewport being pushed into a sideways scroll. */
  grid-template-columns: repeat(auto-fill, minmax(min(310px, 100%), 1fr));
}

.live-card,
.stream-card {
  display: flex;
  flex-direction: column;
  gap: 0.7rem;
  padding: 0.9rem;
  background: var(--panel);
  border: 1px solid var(--border);
  /* The same left edge a toast uses, so "this one is live" is a cue the reader
     has already been taught elsewhere in the app. */
  border-left: 3px solid var(--accent);
  border-radius: 12px;
  cursor: pointer;
  font-variant-numeric: tabular-nums;
}
.live-card:hover, .stream-card:hover { background: var(--panel-2); border-color: var(--accent); }
.live-card:focus-within,
.stream-card:focus-within { outline: 2px solid var(--accent); outline-offset: 2px; }

.live-card-top { display: flex; align-items: center; gap: 0.4rem; flex-wrap: wrap; }
.live-chan {
  color: var(--text); font-weight: 600; font-size: 1rem;
  text-decoration: none; margin-right: auto; overflow-wrap: anywhere;
}
.live-chan:hover { text-decoration: underline; }

.live-card-time { display: flex; align-items: baseline; gap: 0.5rem; flex-wrap: wrap; }
/* Its own colour and tabular figures: this is the one number on the page that
   moves while nothing happens, and digits of differing width would make it
   twitch sideways every second. */
.live-elapsed { color: var(--accent-2); font-weight: 600; font-variant-numeric: tabular-nums; }

.live-card-viewers { display: flex; align-items: center; gap: 0.75rem; flex-wrap: wrap; }
.live-figures { flex: 0 0 auto; }
.live-now { display: flex; align-items: baseline; gap: 0.35rem; }
/* The card's hero figure. The table it replaced could only show the peak. */
.live-now strong { font-size: 1.5rem; color: var(--text); }
.live-now .icon { color: var(--muted); align-self: center; }
.live-peak { display: block; }

/* --gem, not --accent: the LIVE badge and the card's left edge already carry
   the accent here, and a third mark in the same hue competes with both. */
.spark { display: block; flex: 1 1 8rem; min-width: 6rem; height: 34px; }
.spark-line { fill: none; stroke: var(--gem); stroke-width: 2; stroke-linejoin: round; stroke-linecap: round; }
/* A card with too few points keeps the same overall height, so the grid does
   not jolt when the second sample lands and the curve appears. */
.spark-empty {
  display: block; flex: 1 1 8rem; min-width: 6rem; height: 34px;
  border: 1px dashed var(--border); border-radius: 6px;
}

/* Shared by the cards and the live console, so the two read as one object at
   two sizes. A grid rather than a wrap: numbers that line up into columns can
   be scanned down, and a ragged flex row cannot. */
.counter-strip {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(4.5rem, 1fr));
  gap: 0.4rem 0.6rem;
}
.counter-strip.full {
  grid-template-columns: repeat(auto-fit, minmax(8rem, 1fr));
  gap: 0.5rem 1rem;
  padding: 0.75rem 1rem;
  margin-bottom: 0.75rem;
  background: var(--panel);
  border: 1px solid var(--border);
  border-radius: 8px;
}
.counter {
  display: flex; align-items: baseline; gap: 0.3rem;
  min-width: 0; color: var(--muted); font-size: 0.8rem;
}
.counter strong { color: var(--text); font-variant-numeric: tabular-nums; }
.counter .icon { align-self: center; }
.counter-strip.full .counter { font-size: 0.9rem; }
/* The compact strip on a card is read as a row of figures with their icons;
   the word is the fallback for anyone who does not recognise one. */
.live-card .counter-label,
.stream-card .counter-label { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

/* margin-top:auto keeps every Console button on one baseline across a row of
   cards of unequal height. */
.live-card-foot {
  display: flex; align-items: center; justify-content: space-between; gap: 0.5rem;
  margin-top: auto; padding-top: 0.5rem; border-top: 1px solid var(--border);
}

/* --- the recorded-stream grid ---
   Same object as a live card and deliberately the same classes inside it, so
   the two read as one thing at two moments. Only the differences live here.
   The accent left edge is what a live card uses to say "this is happening
   now", so a recorded card gives it back and only borrows it again for a row
   that really is still live. */
.stream-card { border-left-color: var(--border); }
.stream-card.is-live { border-left-color: var(--accent); }

/* The hero on a recorded card is how long it ran, not how many are watching:
   a stream that has ended has no "now". Tabular figures for the same reason
   the live clock uses them - these sit in a grid and must not jitter. */
.stream-len { font-size: 1.35rem; font-weight: 600; color: var(--text); font-variant-numeric: tabular-nums; }
/* Peak is the secondary figure here, so it steps down from the live card's
   1.5rem rather than competing with the length beside it. */
.stream-card .live-now strong { font-size: 1.25rem; }

/* Top gifter and guest count sit on the foot line. min-width:0 with the
   ellipsis lets a long @name give way to the Delete button rather than
   pushing it out of the card. */
.stream-foot-facts {
  display: flex; align-items: center; gap: 0.75rem; min-width: 0;
  color: var(--muted); font-size: 0.8rem;
}
.stream-foot-facts .fact { display: flex; align-items: center; gap: 0.3rem; min-width: 0; }
.stream-foot-facts .fact strong { color: var(--text); font-variant-numeric: tabular-nums; }
.stream-foot-facts a { color: var(--text); text-decoration: none; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.stream-foot-facts a:hover { text-decoration: underline; }

/* --- grouped lists ---
   One heading above a run of rows or cards. In a table this is a row spanning
   every column, so the columns stay one set of widths down the whole table -
   splitting a group into its own table would let them drift and the point of
   the table is comparison. */
.group-head th {
  padding: 0.7rem 0.6rem 0.35rem;
  background: var(--bg);
  border-bottom: 1px solid var(--border);
  font-size: 0.85rem; text-align: left; position: static;
}
.group-head .group-note { font-weight: 400; margin-left: 0.4rem; }
.group-title {
  margin: 1.1rem 0 0.6rem; font-size: 0.95rem; font-weight: 600;
  padding-bottom: 0.35rem; border-bottom: 1px solid var(--border);
}
.group-title:first-child { margin-top: 0; }
/* The live section's heading borrows the accent and the banner's pulse, so
   the boundary between "now" and "recorded" is visible from across the room. */
.group-title.is-live { color: var(--accent); border-bottom-color: var(--accent); }
.live-dot {
  display: inline-block; width: 8px; height: 8px; border-radius: 50%;
  background: var(--accent); margin-right: 0.45rem; vertical-align: 1px;
  animation: pulse 1.4s infinite;
}
.group-head .live-dot { animation: none; }
@media (prefers-reduced-motion: reduce) { .live-dot { animation: none; } }
.group-title .group-note { font-weight: 400; margin-left: 0.4rem; }

/* The two view buttons read as one control, so they lose the gap between them
   and only the outer corners round. */
.view-toggle { display: flex; gap: 0; }
.view-toggle button { border-radius: 0; margin-left: -1px; }
.view-toggle button:first-child { border-radius: 8px 0 0 8px; margin-left: 0; }
.view-toggle button:last-child { border-radius: 0 8px 8px 0; }
/* The pressed one has to sit above its neighbour or its accent border is
   painted over by the next button's plain one. */
.view-toggle button.active { position: relative; z-index: 1; }

/* Date range: the two inputs are one control and should not wrap apart. */
.range-group { display: flex; align-items: center; gap: 0.35rem; flex-wrap: wrap; }
.range-group input[type="date"] {
  padding: 0.35rem 0.5rem; font-size: 0.85rem;
  background: var(--panel); color: var(--text);
  border: 1px solid var(--border); border-radius: 8px;
}
.range-group .range-sep { color: var(--muted); font-size: 0.85rem; }

/* --- the flag panel, at the top of a person --- */

/* Unflagged it is one quiet line — most people are not flagged and the panel
   must not shout at the reader on every person they open. Flagged, it takes a
   left border in the level's colour and reads as the first thing on the page. */
.flag-panel {
  display: flex; flex-wrap: wrap; align-items: center; gap: 0.6rem;
  padding: 0.6rem 0.9rem;
  margin-bottom: 0.75rem;
  background: var(--panel);
  border: 1px solid var(--border);
  border-left: 3px solid var(--border);
  border-radius: 8px;
}
.flag-panel.on { border-left-color: var(--flag-watch); }
.flag-panel.flag-warn { border-left-color: var(--flag-warn); }
.flag-panel.flag-block { border-left-color: var(--flag-block); background: rgba(248, 113, 113, 0.08); }
.flag-panel.editing, .flag-panel.on { display: block; }
.flag-head { display: flex; flex-wrap: wrap; align-items: baseline; gap: 0.5rem; }
.flag-panel p { margin: 0.3rem 0 0; }
.flag-actions { display: flex; gap: 0.4rem; margin-top: 0.5rem; }
.flag-panel:not(.on):not(.editing) .flag-actions { margin-top: 0; }
.flag-levels { display: flex; gap: 0.4rem; flex-wrap: wrap; margin-bottom: 0.5rem; }
.flag-levels .active { border-color: var(--accent); color: var(--text); }
.flag-panel input[type="text"] { width: 100%; }

/* --- live console --- */

.counters {
  display: flex;
  flex-wrap: wrap;
  gap: 0.5rem 1.25rem;
  padding: 0.75rem 1rem;
  margin-bottom: 0.75rem;
  background: var(--panel);
  border: 1px solid var(--border);
  border-radius: 8px;
}
.counters span { display: inline-flex; align-items: center; gap: 0.35rem; color: var(--muted); }
.counters strong { color: var(--text); font-variant-numeric: tabular-nums; }

/* The live feed is a tab of the stream overlay now, not a separate console
   below the grid. The clock rides in the overlay's title beside the live
   badge, where the reader is already looking to see whose stream this is. */
.title-clock { margin-left: 0.5rem; font-size: 0.85rem; font-weight: normal; }

.guests {
  margin-bottom: 0.75rem; display: flex; flex-wrap: wrap; gap: 0.35rem; align-items: center;
  padding: 0.5rem 0.75rem; background: var(--panel-2);
  border: 1px solid var(--border); border-radius: 8px;
}
/* An empty panel is nobody on it, not a bordered gap where a strip would be. */
.guests:empty { display: none; }

/* The feed is newest-first and capped in JS, so it scrolls within a fixed
   frame rather than pushing the rest of the page down as events arrive. */
.feed {
  max-height: 60vh;
  overflow-y: auto;
  background: var(--bg);
  border: 1px solid var(--border);
  border-radius: 8px;
}
.feed-row {
  display: flex;
  align-items: baseline;
  gap: 0.5rem;
  padding: 0.35rem 0.75rem;
  border-bottom: 1px solid var(--panel);
  font-size: 0.9rem;
  line-height: 1.4;
  overflow-wrap: anywhere;
}
.feed-row:last-child { border-bottom: none; }
.feed-time { color: var(--muted); font-variant-numeric: tabular-nums; flex: 0 0 auto; font-size: 0.8rem; }
.feed-user { color: var(--gem); font-weight: 600; }
.feed-gift { background: rgba(125, 211, 252, 0.08); }
.feed-mod { background: rgba(240, 86, 122, 0.12); }
.feed-flagged { background: rgba(248, 113, 113, 0.14); }
/* Room signals are context, not incident: present but quieter than a mod row. */
.feed-room { background: rgba(148, 163, 184, 0.10); }
.feed-like .icon-heart { color: var(--accent); }
.feed-guest_join, .feed-guest_leave { background: rgba(255, 184, 107, 0.10); }

/* Scoped to the console on purpose. feedRow emits these same classes into the
   stream overlay's timeline, where a full-row wash reads correctly against a
   static list. In a feed that is moving, a coloured left edge tells you what
   kind of line just arrived without ten tinted bands fighting the text. */
#console-feed { background: var(--bg); border-radius: 8px; }
/* .modal-body .feed lifts the height cap, which suits the stream timeline: a
   finished list the reader scrolls the page through. This one is still being
   written to, so it keeps its own frame and scrolls inside it rather than
   pushing the composer off the bottom of the dialog as events arrive. */
.modal-body #console-feed { max-height: 55vh; overflow-y: auto; }
#console-feed .feed-row {
  padding: 0.45rem 0.75rem;
  border-left: 2px solid transparent;
  background: none;
}
#console-feed .feed-row:hover { background: var(--panel); }
#console-feed .feed-time { font-size: 0.75rem; flex-basis: 4.5rem; }
#console-feed .feed-gift { border-left-color: var(--gem); }
#console-feed .feed-mod { border-left-color: var(--accent); }
#console-feed .feed-room { border-left-color: var(--muted); }
#console-feed .feed-guest_join, #console-feed .feed-guest_leave { border-left-color: var(--accent-2); }

/* A shell around the composer, so the input reads as part of the console
   rather than as a stray field parked beneath it. */
#console-composer .composer {
  padding: 0.5rem;
  background: var(--panel-2);
  border: 1px solid var(--border);
  border-radius: 10px;
}
#console-composer .composer input { background: var(--bg); border-color: transparent; }

.detail-head { display: flex; align-items: center; justify-content: space-between; gap: 1rem; }

/* --- the detail overlay (person, stream, channel) --- */

/* A native <dialog>: the browser supplies the top layer, focus trap and
   backdrop, so this only has to describe the frame. */
.modal {
  /* The browser centers a native <dialog> with margin:auto in its own
     stylesheet; the * reset above zeroes that and pins it top-left. */
  margin: auto;
  width: min(1000px, 92vw);
  max-height: 88vh;
  padding: 0;
  border: 1px solid var(--border);
  border-radius: 12px;
  background: var(--panel);
  color: var(--text);
  box-shadow: 0 20px 60px rgba(0, 0, 0, 0.55);
  /* The frame is fixed; .modal-body is what scrolls. */
  display: flex;
  flex-direction: column;
  overflow: hidden;
}
/* A closed dialog must stay hidden — display:flex above would otherwise
   override the browser default and leave it on screen. */
.modal:not([open]) { display: none; }

.modal::backdrop { background: rgba(15, 17, 23, 0.72); }

.modal-head {
  display: flex; align-items: center; justify-content: space-between; gap: 1rem;
  padding: 0.9rem 1rem;
  border-bottom: 1px solid var(--border);
  flex: 0 0 auto;
}
.modal-head h3 { font-size: 0.95rem; }
.modal-head h3 a { color: inherit; text-decoration: none; }
.modal-head h3 a:hover { text-decoration: underline; }

/* Where the open subject sits — its list, and its channel if it has one — with
   the same-channel prev/next steppers on the right. */
.crumbs {
  display: flex; align-items: center; gap: 0.35rem;
  padding: 0.55rem 1rem 0;
  font-size: 0.78rem; color: var(--muted);
  flex: 0 0 auto; overflow-x: visible;
}
.crumbs a { color: var(--muted); text-decoration: none; }
.crumbs a:hover { color: var(--text); text-decoration: underline; }
.crumb-sep { color: var(--muted); opacity: 0.6; }
.crumb-steppers { margin-left: auto; display: flex; gap: 0.4rem; align-items: center; }

.modal-tabs {
  display: flex; gap: 0.4rem; flex-wrap: wrap;
  padding: 0.7rem 1rem;
  border-bottom: 1px solid var(--border);
  flex: 0 0 auto;
}
.modal-tabs button:disabled { opacity: 0.4; cursor: default; }
.modal-tabs button:disabled:hover { filter: none; }

.modal-body { padding: 1rem; overflow-y: auto; flex: 1 1 auto; }
.modal-body h4 { margin-bottom: 0.6rem; font-size: 0.85rem; color: var(--muted); }
.modal-body h4:not(:first-child) { margin-top: 1.2rem; }
/* Inside the modal the body owns the scrolling, so the feed must not cap
   itself as it does on the page. */
.modal-body .feed { max-height: none; }

/* <dialog> does not consistently stop the page behind it scrolling. */
body.modal-open { overflow: hidden; }

/* --- diagrams ---
   Same frame as .chart-group: positioned, so the shared tooltip can be placed
   against it and clamped to its width. */
.viz { position: relative; margin-bottom: 0.9rem; }
svg.viz-svg { display: block; width: 100%; height: auto; overflow: visible; }

.viz-label { fill: var(--text); font-size: 10px; }
.viz-count { fill: var(--muted); font-size: 9px; font-variant-numeric: tabular-nums; }
.viz-rail { stroke: var(--border); stroke-width: 2; }

.viz a { cursor: pointer; }
.viz a .viz-mark { transition: filter 0.1s ease; }
.viz a:hover .viz-mark { filter: brightness(1.3); }
/* SVG anchors are focusable natively, so this is the whole keyboard story. */
.viz a:focus { outline: none; }
.viz a:focus-visible .viz-mark { outline: 2px solid var(--accent); outline-offset: 2px; }

.viz-mark { fill: var(--flow-in); }
.viz-mark.mine { fill: var(--accent); }
/* A ring, the same idiom the chart cursor uses: this mark sits on top of
   something — here, the first time they ever appeared in that channel. */
.viz-mark.first { stroke: var(--accent-2); stroke-width: 2; }
.viz-more { fill: var(--muted); }
.viz-edge { fill: none; stroke: var(--flow-in); stroke-linecap: round; }
.viz-link { fill: var(--muted); text-decoration: underline; }
.viz a:hover .viz-link { fill: var(--text); }

/* Columns and the equality reference line share the chart card's language. */
.chart-bar { opacity: 0.9; }
.chart-equality { stroke: var(--grid); stroke-width: 1.5; stroke-dasharray: 4 4; }

/* --- viewer trajectories ---
   Small multiples: one card per stream, all in the same ink because ten hues
   would be indistinguishable here. The caption on each card carries identity. */
.traj-grid {
  display: grid; gap: 0.6rem; margin-bottom: 0.9rem;
  grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));
}
.traj-card {
  display: block; text-decoration: none; color: inherit;
  border: 1px solid var(--border); border-radius: 8px; padding: 0.45rem 0.5rem;
  background: var(--panel-2);
}
.traj-card:hover, .traj-card:focus-visible { border-color: var(--accent); outline: none; }
.traj-head { display: block; font-size: 0.75rem; color: var(--text); }
.traj-foot { display: block; font-size: 0.7rem; color: var(--muted); font-variant-numeric: tabular-nums; }
.traj-svg { display: block; width: 100%; height: auto; }
.traj-line { fill: none; stroke: var(--series-diamonds); stroke-width: 2; stroke-linejoin: round; }
.traj-card:hover .traj-line, .traj-card:focus-visible .traj-line { stroke: var(--accent); }
/* A watched channel with no audience yet still takes its seat: one missing
   from the picture of watched channels is a hole the reader cannot see. */
.viz-mark.idle { fill: var(--panel-2); stroke: var(--border); stroke-dasharray: 2 2; }
#network-viz h3 { font-size: 0.9rem; margin: 1.1rem 0 0.5rem; }
#network-viz h3:first-child { margin-top: 0; }

/* The stream timeline's filter strip and its caption both need air the plain
   .filter-row (used inline in a toolbar) deliberately does not have. */
.timeline-filters { margin-bottom: 0.6rem; }
.timeline-caption { margin-bottom: 0.7rem; }
.feed-stream_start, .feed-stream_end { opacity: 0.85; }

/* --- person timeline ---
   One rail of eras, newest at the top. The dot marks the era's channel; an
   accent ring on it is the person's first appearance in that channel — the
   migration moments the tab exists to show. */
.timeline { display: flex; flex-direction: column; margin-left: 0.4rem; }
.timeline-era {
  position: relative;
  display: flex; gap: 0.8rem; align-items: baseline; flex-wrap: wrap;
  padding: 0.4rem 0 0.4rem 1rem;
  border-left: 2px solid var(--border);
}
.timeline-era::before {
  content: '';
  position: absolute; left: -6px; top: 0.75em;
  width: 6px; height: 6px; border-radius: 50%;
  background: var(--muted); border: 2px solid var(--panel-2);
}
.timeline-era.first::before { background: var(--accent-2); }
.timeline-when { flex: 0 0 auto; min-width: 8.5rem; }
.timeline-what { display: flex; gap: 0.45rem; align-items: baseline; flex-wrap: wrap; }
.timeline-what a { color: var(--text); }

/* --- header and shell ---
   Sticky, because the tab strip is the only way back out of a long table and
   scrolling to the top to reach it was most of what made this feel like hard
   work. */
header {
  position: sticky; top: 0; z-index: 20;
}
/* Four tabs fit at any width, but a narrow phone still needs somewhere for
   them to go — scroll the strip rather than wrapping it into a second row
   that shifts everything below it down. */
nav {
  overflow-x: auto;
  scrollbar-width: none;
  -webkit-overflow-scrolling: touch;
}
nav::-webkit-scrollbar { display: none; }
nav .tab { white-space: nowrap; }

main { max-width: 1400px; }

/* --- toolbars --- */
.toolbar {
  display: flex; flex-wrap: wrap; align-items: center;
  gap: 0.5rem 1rem; margin-bottom: 0.9rem;
}
.toolbar-group { display: flex; gap: 0.5rem; flex-wrap: wrap; align-items: center; }
.toolbar input[type="search"] { min-width: 15rem; }
.filter-row { display: flex; gap: 0.4rem; flex-wrap: wrap; margin-bottom: 0; }
.filter-row .active { border-color: var(--accent); color: var(--text); background: var(--panel-2); }

/* A toolbar that stacks: controls on the first row, chips on the second. Each
   row wraps on its own so a long run of channel chips never drags the search
   box down the page with it. */
.toolbar.stacked { flex-direction: column; align-items: stretch; gap: 0.6rem; }
.toolbar-row { display: flex; flex-wrap: wrap; align-items: center; gap: 0.5rem 1rem; }
/* One word in front of a chip row, so "Live · Ended · Had gifts" and a run of
   channel names read as two different questions rather than one long one. */
.chip-label { color: var(--muted); font-size: 0.78rem; margin-right: 0.1rem; }
/* What the list is narrowed to, and the one control that undoes all of it.
   Pushed to the right end of the row so it reads as a status, not a filter. */
.filter-summary { margin-left: auto; gap: 0.4rem; color: var(--muted); font-size: 0.8rem; }
.filter-summary .chip-label { margin: 0; }
/* The heading's totals for whatever is on screen. */
#streams-count .sep { margin: 0 0.3rem; opacity: 0.6; }
.empty-actions { margin-top: 0.75rem; }

/* An <a> that has to sit in a row of buttons. */
.button-like { text-decoration: none; display: inline-flex; align-items: center; }
.detail-actions { display: flex; gap: 0.4rem; align-items: center; }

/* Everyone the monitor can see right now. Not a table filter: presence is
   runtime state the paged query knows nothing about. */
.live-strip {
  display: flex; flex-wrap: wrap; gap: 0.35rem; align-items: center;
  margin-bottom: 0.8rem;
}
.live-strip .badge { text-decoration: none; }

/* --- sortable headers --- */
.th-sort {
  display: inline-flex; align-items: center; gap: 0.3rem;
  background: none; border: none; padding: 0; margin: 0;
  color: inherit; font: inherit; text-transform: inherit; letter-spacing: inherit;
  cursor: pointer;
}
.th-sort:hover { color: var(--text); filter: none; }
th.num .th-sort { flex-direction: row-reverse; }
.sort-arrow { font-size: 0.6em; color: var(--accent); }
th[aria-sort] { color: var(--text); }

/* --- pager --- */
.pager {
  display: flex; align-items: center; justify-content: center; gap: 0.8rem;
  padding: 0.9rem 0;
}
.pager-note { padding: 0.7rem 0; text-align: center; }

/* Non-numeric cells wrap by default, which is right for a comment and wrong
   for an error string long enough to double a table's row height. Truncate it
   and leave the whole text in the cell's title. */
/* The gift's own artwork, sized to sit on a line of text rather than to be
   looked at — it is there to be recognised at a glance beside the name. */
.gift-img {
  width: 20px; height: 20px; vertical-align: -0.35em;
  margin-right: 0.35rem; border-radius: 3px;
}

/* The raw protocol payload, folded away. It exists for the action codes the
   protocol does not name, so it has to be readable when opened and invisible
   when not. */
.raw summary { cursor: pointer; color: var(--muted); font-size: 0.8rem; }
.raw pre {
  margin-top: 0.4rem; padding: 0.5rem; max-width: 40ch; max-height: 16rem;
  overflow: auto; background: var(--bg); border: 1px solid var(--border);
  border-radius: 4px; font-size: 0.75rem; white-space: pre;
}

.trunc {
  display: inline-block; max-width: 22ch;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap; vertical-align: bottom;
}

/* --- responsive columns ---
   Every cell carries the priority its column declared. A narrow screen drops
   the secondary columns rather than side-scrolling a table whose first column
   is the one being looked for. */
@media (max-width: 1200px) {
  [data-priority="4"] { display: none; }
}

@media (max-width: 900px) {
  [data-priority="3"] { display: none; }
}
@media (max-width: 700px) {
  [data-priority="2"] { display: none; }
  main { padding: 1rem 0.75rem; }
  header { padding: 0.7rem 0.75rem; gap: 0.75rem; }
  header h1 { font-size: 1rem; }
  header h1 span { display: none; }
  .toolbar input[type="search"] { min-width: 0; width: 100%; }
  .toolbar-group { width: 100%; }
  .filter-summary { margin-left: 0; }
  /* Full-bleed: a 92vw dialog with rounded corners inside a 375px viewport is
     mostly frame. */
  .modal {
    width: 100vw; max-width: 100vw; height: 100dvh; max-height: 100dvh;
    border-radius: 0; border: none;
  }
  .counters { gap: 0.4rem 0.9rem; font-size: 0.85rem; }
  .counter-strip.full { gap: 0.4rem 0.9rem; padding: 0.6rem 0.75rem; }
  .counter-strip.full .counter { font-size: 0.85rem; }
  .live-card, .stream-card { padding: 0.75rem; }
  .live-now strong { font-size: 1.3rem; }
}

/* A 24px-tall button is a fine mouse target and a poor thumb one. */
@media (pointer: coarse) {
  button.tiny, .button-like { min-height: 40px; padding: 0.5rem 0.8rem; }
  .th-sort { min-height: 0; padding: 0; }
}

/* Long usernames and comment text must be allowed to wrap; the numeric
   columns are what benefit from nowrap. */
td:not(.num) { white-space: normal; }

/* A refetch that keeps its frame: the current render dims rather than
   blanking to a skeleton and jumping the panel under the reader. */
.modal-body.loading { opacity: 0.55; transition: opacity 0.15s ease; }

/* This tab went from a single pulsing dot in the banner to one on every card,
   which is the point at which the animation needs an opt-out. */
@media (prefers-reduced-motion: reduce) {
  .badge.now .dot, #live-banner .dot { animation: none; }
}

/* A comment TikTok deleted. Kept past the rolling window on purpose, so it can
   sit among far newer ones — the strike is what says why it is still here. */
.feed-row.removed { opacity: 0.72; }
.feed-row.removed s { text-decoration-color: #f87171; }

/* The comments a moderation row removed, listed under its detail cell. */
.removed-comments { margin: 0.4rem 0 0; padding-left: 1rem; list-style: none; }
.removed-comments li { margin-bottom: 0.25rem; border-left: 2px solid #f87171; padding-left: 0.5rem; }

/* --- the Likes tab's Live view ---
   Two columns: the race on the left answers "which room is hot right now", the
   ticker on the right answers "who is doing it". Both are patched in place once
   a second rather than re-rendered, so everything here that moves is a
   transition on a property and not a redraw. */
.like-live { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 20rem); gap: 1rem; }
.like-live-race, .like-live-side {
  background: var(--panel); border: 1px solid var(--border); border-radius: 10px; padding: 0.9rem;
}
.like-live-head {
  display: flex; align-items: baseline; justify-content: space-between; gap: 0.75rem;
  margin-bottom: 0.6rem;
}
.like-live-head h3 { font-size: 0.95rem; }
.like-live .icon-heart { color: var(--accent); }
/* The empty states sit beside their lists rather than inside them, so the row
   patcher never has to tell a placeholder apart from a row it owns. */
.like-live .empty { padding: 1.25rem 0.5rem; }

/* A room in the race. A button because it is one — clicking narrows the ticker
   to that room — but it carries no button chrome, because a bordered control
   per row would read as a toolbar rather than a chart. */
.like-room {
  display: block; width: 100%; text-align: left; cursor: pointer;
  background: none; border: none; border-radius: 6px;
  padding: 0.45rem 0.5rem; margin-bottom: 0.15rem; color: inherit; font: inherit;
}
.like-room:hover { background: var(--panel-2); }
.like-room.focus { background: var(--panel-2); box-shadow: inset 2px 0 0 var(--accent); }
.like-room:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; }
.like-room-top { display: flex; align-items: baseline; justify-content: space-between; gap: 0.75rem; }
.like-room-name { font-weight: 600; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.like-room-rate { color: var(--accent); font-weight: 700; font-variant-numeric: tabular-nums; }
.like-room-track {
  display: block; height: 6px; margin: 0.35rem 0 0.3rem;
  background: var(--panel-2); border-radius: 3px; overflow: hidden;
}
/* The width is the whole chart. Transitioned because the value is repainted
   every second and a bar that jumps reads as noise, where one that slides reads
   as a rate changing. */
.like-room-bar {
  display: block; height: 100%; width: 0;
  background: linear-gradient(90deg, var(--accent), var(--accent-2));
  border-radius: 3px; transition: width 0.9s ease-out;
}
.like-room-sub { display: block; }

/* A liker's current burst. One row per person per burst, not per message —
   see LIKE_HOLD_IDLE_MS. */
.like-tap {
  display: grid; grid-template-columns: minmax(0, 1fr) auto;
  gap: 0 0.5rem; padding: 0.4rem 0.25rem;
  border-bottom: 1px solid var(--border); text-decoration: none; color: inherit;
}
.like-tap:last-child { border-bottom: none; }
.like-tap:hover { background: var(--panel-2); }
.like-tap-who {
  color: var(--gem); font-weight: 600;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.like-tap-count {
  display: flex; align-items: center; gap: 0.25rem;
  color: var(--accent); font-weight: 700; font-variant-numeric: tabular-nums;
}
.like-tap-where { grid-column: 1 / -1; }

/* Still tapping: the heart beats while the count is climbing, and stops when
   the burst settles. This is the one cue that separates a row still being added
   to from one that finished ten seconds ago and merely sits near the top. */
.like-tap.holding .like-tap-who { color: var(--text); }
.like-tap.holding .icon-heart { animation: like-beat 0.9s ease-in-out infinite; }
@keyframes like-beat {
  0%, 100% { transform: scale(1); opacity: 0.85; }
  30% { transform: scale(1.35); opacity: 1; }
}

@media (max-width: 900px) {
  .like-live { grid-template-columns: minmax(0, 1fr); }
}

/* The race still slides under reduced motion — a width change is the value
   itself, not decoration — but the beating heart is pure ornament. */
@media (prefers-reduced-motion: reduce) {
  .like-tap.holding .icon-heart { animation: none; }
}
