/* ==========================================================================
   SavePace  —  design system
   Light first, dark via prefers-color-scheme. All colours are checked for
   WCAG 2.2 AA contrast against their intended background.

   The palette is violet primary + cyan "neon" accent on a cool neutral base,
   and the two display faces (Outfit for headings, Figtree for body) are served
   from /fonts so the CSP can keep font-src 'self'. Both are SIL OFL 1.1; the
   licence texts sit next to the files as OFL-*.txt.
   ========================================================================== */

/* ── Self-hosted webfonts ────────────────────────────────────────────────── */

@font-face {
  font-family: "Outfit"; font-style: normal; font-weight: 600; font-display: swap;
  src: url("/fonts/outfit-600-latin.woff2") 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;
}
@font-face {
  font-family: "Outfit"; font-style: normal; font-weight: 600; font-display: swap;
  src: url("/fonts/outfit-600-latin-ext.woff2") format("woff2");
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF,
    U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020,
    U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
@font-face {
  font-family: "Outfit"; font-style: normal; font-weight: 700; font-display: swap;
  src: url("/fonts/outfit-700-latin.woff2") 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;
}
@font-face {
  font-family: "Outfit"; font-style: normal; font-weight: 700; font-display: swap;
  src: url("/fonts/outfit-700-latin-ext.woff2") format("woff2");
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF,
    U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020,
    U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
@font-face {
  font-family: "Figtree"; font-style: normal; font-weight: 400; font-display: swap;
  src: url("/fonts/figtree-400-latin.woff2") 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;
}
@font-face {
  font-family: "Figtree"; font-style: normal; font-weight: 400; font-display: swap;
  src: url("/fonts/figtree-400-latin-ext.woff2") format("woff2");
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF,
    U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020,
    U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
@font-face {
  font-family: "Figtree"; font-style: normal; font-weight: 600; font-display: swap;
  src: url("/fonts/figtree-600-latin.woff2") 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;
}
@font-face {
  font-family: "Figtree"; font-style: normal; font-weight: 600; font-display: swap;
  src: url("/fonts/figtree-600-latin-ext.woff2") format("woff2");
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF,
    U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020,
    U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}

:root {
  --accent: #7034f5;
  --accent-ink: #ffffff;
  --accent-soft: #f1ecff;
  --accent-line: #d9cbff;
  --accent-deep: #4a1bb5;

  /* Cyan. Only ever drawn on dark ink or as a fill — never as text on white,
     where it drops to 2.4:1. */
  --neon: #0b7f80;
  --neon-bright: #00e3e4;
  --neon-soft: #e2fbfb;
  --neon-line: #b6eeee;

  --bg: #f7f8fb;
  --surface: #ffffff;
  --surface-2: #f3f4f8;
  --ink: #171a26;
  --ink-2: #4a4e60;
  --ink-3: #6e7184;
  --line: #e8e9ee;
  --line-strong: #d7d9e2;

  --ok: #0f6e56;
  --ok-soft: #e1f5ee;
  --warn: #854f0b;
  --warn-soft: #faeeda;
  --danger: #b3261e;
  --danger-soft: #fcebeb;

  --radius-s: 10px;
  --radius: 14px;
  --radius-l: 20px;
  --shadow: 0 1px 2px rgba(23, 26, 38, 0.04), 0 10px 30px -18px rgba(23, 26, 38, 0.18);
  --shadow-lift: 0 2px 4px rgba(23, 26, 38, 0.05), 0 18px 44px -20px rgba(23, 26, 38, 0.22);

  /* Two soft radial washes — the "grain" the product uses behind hero blocks. */
  --wash: radial-gradient(circle at 12% 18%, rgba(112, 52, 245, 0.10), transparent 45%),
          radial-gradient(circle at 86% 6%, rgba(0, 227, 228, 0.12), transparent 46%),
          radial-gradient(circle at 70% 70%, rgba(155, 104, 255, 0.07), transparent 45%);

  /* Where the front-door artwork is allowed to show. A mask, so it is a shape
     rather than a colour and needs no dark-mode twin: solid at the top, gone by
     the bottom edge where the page content begins. */
  --hero-fade: linear-gradient(to bottom, rgba(0, 0, 0, 0.9), rgba(0, 0, 0, 0.55) 58%, rgba(0, 0, 0, 0) 100%);

  --font-display: "Outfit", "Figtree", -apple-system, BlinkMacSystemFont, "Segoe UI",
    "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", sans-serif;
  --font: "Figtree", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,
    "Helvetica Neue", Arial, "Noto Sans", "PingFang SC", "Hiragino Sans GB",
    "Microsoft YaHei", sans-serif;
  --mono: ui-monospace, SFMono-Regular, Menlo, Consolas, "Liberation Mono", monospace;
}

@media (prefers-color-scheme: dark) {
  :root {
    --accent: #9b68ff;
    --accent-ink: #10081f;
    --accent-soft: #241a45;
    --accent-line: #3d2f70;
    --accent-deep: #c9aaff;

    --neon: #00d6d7;
    --neon-bright: #00d6d7;
    --neon-soft: #0d2c2d;
    --neon-line: #17494a;

    --bg: #0d0c18;
    --surface: #181729;
    --surface-2: #212036;
    --ink: #f2f1fb;
    --ink-2: #c2c1d6;
    --ink-3: #a3a2bc;
    --line: #2a2940;
    --line-strong: #3b3a58;

    --ok: #6fd3b3;
    --ok-soft: #10312a;
    --warn: #e9b872;
    --warn-soft: #35270d;
    --danger: #f0a0a0;
    --danger-soft: #3a1a1a;

    --shadow: 0 1px 2px rgba(0, 0, 0, 0.35), 0 10px 30px -18px rgba(0, 0, 0, 0.55);
    --shadow-lift: 0 2px 4px rgba(0, 0, 0, 0.4), 0 18px 44px -20px rgba(0, 0, 0, 0.65);
  }

  /* The same wash reads fainter on a dark surface, so it is nudged up — the
     point is a visible hint of the showcase, not a number. */
  .hero-art { opacity: .22; }
}

*, *::before, *::after { box-sizing: border-box; }

html { -webkit-text-size-adjust: 100%; }

body {
  margin: 0;
  background: var(--bg);
  color: var(--ink);
  font-family: var(--font);
  font-size: 15px;
  line-height: 1.65;
  -webkit-font-smoothing: antialiased;
}

h1, h2, h3, h4 { line-height: 1.25; margin: 0 0 .5rem; font-weight: 600; letter-spacing: -.02em; font-family: var(--font-display); }
h1 { font-size: 1.9rem; font-weight: 700; }
h2 { font-size: 1.35rem; }
h3 { font-size: 1.08rem; }
h4 { font-size: .95rem; }

.eyebrow {
  font-size: .74rem; font-weight: 700; letter-spacing: .12em; text-transform: uppercase;
  color: var(--accent); margin: 0 0 .25rem;
}
.dark-ink { color: var(--ink); }
p { margin: 0 0 .85rem; }
a { color: var(--accent); text-decoration: none; }
a:hover { text-decoration: underline; }
hr { border: none; border-top: 1px solid var(--line); margin: 1.75rem 0; }
small { font-size: .84rem; }

:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
  border-radius: 4px;
}

.sr-only {
  position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
  overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0;
}

.skip-link {
  position: absolute; left: -9999px; top: 0; z-index: 50;
  background: var(--surface); color: var(--ink);
  padding: .6rem 1rem; border: 1px solid var(--line-strong); border-radius: var(--radius);
}
.skip-link:focus { left: 1rem; top: 1rem; }

/* ── Layout ─────────────────────────────────────────────────────────────── */

.wrap { max-width: 1080px; margin: 0 auto; padding: 0 1.25rem; }
.wrap-narrow { max-width: 720px; margin: 0 auto; padding: 0 1.25rem; }
main { padding: 2rem 0 4rem; min-height: 60vh; }

.page-head { display: flex; flex-wrap: wrap; gap: 1rem; align-items: flex-end; justify-content: space-between; margin-bottom: 1.5rem; }
.page-head p { margin: 0; color: var(--ink-2); }
.lede { color: var(--ink-2); max-width: 62ch; }
.muted { color: var(--ink-3); }
.small { font-size: .85rem; }
.mono { font-family: var(--mono); font-size: .85rem; }
.nowrap { white-space: nowrap; }

.stack > * + * { margin-top: 1rem; }
.row { display: flex; gap: .75rem; align-items: center; flex-wrap: wrap; }
.row-end { margin-left: auto; }
.spread { display: flex; gap: 1rem; justify-content: space-between; align-items: baseline; }
.grid { display: grid; gap: 1rem; }
.grid-2 { grid-template-columns: repeat(auto-fit, minmax(260px, 1fr)); }
.grid-3 { grid-template-columns: repeat(auto-fit, minmax(230px, 1fr)); }
.grid-sidebar { display: grid; gap: 1.5rem; grid-template-columns: minmax(0, 1fr); }
@media (min-width: 900px) {
  .grid-sidebar { grid-template-columns: minmax(0, 1fr) 300px; }
}

/* A room reads left to right: the posts, then the chat. The sidebar cards
   (members, who is online, the rules) sit under the chat so the live half of
   the page stays in one column.
   Even halves rather than a narrow sidebar: the chat composer carries two
   tools, a textarea and a send button, and it wraps badly under about 400px. */
.room-layout { display: grid; gap: 1.5rem; grid-template-columns: minmax(0, 1fr); align-items: start; }
@media (min-width: 1000px) {
  .room-layout { grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); }
}

/* A post and its comments, one under the other. A reply is written while
   looking at what it answers, so the two used to sit side by side on a wide
   screen — but that squeezed the article into a narrow column and put the
   comments in a rail that scrolled independently, which read as a second,
   competing page. Down the page in reading order is the shape a comment
   thread already has everywhere else. */
.post-layout { display: grid; gap: 1.75rem; grid-template-columns: minmax(0, 1fr); align-items: start; }

/* The games a room is about. A strip rather than a grid: it is a signpost into
   the showcase, not the main content, so it stays one row high and scrolls
   sideways when a room has a lot of them. */
.game-strip { list-style: none; margin: 0; padding: 0 0 .2rem; display: flex; gap: .6rem; overflow-x: auto; }
.game-strip-item { flex: none; width: 8.5rem; display: flex; flex-direction: column; gap: .2rem; }
.game-strip-item > a { display: flex; flex-direction: column; gap: .2rem; color: inherit; text-decoration: none; }
.game-strip-cover {
  width: 8.5rem; height: 4.8rem; border-radius: var(--radius); object-fit: cover;
  display: flex; align-items: center; justify-content: center;
  font-weight: 700; color: #fff; font-size: 1rem;
}
.game-strip-title { font-size: .85rem; font-weight: 600; }
.game-strip-meta { font-size: .76rem; color: var(--ink-3); }
.game-strip-item .link-button { align-self: flex-start; font-size: .76rem; }

/* ── Top bar ────────────────────────────────────────────────────────────── */

.topbar { background: var(--surface); border-bottom: 1px solid var(--line); position: sticky; top: 0; z-index: 20; }
/* Frosted glass where the browser supports it: the page scrolls underneath a
   translucent bar, which reads lighter than a solid strip. The solid colour
   above is the fallback. */
@supports (backdrop-filter: blur(10px)) or (-webkit-backdrop-filter: blur(10px)) {
  .topbar {
    background: color-mix(in srgb, var(--surface) 78%, transparent);
    -webkit-backdrop-filter: blur(14px) saturate(1.4);
    backdrop-filter: blur(14px) saturate(1.4);
  }
}
/* `flex-wrap` is the no-JavaScript fallback: rather than overflow the viewport
   the controls drop onto a second and third line. `app.js` adds
   `.is-collapsible`, which replaces this with the burger panel below 900px. */
.topbar-inner { display: flex; align-items: center; gap: 1rem; min-height: 60px; flex-wrap: wrap; }
.brand { display: inline-flex; align-items: center; font-family: var(--font-display); font-weight: 700; letter-spacing: -.02em; color: var(--ink); font-size: 1.15rem; }
.brand:hover { text-decoration: none; }
/* The mark is inline SVG so it scales with the header instead of being a fixed
   box; 26px is the optical match for the wordmark's cap height. */
.brand-mark { display: inline-flex; margin-right: .5rem; }
.brand-mark svg { display: block; width: 26px; height: 26px; }
.brand-word { white-space: nowrap; }
.brand-word-tail { color: var(--accent); }
.topbar-panel { display: flex; align-items: center; gap: 1.1rem; flex: 1 1 auto; min-width: 0; }
.nav { display: flex; gap: .25rem; align-items: center; flex-wrap: wrap; }
.nav a { color: var(--ink-2); padding: .4rem .75rem; border-radius: 999px; font-size: .93rem; transition: background .15s ease, color .15s ease; }
.nav a:hover { background: var(--surface-2); color: var(--ink); text-decoration: none; }
.nav a[aria-current="page"] { color: var(--accent); background: var(--accent-soft); font-weight: 600; }
.nav-spacer { flex: 1 1 auto; }
.nav-search { margin: 0; }
.nav-search input { width: 170px; padding: .35rem .6rem; font-size: .88rem; }

/* ── Burger, only once collapsing is active ─────────────────────────────── */
.topbar-burger {
  display: none; align-items: center; justify-content: center; margin-left: auto;
  width: 42px; height: 38px; padding: 0; background: none; color: var(--ink);
  border: 1px solid var(--line); border-radius: var(--radius-s); cursor: pointer; font: inherit;
}
.topbar-burger:hover { background: var(--surface-2); }
.burger-mark, .burger-mark span { display: block; }
.burger-mark { width: 18px; }
.burger-mark span { height: 2px; background: currentColor; border-radius: 1px; }
.burger-mark span + span { margin-top: 4px; }

/* Below 900px the panel becomes a dropdown strip under the bar. Everything is
   scoped to `.is-collapsible` so that a browser without JavaScript keeps the
   wrapped, always-visible layout rather than losing the controls entirely. */
@media (max-width: 899.98px) {
  .topbar.is-collapsible .topbar-inner { flex-wrap: nowrap; }
  .topbar.is-collapsible .topbar-burger { display: inline-flex; }
  .topbar.is-collapsible .topbar-panel {
    display: none;
    position: absolute; top: 100%; left: 0; right: 0;
    flex-direction: column; align-items: stretch; gap: .2rem;
    padding: .5rem 1.25rem 1rem;
    background: var(--surface);
    border-bottom: 1px solid var(--line);
    box-shadow: var(--shadow);
    max-height: calc(100vh - 61px); overflow-y: auto;
    /* The bar is sticky, so a tall panel must scroll rather than push the page. */
    overscroll-behavior: contain;
  }
  .topbar.is-collapsible .topbar-panel.is-open { display: flex; }
  /* Stacked rows read as a list, so each control gets the full width and a
     comfortable tap target instead of shrinking to its label. */
  .topbar.is-collapsible .topbar-panel .nav { flex-direction: column; align-items: stretch; gap: .1rem; }
  .topbar.is-collapsible .topbar-panel .nav a { padding: .6rem .7rem; }
  .topbar.is-collapsible .topbar-panel .nav-spacer { display: none; }
  .topbar.is-collapsible .topbar-panel .nav-search { width: 100%; }
  .topbar.is-collapsible .topbar-panel .nav-search input { width: 100%; }
  .topbar.is-collapsible .topbar-panel details.menu { width: 100%; }
  .topbar.is-collapsible .topbar-panel details.menu > summary { padding: .6rem .7rem; }
  /* A menu opening sideways out of a narrow panel would be clipped by the
     panel's own scroll box, so inside it the panel simply unfolds in place. */
  .topbar.is-collapsible .topbar-panel .menu-panel {
    position: static; min-width: 0; box-shadow: none; border: none;
    border-radius: 0; padding: .15rem 0 .35rem .7rem; margin: 0;
  }
}

.avatar {
  width: 34px; height: 34px; border-radius: 50%; display: inline-flex; align-items: center;
  justify-content: center; font-size: .8rem; font-weight: 700; color: #fff; flex: none;
}
.avatar-lg { width: 52px; height: 52px; font-size: 1.05rem; }

details.menu { position: relative; }
details.menu > summary { list-style: none; cursor: pointer; display: flex; align-items: center; gap: .55rem; padding: .25rem .5rem; border-radius: var(--radius-s); }
details.menu > summary::-webkit-details-marker { display: none; }
details.menu > summary:hover { background: var(--surface-2); }
details.menu[open] > summary { background: var(--surface-2); }
.menu-panel {
  position: absolute; right: 0; top: calc(100% + 8px); min-width: 230px; background: var(--surface);
  border: 1px solid var(--line); border-radius: var(--radius); box-shadow: var(--shadow); padding: .4rem; z-index: 30;
}
.menu-panel a, .menu-panel button {
  display: block; width: 100%; text-align: left; padding: .5rem .65rem; border-radius: var(--radius-s);
  color: var(--ink); font-size: .92rem; background: none; border: none; cursor: pointer; font-family: inherit;
}
.menu-panel a:hover, .menu-panel button:hover { background: var(--surface-2); text-decoration: none; }
.menu-panel hr { margin: .35rem 0; }
.menu-sep { border-top: 1px solid var(--line); margin: .35rem 0; }

.badge-count {
  background: var(--danger); color: #fff; border-radius: 999px; font-size: .7rem; line-height: 1;
  padding: 3px 6px; margin-left: 6px; font-weight: 700;
}

/* ── Buttons ────────────────────────────────────────────────────────────── */

.btn {
  display: inline-flex; align-items: center; justify-content: center; gap: .45rem;
  padding: .55rem 1.05rem; border-radius: 999px; border: 1px solid var(--line-strong);
  background: var(--surface); color: var(--ink); font: inherit; font-size: .92rem; font-weight: 600;
  cursor: pointer; text-decoration: none; white-space: nowrap;
  transition: background .15s ease, border-color .15s ease, box-shadow .15s ease, transform .15s ease;
}
.btn:hover { border-color: var(--ink-3); text-decoration: none; transform: translateY(-1px); }
.btn:active { transform: translateY(0); }
.btn-primary { background: var(--accent); border-color: var(--accent); color: var(--accent-ink); box-shadow: 0 1px 2px rgba(112, 52, 245, .28); }
.btn-primary:hover { filter: brightness(1.07); border-color: var(--accent); box-shadow: 0 4px 14px -4px rgba(112, 52, 245, .45); }
.btn-danger { background: var(--danger); border-color: var(--danger); color: #fff; }
.btn-ghost { background: transparent; border-color: transparent; color: var(--ink-2); }
.btn-ghost:hover { background: var(--surface-2); color: var(--ink); }
.btn-sm { padding: .35rem .75rem; font-size: .85rem; }
.btn-block { width: 100%; }
.btn[disabled], .btn[aria-disabled="true"] { opacity: .55; cursor: not-allowed; transform: none; }

/* ── Forms ──────────────────────────────────────────────────────────────── */

label { display: block; font-size: .88rem; font-weight: 500; margin-bottom: .3rem; }
input[type="text"], input[type="email"], input[type="password"], input[type="url"],
input[type="search"], select, textarea {
  width: 100%; padding: .6rem .8rem; border: 1px solid var(--line-strong); border-radius: var(--radius-s);
  background: var(--surface); color: var(--ink); font: inherit; font-size: .94rem;
  transition: border-color .15s ease, box-shadow .15s ease;
}
textarea { min-height: 130px; resize: vertical; line-height: 1.6; }
input:focus, select:focus, textarea:focus {
  outline: none; border-color: var(--accent);
  box-shadow: 0 0 0 3px color-mix(in srgb, var(--accent) 18%, transparent);
}
input::placeholder, textarea::placeholder { color: var(--ink-3); }
.field { margin-bottom: 1rem; }
.field-note { font-size: .82rem; color: var(--ink-3); margin-top: .3rem; }
.field-error { font-size: .85rem; color: var(--danger); margin-top: .3rem; }
.checkbox { display: flex; gap: .6rem; align-items: flex-start; font-weight: 400; font-size: .9rem; }
.checkbox input { margin-top: .28rem; width: 17px; height: 17px; flex: none; accent-color: var(--accent); }
/* An always-on row is shown, not hidden: the member should be able to see that
   the notice exists and why they cannot switch it off. */
.checkbox.is-locked { color: var(--ink-3); cursor: default; }
.checkbox.is-locked input { cursor: default; }
.toggle-list { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; }
.toggle-list li { padding: .5rem 0; border-bottom: 1px solid var(--line); }
.toggle-list li:last-child { border-bottom: none; padding-bottom: 0; }
.toggle-note { display: block; color: var(--ink-3); font-size: .8rem; margin-top: .15rem; }
.avatar-link { flex: none; display: inline-flex; }
fieldset { border: 1px solid var(--line); border-radius: var(--radius); padding: 1rem 1.1rem; margin: 0 0 1rem; }
legend { font-size: .85rem; font-weight: 600; padding: 0 .4rem; }
.inline-form { display: inline; }

/* The game form's room picker. Two groups — rooms you run, and rooms that have
   to approve you — each a scrolling list, because there can be a hundred rooms
   and the form is already long. */
.room-picker { border: none; border-left: 3px solid var(--line); border-radius: 0; padding: 0 0 0 .8rem; margin: 0 0 .9rem; }
.room-picker legend { padding: 0 0 .4rem; }
.room-picker .checkbox { padding: .18rem 0; }
.room-picker-list { display: flex; flex-direction: column; max-height: 15rem; overflow-y: auto; }
.room-picker .checkbox input:disabled + span { color: var(--ink-3); }

/* ── Surfaces ───────────────────────────────────────────────────────────── */

.card { background: var(--surface); border: 1px solid var(--line); border-radius: var(--radius-l); padding: 1.25rem; }
.card-tight { padding: .9rem 1rem; }
.card-hover { transition: border-color .18s ease, box-shadow .18s ease, transform .18s ease; }
.card-hover:hover { border-color: var(--line-strong); box-shadow: var(--shadow-lift); transform: translateY(-2px); }
.card-head { display: flex; justify-content: space-between; align-items: baseline; gap: 1rem; margin-bottom: .75rem; }
.card-head h3, .card-head h2 { margin: 0; }

.callout { border: 1px solid var(--line); border-left: 4px solid var(--accent); background: var(--surface); border-radius: var(--radius); padding: .9rem 1.1rem; }
.callout-warn { border-left-color: var(--warn); background: var(--warn-soft); }
.callout-danger { border-left-color: var(--danger); background: var(--danger-soft); }
.callout-ok { border-left-color: var(--ok); background: var(--ok-soft); }
.callout p:last-child { margin-bottom: 0; }

.pill {
  display: inline-block; font-size: .76rem; font-weight: 600; padding: .15rem .5rem; border-radius: 999px;
  border: 1px solid var(--line-strong); color: var(--ink-2); background: var(--surface-2); white-space: nowrap;
}
.pill-accent { background: var(--accent-soft); border-color: var(--accent-line); color: var(--accent); }
.pill-ok { background: var(--ok-soft); border-color: transparent; color: var(--ok); }
.pill-warn { background: var(--warn-soft); border-color: transparent; color: var(--warn); }
.pill-danger { background: var(--danger-soft); border-color: transparent; color: var(--danger); }

.empty { text-align: center; padding: 2.5rem 1rem; color: var(--ink-3); border: 1px dashed var(--line-strong); border-radius: var(--radius-l); }

.flash { display: flex; gap: .75rem; align-items: flex-start; padding: .75rem 1rem; border-radius: var(--radius); margin-bottom: 1rem; border: 1px solid var(--line); font-size: .93rem; transition: opacity .2s ease; }
/* An advisory notice fades out on its own after a few seconds (see app.js).
   The class is applied for the length of that fade and then the node is gone. */
.flash.is-leaving { opacity: 0; }
.flash-info { background: var(--accent-soft); border-color: var(--accent-line); }
.flash-success { background: var(--ok-soft); border-color: transparent; color: var(--ok); }
.flash-warning { background: var(--warn-soft); border-color: transparent; color: var(--warn); }
.flash-error { background: var(--danger-soft); border-color: transparent; color: var(--danger); }
.flash form { margin-left: auto; }

/* ── Hero ───────────────────────────────────────────────────────────────── */

/* The hero is a badge and a headline and nothing else — the copy that used to
   sit under it moved into the page body. There is no lede, no action row and no
   feature grid here any more, so `margin-bottom` on the heading would only add
   space before the section's own padding. */
.hero { position: relative; overflow: hidden; background: var(--surface); background-image: var(--wash); border-bottom: 1px solid var(--line); padding: 3.5rem 0 3rem; }
.hero h1 { font-size: clamp(1.9rem, 4.4vw, 2.8rem); max-width: 22ch; margin-bottom: 0; }
/* Who the place is for — a quiet row of tags standing in for the old eyebrow. */
.hero-tags { display: flex; flex-wrap: wrap; gap: .4rem; margin: 0 0 1rem; }

/* Covers from the showcase, washed into the front door. Decorative only: the
   tiles are `aria-hidden`, take no pointer events, and sit at low opacity
   behind a blur, so the headline keeps its contrast whatever pictures happen to
   be newest. The mask fades them out towards the bottom, which is what makes
   them read as part of the surface rather than as a row of images — a hard edge
   there would be a strip of pictures the page then abruptly stops showing.

   They are the 640px thumbnails, not the originals: four full-size covers at
   the top of the busiest page would cost more than the whole rest of it. */
.hero-art {
  position: absolute; inset: 0; display: flex; gap: 0;
  opacity: .2; filter: blur(22px) saturate(1.05); pointer-events: none;
  -webkit-mask-image: var(--hero-fade); mask-image: var(--hero-fade);
}
/* The tiles are scaled past their own slot and so overlap their neighbours.
   With a gap between them the page background shows through as a stripe, and
   butted up edge to edge each picture's darkened border meets the next one as a
   visible vertical band — either way the layer reads as four rectangles rather
   than as artwork blended into the page. Overlapping gives the blur a
   continuous surface to work on. No clipping, deliberately: overflow:hidden on
   a tile would restore the hard edge this is here to remove. */
.hero-art-tile { flex: 1 1 0; min-width: 0; }
.hero-art-tile img {
  display: block; width: 100%; height: 100%; object-fit: cover;
  transform: scale(1.3);
}
/* The content, painted above the pictures. An absolutely positioned sibling
   outranks the flow content that follows it, so the wrapper has to be
   positioned for the two to be ordered the way they read. */
.hero > .wrap { position: relative; }

/* ── Lobby / chat ───────────────────────────────────────────────────────── */

.chat-shell { display: grid; gap: 1.25rem; grid-template-columns: minmax(0, 1fr); }
@media (min-width: 960px) { .chat-shell { grid-template-columns: minmax(0, 1fr) 280px; } }

.chat { display: flex; flex-direction: column; background: var(--surface); border: 1px solid var(--line); border-radius: var(--radius-l); overflow: hidden; height: min(calc(70vh - var(--banner-h, 0px)), 620px); }
/* The lobby is the one page whose chat *is* the page, so it gets the height to
   match: the sidebar there is short, and a 620px chat under a short sidebar
   left a third of the screen empty. The 220px is the header and page head above
   it, and --banner-h (set by app.js when the cookie banner is up) is the strip
   fixed over the bottom of the viewport — leave either out and the compose row
   slides off the bottom of the screen, or under the banner, where its buttons
   stop answering. The room page keeps the smaller size — there the chat sits
   beside a long column of posts. */
.chat-shell > .chat { height: clamp(620px, calc(100vh - 220px - var(--banner-h, 0px)), 920px); }
.chat-head { display: flex; align-items: center; gap: .75rem; padding: .8rem 1rem; border-bottom: 1px solid var(--line); background: var(--surface); }
.chat-head h2 { margin: 0; font-size: 1rem; }
.presence { margin-left: auto; font-size: .83rem; color: var(--ink-3); display: flex; align-items: center; gap: .4rem; }
.dot { width: 8px; height: 8px; border-radius: 50%; background: var(--ok); display: inline-block; }
.chat-log { flex: 1; overflow-y: auto; padding: 1rem; display: flex; flex-direction: column; gap: .85rem; }
.chat-msg { display: flex; gap: .65rem; }
.chat-msg-body { min-width: 0; }
.chat-msg-meta { display: flex; gap: .5rem; align-items: baseline; font-size: .8rem; color: var(--ink-3); margin-bottom: .12rem; }
/* The server renders the author as <a><strong>name</strong></a> while the live
   client builds a bare <a>name</a>, so the anchor itself has to carry the dark,
   semibold look or the two paths disagree the moment a message arrives. */
.chat-author { color: var(--ink); font-weight: 600; font-size: .88rem; }
.chat-author:hover { color: var(--accent); text-decoration: none; }
.chat-msg-meta strong { color: var(--ink); font-size: .88rem; font-weight: 600; }
.chat-msg-text { white-space: pre-wrap; overflow-wrap: anywhere; font-size: .94rem; }
.chat-msg-actions { opacity: 0; transition: opacity .12s ease; }
.chat-msg:hover .chat-msg-actions, .chat-msg:focus-within .chat-msg-actions { opacity: 1; }
.chat-msg-actions button { background: none; border: none; color: var(--ink-3); font-size: .78rem; cursor: pointer; padding: 0; font-family: inherit; }
.chat-msg-actions button:hover { color: var(--danger); text-decoration: underline; }
.chat-day { text-align: center; font-size: .78rem; color: var(--ink-3); margin: .5rem 0; }
.chat-compose { border-top: 1px solid var(--line); padding: .75rem; display: flex; flex-direction: column; gap: .55rem; background: var(--surface); }
.chat-input-row { display: flex; gap: .5rem; align-items: flex-end; }
/* Two lines tall by default: the placeholder ("Write a message. Enter sends,
   Shift+Enter adds a line.") wraps once the chat sits in a half-width column,
   and a one-line box clipped the second line rather than showing it. Chat.js
   still grows and shrinks the box from here. */
.chat-input-row textarea { flex: 1; min-height: 3.5rem; max-height: 140px; }
.chat-tool {
  flex: none; width: 38px; height: 38px; display: inline-flex; align-items: center; justify-content: center;
  background: var(--surface-2); border: 1px solid var(--line); border-radius: var(--radius-s);
  color: var(--ink-2); cursor: pointer; font-size: 1.05rem; padding: 0;
}
.chat-tool:hover { background: var(--accent-soft); border-color: var(--accent-line); color: var(--accent-deep); }
.chat-tool:disabled { opacity: .5; cursor: progress; }
.chat-tool[aria-expanded="true"] { background: var(--accent-soft); border-color: var(--accent-line); color: var(--accent-deep); }

/* The chosen-but-not-yet-sent picture. */
.chat-attach { position: relative; display: inline-flex; align-items: center; gap: .5rem; align-self: flex-start; }
/* `display: inline-flex` above outranks the user agent's `[hidden] { display:
   none }`, so the strip stayed in the layout when it had nothing in it —
   leaving the remove button floating on its own above the composer. */
.chat-attach[hidden] { display: none; }
.chat-attach img { max-height: 76px; max-width: 140px; border-radius: var(--radius-s); border: 1px solid var(--line); display: block; }
.chat-attach-remove {
  position: absolute; top: -7px; right: -7px; width: 22px; height: 22px; padding: 0;
  display: inline-flex; align-items: center; justify-content: center;
  background: var(--surface); border: 1px solid var(--line-strong); border-radius: 50%;
  color: var(--ink-2); cursor: pointer; font-size: .8rem;
}
.chat-attach-remove:hover { color: var(--danger); border-color: var(--danger); }
.chat-attach-progress { font-size: .78rem; color: var(--ink-3); }

/* "Someone is typing". Sits above the input row and takes no space when it has
   nothing to say, so the composer does not jump every time it appears. */
.chat-typing { font-size: .78rem; color: var(--ink-3); }
.chat-typing[hidden] { display: none; }
/* A file dragged over the chat: the whole widget is the drop target, so the
   whole widget says so. */
.chat.is-dropping { outline: 2px dashed var(--accent-line); outline-offset: -3px; }

/* The emoji grid. */
.chat-emoji {
  padding: .4rem; border: 1px solid var(--line); border-radius: var(--radius);
  background: var(--surface-2); max-height: 190px; overflow-y: auto;
}
.chat-emoji-btn {
  background: none; border: none; cursor: pointer; font-size: 1.25rem; line-height: 1;
  padding: .22rem; border-radius: var(--radius-s); font-family: inherit;
}
.chat-emoji-btn:hover { background: var(--surface); }

/* ── Emoji picker (shared: chat, direct messages, editor) ───────────────── */
/* Groups of a fixed grid inside a scrolling box. One flat grid of this many
   characters is unusable; the headings are what make it findable. */
.emoji-group { margin-bottom: .3rem; }
.emoji-group:last-child { margin-bottom: 0; }
.emoji-group-title {
  margin: 0 0 .15rem; padding: 0 .25rem;
  font-size: .66rem; letter-spacing: .06em; text-transform: uppercase;
  color: var(--muted); font-weight: 600;
}
.emoji-grid { display: grid; grid-template-columns: repeat(var(--emoji-cols, 10), 1fr); gap: .1rem; }

/* A picture in the conversation. */
.chat-msg-image { display: block; margin-top: .35rem; width: fit-content; max-width: 100%; }
.chat-msg-image img {
  display: block; max-width: min(320px, 100%); max-height: 280px; width: auto; height: auto;
  border-radius: var(--radius); border: 1px solid var(--line); background: var(--surface-2);
}
.chat-msg-image:hover img { border-color: var(--accent-line); }

/* ── Rich text editor ───────────────────────────────────────────────────── */

.editor { border: 1px solid var(--line-strong); border-radius: var(--radius); background: var(--surface); overflow: hidden; }
.editor:focus-within { border-color: var(--accent); box-shadow: 0 0 0 3px var(--accent-soft); }

.editor-bar {
  display: flex; flex-wrap: wrap; gap: .1rem; align-items: center;
  padding: .3rem .35rem; border-bottom: 1px solid var(--line); background: var(--surface-2); position: relative;
}
.editor-btn {
  width: 30px; height: 30px; padding: 0; display: inline-flex; align-items: center; justify-content: center;
  background: none; border: 1px solid transparent; border-radius: var(--radius-s);
  color: var(--ink-2); cursor: pointer;
}
.editor-btn:hover { background: var(--surface); border-color: var(--line); color: var(--ink); }
.editor-btn:disabled { opacity: .5; cursor: progress; }
.editor-icon { width: 16px; height: 16px; fill: none; stroke: currentColor; stroke-width: 1.7; stroke-linecap: round; stroke-linejoin: round; }

.editor-surface {
  min-height: 180px; max-height: 60vh; overflow-y: auto;
  padding: .8rem .9rem; font-size: .96rem; line-height: 1.6; outline: none;
}
/* The hint has to survive the surface being non-empty but blank: an empty
   contenteditable is `<p><br></p>`, so `:empty` alone never matched and the
   placeholder never appeared. `:has()` catches that shape; where it is not
   supported the field simply shows no hint, which is where it started. */
.editor-surface:empty::before,
.editor-surface:has(> p:only-child > br:only-child):not(:focus)::before {
  content: attr(data-placeholder); color: var(--ink-3);
}
.editor-surface p { margin: 0 0 .7rem; }
.editor-surface p:last-child { margin-bottom: 0; }
.editor-surface h2, .editor-surface h3 { margin: .9rem 0 .45rem; font-family: var(--font-display); }
.editor-surface blockquote { margin: .6rem 0; padding-left: .85rem; border-left: 3px solid var(--accent-line); color: var(--ink-2); }
.editor-surface pre { background: var(--surface-2); padding: .6rem .75rem; border-radius: var(--radius-s); overflow-x: auto; font-family: var(--mono); font-size: .87rem; }
.editor-surface ul, .editor-surface ol { margin: .4rem 0; padding-left: 1.4rem; }
.editor-surface img { max-width: 100%; height: auto; border-radius: var(--radius-s); }

.editor-emoji {
  position: absolute; top: calc(100% + 4px); left: .35rem; z-index: 20;
  --emoji-cols: 8;
  padding: .4rem; width: min(300px, 80vw); max-height: 240px; overflow-y: auto;
  border: 1px solid var(--line-strong); border-radius: var(--radius);
  background: var(--surface); box-shadow: var(--shadow-lift);
}
.editor-emoji-btn { background: none; border: none; cursor: pointer; font-size: 1.2rem; line-height: 1; padding: .2rem; border-radius: var(--radius-s); font-family: inherit; }
.editor-emoji-btn:hover { background: var(--surface-2); }

/* Rich bodies: the block elements carry their own layout, so browser whitespace
   collapsing is what is wanted here. Plain bodies keep `pre-wrap` above, which
   is how a textarea's line breaks have always been preserved. */
.prose-rich { white-space: normal; }
.prose-rich p { margin: 0 0 .75rem; }
.prose-rich p:last-child { margin-bottom: 0; }
.prose-rich h2, .prose-rich h3 { margin: 1.1rem 0 .5rem; font-family: var(--font-display); line-height: 1.3; }
.prose-rich blockquote { margin: .8rem 0; padding-left: .9rem; border-left: 3px solid var(--accent-line); color: var(--ink-2); }
.prose-rich ul, .prose-rich ol { margin: .6rem 0; padding-left: 1.5rem; }
.prose-rich li { margin: .2rem 0; }
.prose-rich pre { background: var(--surface-2); padding: .7rem .85rem; border-radius: var(--radius-s); overflow-x: auto; font-family: var(--mono); font-size: .88rem; white-space: pre; }
.prose-rich code { font-family: var(--mono); font-size: .88em; background: var(--surface-2); padding: .1em .35em; border-radius: 4px; }
.prose-rich pre code { background: none; padding: 0; }
.prose-rich img { max-width: 100%; height: auto; border-radius: var(--radius); border: 1px solid var(--line); margin: .5rem 0; display: block; }
.prose-rich hr { border: none; border-top: 1px solid var(--line); margin: 1.2rem 0; }
.prose-rich a { color: var(--accent-deep); text-decoration: underline; }
.chat-status { font-size: .8rem; color: var(--ink-3); padding: 0 1rem .5rem; min-height: 1.2em; }
.chat-status[data-state="error"] { color: var(--danger); }
.chat-status[data-state="ok"] { color: var(--ok); }

.member-list { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: .4rem; }
.member-list li { display: flex; align-items: center; gap: .5rem; font-size: .9rem; }
/* Promote / demote / remove share one slot on the right of a roster row. They
   only appear for the people who may actually use them, so the row is a plain
   flex line until then. */
.member-actions { display: flex; align-items: center; gap: .6rem; flex-wrap: wrap; justify-content: flex-end; }
.member-actions .inline-form { flex: none; }

/* ── Rooms / posts ──────────────────────────────────────────────────────── */

.room-card { display: flex; flex-direction: column; gap: .6rem; }
.room-card h3 { margin: 0; }
.room-card h3 a { color: var(--ink); }
.room-meta { display: flex; gap: .75rem; flex-wrap: wrap; font-size: .83rem; color: var(--ink-3); align-items: center; }

.post-list { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: .75rem; }
.post-row { background: var(--surface); border: 1px solid var(--line); border-radius: var(--radius); padding: 1rem 1.1rem; }
.post-row h3 { margin: 0 0 .3rem; font-size: 1.02rem; }
.post-row h3 a { color: var(--ink); }
.post-excerpt { color: var(--ink-2); font-size: .92rem; margin: 0 0 .5rem; }
.meta-line { font-size: .82rem; color: var(--ink-3); display: flex; gap: .75rem; flex-wrap: wrap; align-items: center; }

.prose { white-space: pre-wrap; overflow-wrap: anywhere; }
.prose p:last-child { margin-bottom: 0; }

.comment { border-top: 1px solid var(--line); padding: 1rem 0; }
.comment:first-of-type { border-top: none; }
.comment-head { display: flex; gap: .6rem; align-items: center; margin-bottom: .4rem; font-size: .88rem; }
.comment-head strong { font-weight: 600; }
.comment-head time { color: var(--ink-3); font-size: .8rem; }

/* ── Games ──────────────────────────────────────────────────────────────── */

.game-card {
  padding: 0; overflow: hidden; display: flex; flex-direction: column;
  transition: transform .18s ease, box-shadow .18s ease, border-color .18s ease;
}
.game-card:hover { transform: translateY(-3px); box-shadow: var(--shadow-lift); }
.game-card .game-thumb { border: none; border-bottom: 1px solid var(--line); border-radius: 0; }
.game-card h3 { margin: 0; }
.game-card p { margin: 0; color: var(--ink-2); font-size: .91rem; }
/* The follow button turns green once the member is following. The rule used to
   be scoped to a .follow-form wrapper that the markup never had, so the state
   change was invisible. */
button.is-following { color: var(--ok); border-color: var(--ok); }

/* A cover image, or a coloured panel standing in for one. The stand-in is a
   hash of the title, so it can be any hue at all — the overlay is what keeps
   the white title legible on a pale one. */
.game-thumb {
  display: block; aspect-ratio: 16 / 9; border-radius: var(--radius-l); overflow: hidden;
  border: 1px solid var(--line); background: var(--surface-2);
}
.game-thumb img { width: 100%; height: 100%; object-fit: cover; display: block; }
.game-thumb-fallback {
  position: relative; display: grid; place-items: center; width: 100%; height: 100%;
  padding: 1rem; text-align: center; color: #fff;
  font-weight: 700; letter-spacing: -.02em; font-size: 1.05rem; text-shadow: 0 1px 2px rgba(0, 0, 0, .35);
  background-image: linear-gradient(155deg, rgba(12, 9, 31, .18) 0%, rgba(12, 9, 31, .58) 100%);
}
.game-thumb-fallback { font-size: .95rem; padding: .6rem; }

.game-hero { margin: 1rem 0 1.4rem; }
.game-hero-image {
  display: block; width: 100%; aspect-ratio: 21 / 7; min-height: 160px; object-fit: cover;
  border-radius: var(--radius-l); border: 1px solid var(--line); overflow: hidden;
}

/* ── Tables ─────────────────────────────────────────────────────────────── */

.table-scroll { overflow-x: auto; }
table.data { width: 100%; border-collapse: collapse; font-size: .9rem; }
table.data th, table.data td { text-align: left; padding: .6rem .7rem; border-bottom: 1px solid var(--line); vertical-align: top; }
table.data th { font-size: .78rem; text-transform: uppercase; letter-spacing: .05em; color: var(--ink-3); font-weight: 600; }

/* ── Legal documents ────────────────────────────────────────────────────── */

.doc { max-width: 74ch; }
.doc h2 { margin-top: 2.25rem; }
.doc h3 { margin-top: 1.6rem; }
.doc ul, .doc ol { padding-left: 1.35rem; }
.doc li { margin: .35rem 0; }
.doc table { width: 100%; border-collapse: collapse; margin: 1rem 0; font-size: .89rem; }
.doc table th, .doc table td { border: 1px solid var(--line); padding: .55rem .7rem; text-align: left; vertical-align: top; }
.doc table th { background: var(--surface-2); font-weight: 600; }
.doc .meta { color: var(--ink-3); font-size: .87rem; }
.doc-toc { list-style: none; padding: 0; margin: 0; display: grid; gap: .3rem; }
.doc-toc a { font-size: .92rem; }

/* ── Cookie banner ──────────────────────────────────────────────────────── */

.cookie-banner {
  position: fixed; left: 0; right: 0; bottom: 0; z-index: 60;
  background: var(--surface); border-top: 1px solid var(--line-strong); box-shadow: var(--shadow); padding: 1rem 0;
}
.cookie-inner { display: flex; gap: 1rem; align-items: center; flex-wrap: wrap; }
.cookie-inner p { margin: 0; font-size: .9rem; color: var(--ink-2); flex: 1 1 320px; }

/* ── Footer ─────────────────────────────────────────────────────────────── */

.site-footer { border-top: 1px solid var(--line); background: var(--surface); padding: 2.25rem 0 2rem; font-size: .88rem; }
.footer-cols { display: grid; gap: 1.75rem; grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); }
.footer-cols h4 { font-size: .8rem; text-transform: uppercase; letter-spacing: .06em; color: var(--ink-3); margin-bottom: .6rem; }
.footer-cols ul { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: .35rem; }
.footer-cols a { color: var(--ink-2); }
.footer-bottom { margin-top: 1.75rem; padding-top: 1.25rem; border-top: 1px solid var(--line); color: var(--ink-3); font-size: .82rem; display: flex; gap: 1rem; flex-wrap: wrap; justify-content: space-between; }

/* ── Market ─────────────────────────────────────────────────────────────── */

.market-grid { display: grid; gap: 1.25rem; grid-template-columns: repeat(auto-fill, minmax(248px, 1fr)); }

.market-card {
  padding: 0; overflow: hidden; display: flex; flex-direction: column;
  transition: transform .18s ease, box-shadow .18s ease, border-color .18s ease;
}
.market-card:hover { transform: translateY(-3px); box-shadow: var(--shadow-lift); }

.market-thumb {
  position: relative; aspect-ratio: 16 / 10; width: 100%; overflow: hidden;
  background: var(--surface-2); display: grid; place-items: center;
}
.market-thumb img { width: 100%; height: 100%; object-fit: cover; display: block; transition: transform .5s ease; }
.market-card:hover .market-thumb img { transform: scale(1.04); }
.market-thumb-fallback { display: grid; place-items: center; color: var(--ink-3); }
.market-thumb-fallback .icon-mark { width: 1.9rem; height: 1.9rem; }

/* A listing as a search result: a square thumbnail beside the text. The grid
   card's 16:10 cover would make one result taller than the other four. */
.market-row { display: flex; gap: .9rem; align-items: flex-start; }
.market-thumb-sm {
  flex: none; width: 76px; aspect-ratio: 1 / 1;
  border: 1px solid var(--line); border-radius: var(--radius-s);
}
.market-thumb-sm .icon-mark { width: 1.5rem; height: 1.5rem; }
.market-row-body { flex: 1; min-width: 0; }
.market-row-body h3 { margin: 0 0 .3rem; font-size: 1.02rem; }
.price { font-weight: 700; color: var(--ink); }

/* A member in search results. */
.person-card { display: flex; gap: .75rem; align-items: flex-start; }
.person-body { min-width: 0; }
.person-body h3 { margin: 0; font-size: 1rem; }
.person-body .inline-form { margin-top: .5rem; }

/* ── Activity feed ──────────────────────────────────────────────────────── */
/* A dated list, not a card grid: entries are short and read as a timeline. */
.activity-list { list-style: none; margin: 0; padding: 0; }
.activity-item {
  display: flex;
  gap: .7rem;
  align-items: flex-start;
  padding: .85rem 0;
  border-bottom: 1px solid var(--line);
}
.activity-item:last-child { border-bottom: none; padding-bottom: 0; }
.activity-body { min-width: 0; flex: 1; }
.activity-body a { color: var(--ink); }

.market-body { display: flex; flex-direction: column; gap: .4rem; padding: .95rem 1.05rem 1.1rem; flex: 1; }
.market-body h3 { margin: 0; font-size: 1.05rem; }
.market-body h3 a { color: var(--ink); }
.market-body p { margin: 0; color: var(--ink-2); font-size: .9rem; }
.market-price { font-family: var(--font-display); font-weight: 700; font-size: 1.08rem; color: var(--accent); letter-spacing: -.01em; }
.market-price.is-free { color: var(--ok); }
.market-foot { margin-top: auto; padding-top: .55rem; font-size: .82rem; color: var(--ink-3); display: flex; align-items: center; gap: .5rem; flex-wrap: wrap; }
.market-tags { display: flex; flex-wrap: wrap; gap: .35rem; }

.market-hero { aspect-ratio: 16 / 9; border-radius: var(--radius-l); overflow: hidden; background: var(--surface-2); border: 1px solid var(--line); display: grid; place-items: center; }
.market-hero img { width: 100%; height: 100%; object-fit: cover; display: block; }

.gallery { display: grid; gap: .6rem; grid-template-columns: repeat(auto-fill, minmax(120px, 1fr)); }
.gallery-item { position: relative; aspect-ratio: 1; border-radius: var(--radius); overflow: hidden; border: 1px solid var(--line); background: var(--surface-2); }
.gallery-item img { width: 100%; height: 100%; object-fit: cover; display: block; }

/* ── Uploads ────────────────────────────────────────────────────────────── */

.dropzone {
  border: 1.5px dashed var(--line-strong); border-radius: var(--radius-l); background: var(--surface-2);
  padding: 1.1rem 1rem; text-align: center;
}
.dropzone .dropzone-mark { display: block; width: 1.9rem; height: 1.9rem; margin: 0 auto .4rem; color: var(--accent); opacity: .8; }
.dropzone .dropzone-mark .icon-mark { width: 100%; height: 100%; }
.dropzone p { margin: .3rem 0 0; font-size: .83rem; color: var(--ink-3); }

input[type="file"] {
  width: 100%; font: inherit; font-size: .88rem; color: var(--ink);
  padding: .45rem; border: 1px dashed var(--line-strong); border-radius: var(--radius); background: var(--surface);
}
input[type="file"]::file-selector-button {
  font: inherit; font-size: .85rem; margin-right: .7rem; padding: .35rem .7rem; cursor: pointer;
  border: 1px solid var(--line-strong); border-radius: var(--radius-s); background: var(--surface-2); color: var(--ink);
}
input[type="file"]:focus-visible { outline: 3px solid var(--accent); outline-offset: 1px; }

.media-strip { display: flex; gap: .6rem; flex-wrap: wrap; }
.media-thumb { width: 88px; height: 88px; border-radius: var(--radius); overflow: hidden; border: 1px solid var(--line); background: var(--surface-2); display: grid; place-items: center; }
.media-thumb img { width: 100%; height: 100%; object-fit: cover; display: block; }
.media-thumb video { width: 100%; height: 100%; object-fit: cover; display: block; }
video.media-video { width: 100%; max-height: 460px; border-radius: var(--radius-l); background: #000; display: block; }
img.media-image { max-width: 100%; border-radius: var(--radius-l); display: block; border: 1px solid var(--line); }

.quota-bar { height: 8px; border-radius: 999px; background: var(--surface-2); border: 1px solid var(--line); overflow: hidden; }
.quota-bar span { display: block; height: 100%; background: var(--accent); }

/* ── Direct messages ────────────────────────────────────────────────────── */

.dm-shell { display: grid; gap: 1rem; grid-template-columns: minmax(0, 1fr); align-items: start; }
@media (min-width: 900px) { .dm-shell { grid-template-columns: 17rem minmax(0, 1fr); } }

.dm-list { list-style: none; margin: 0; padding: 0; background: var(--surface); border: 1px solid var(--line); border-radius: var(--radius-l); overflow: hidden; }
.dm-list li + li { border-top: 1px solid var(--line); }
.dm-list a { display: flex; gap: .6rem; align-items: center; padding: .7rem .85rem; color: var(--ink); }
.dm-list a:hover { background: var(--surface-2); text-decoration: none; }
.dm-list a[aria-current="page"] { background: var(--accent-soft); }
.dm-min { min-width: 0; flex: 1; }
.dm-name { display: block; font-weight: 600; font-size: .9rem; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.dm-preview { display: block; font-size: .8rem; color: var(--ink-3); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

.dm-thread { background: var(--surface); border: 1px solid var(--line); border-radius: var(--radius-l); display: flex; flex-direction: column; height: min(68vh, 620px); overflow: hidden; }
.dm-thread-head { display: flex; align-items: center; gap: .7rem; padding: .75rem 1rem; border-bottom: 1px solid var(--line); }
.dm-thread-head h2 { margin: 0; font-size: 1rem; }
.dm-log { flex: 1; overflow-y: auto; padding: 1rem; display: flex; flex-direction: column; gap: .55rem; }
.dm-row { display: flex; }
.dm-row-mine { justify-content: flex-end; }
.dm-bubble { max-width: 78%; padding: .55rem .85rem; border-radius: 16px; background: var(--surface-2); white-space: pre-wrap; overflow-wrap: anywhere; font-size: .93rem; }
.dm-row-mine .dm-bubble { background: var(--accent); color: var(--accent-ink); }
.dm-time { display: block; font-size: .74rem; color: var(--ink-3); margin-top: .2rem; }
.dm-row-mine .dm-time { color: var(--accent-ink); opacity: .82; }
/* A picture keeps its own frame, so the bubble tightens around it rather than
   padding it like a paragraph. */
.dm-bubble.has-image { padding: .3rem .3rem .25rem; }
.dm-image { display: block; }
.dm-image img {
  display: block; max-width: min(280px, 100%); max-height: 240px; width: auto; height: auto;
  border-radius: 12px; border: 1px solid var(--line); background: var(--surface-2);
}
.dm-bubble.has-image .dm-time { padding: 0 .35rem; }
.dm-compose { border-top: 1px solid var(--line); padding: .75rem; display: flex; flex-direction: column; gap: .55rem; background: var(--surface); }
.dm-input-row { display: flex; gap: .5rem; align-items: flex-end; }
.dm-input-row textarea { flex: 1; min-height: 46px; max-height: 160px; }
.dm-compose .field-note:empty { display: none; }
/* Sits where the composer would be, so the thread does not appear to lose its
   bottom edge when messaging is closed. */
.dm-blocked-note { margin: 0; padding: .9rem 1rem; border-top: 1px solid var(--line); }

/* ── Page furniture ─────────────────────────────────────────────────────── */

.pill-neon { background: var(--neon-soft); border-color: var(--neon-line); color: var(--neon); }

/* ── Empty states ───────────────────────────────────────────────────────── */

.empty-state {
  padding: 2rem 1.25rem; text-align: center;
  border: 1px dashed var(--line-strong); border-radius: var(--radius-l); background: var(--surface-2);
}
.empty-state p { margin: .5rem 0 0; color: var(--ink-2); }
.empty-mark, .empty-icon {
  display: block; width: 2.2rem; height: 2.2rem; margin: 0 auto .6rem;
  color: var(--accent); opacity: .85;
}
.empty-mark .icon-mark, .empty-icon .icon-mark { width: 100%; height: 100%; }

.section-head { display: flex; flex-wrap: wrap; gap: .75rem; align-items: baseline; justify-content: space-between; margin-bottom: .85rem; }
.section-head h2 { margin: 0; }
.filter-bar { display: flex; flex-wrap: wrap; gap: .4rem; align-items: center; }
.filter-bar .btn { border-radius: 999px; padding: .3rem .75rem; font-size: .84rem; }
/* The search field is one control among the filter rows, not a banner of its
   own: a capped width keeps it on one line with its button instead of
   stretching across the page. */
.filter-bar input[type="search"] { width: auto; flex: 0 1 17rem; min-width: 9rem; max-width: 17rem; padding: .35rem .65rem; font-size: .85rem; }

/* ── Tabs on a profile ──────────────────────────────────────────────────── */

.tabs {
  display: flex; gap: .2rem; flex-wrap: wrap; align-items: center;
  border-bottom: 1px solid var(--line); margin: 1.25rem 0 1.4rem;
}
.tab {
  position: relative; padding: .55rem .8rem .6rem; color: var(--ink-2); font-size: .92rem; font-weight: 500;
  border-bottom: 2px solid transparent; margin-bottom: -1px;
}
.tab:hover { color: var(--ink); text-decoration: none; }
.tab.is-on { color: var(--accent); border-bottom-color: var(--accent); font-weight: 600; }

/* ── Small avatars and presence chips ───────────────────────────────────── */

.avatar-xs { width: 20px; height: 20px; font-size: .58rem; }
img.avatar, img.avatar-xs, img.avatar-lg, img.avatar-xl { object-fit: cover; display: block; }

/* Chip list of who is in the channel. It used to be a full-width strip bolted
   under the chat header, which is why it carried its own padding, rule and
   background; it now lives inside the sidebar card, so it is just a wrapping
   row of chips and inherits the card's surface. */
.chat-roster { display: flex; flex-wrap: wrap; gap: .35rem; align-items: center; margin-top: .6rem; }
.presence-chip {
  display: inline-flex; align-items: center; gap: .35rem; max-width: 12rem;
  padding: .18rem .55rem .18rem .25rem; border-radius: 999px;
  border: 1px solid var(--line); background: var(--surface); color: var(--ink);
  font-size: .82rem; line-height: 1.5; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.presence-chip:hover { text-decoration: none; border-color: var(--accent-line); background: var(--accent-soft); }
.chat-roster-empty { font-size: .82rem; color: var(--ink-3); }

/* ── Profile card ───────────────────────────────────────────────────────── */

.user-card {
  position: fixed; z-index: 60; width: 300px; max-width: calc(100vw - 16px);
  padding: .85rem; border-radius: var(--radius-l);
  background: var(--surface); border: 1px solid var(--line); box-shadow: var(--shadow);
}
.user-card-head { display: flex; gap: .65rem; align-items: center; margin-bottom: .5rem; }
.user-card-head strong { display: block; font-size: .98rem; }
.user-card p { margin: .35rem 0; }
.user-card .row { margin-top: .6rem; }

/* ── Progress and stat lines ────────────────────────────────────────────── */

.progress { height: 6px; border-radius: 999px; background: var(--surface-2); border: 1px solid var(--line); overflow: hidden; }
.progress span { display: block; height: 100%; background: var(--gradient-brand, var(--accent)); }
.progress-lg { height: 9px; }
.stat-row { display: flex; flex-wrap: wrap; gap: .4rem 1.1rem; font-size: .84rem; color: var(--ink-2); }
.stat-row strong { color: var(--ink); }

/* ── Admin overview ─────────────────────────────────────────────────────── */

.metric { display: flex; flex-direction: column; gap: .1rem; }
.metric-value { font-family: var(--font-display); font-size: 1.55rem; font-weight: 700; line-height: 1.15; }
.metric-label { font-size: .85rem; font-weight: 600; color: var(--ink-2); }

/* A fourteen-bar column chart drawn with flexbox — the numbers are small,
   static and already aggregated, so pulling in a charting library to render
   fourteen divs would cost more than the whole page. */
.chart-bars { display: flex; align-items: flex-end; gap: 3px; height: 72px; }
.chart-bar {
  flex: 1 1 0; min-width: 0; min-height: 2px; background: var(--accent);
  border-radius: 2px 2px 0 0;
}
/* A day with nothing in it still gets a bar, so a gap reads as a zero rather
   than as a missing column the eye skips over. */
.chart-bar.is-empty { background: var(--line); }
.chart-foot { display: flex; justify-content: space-between; font-size: .78rem; color: var(--ink-3); margin: .4rem 0 0; }

.plain-list { list-style: none; margin: 0; padding: 0; }
.plain-list li {
  display: flex; flex-wrap: wrap; align-items: center; gap: .5rem;
  padding: .5rem 0; border-bottom: 1px solid var(--line); font-size: .92rem;
}
.plain-list li:last-child { border-bottom: none; padding-bottom: 0; }
.plain-list a { color: var(--ink); }

/* ── Pagination ─────────────────────────────────────────────────────────── */

.pager {
  display: flex; flex-wrap: wrap; gap: .6rem 1rem;
  align-items: center; justify-content: space-between; margin-top: 1.6rem;
}
.pager-count { margin: 0; }
.pager-links { display: flex; flex-wrap: wrap; gap: .3rem; align-items: center; }
.pager-gap { color: var(--ink-3); padding: 0 .15rem; }
/* The current page is a button that is deliberately not a link, so it needs to
   read as selected rather than as one more destination. */
.pager .is-current {
  background: var(--accent-soft); border-color: var(--accent-line);
  color: var(--accent); font-weight: 700;
}

/* ── Devlog ─────────────────────────────────────────────────────────────── */

.devlog { list-style: none; margin: .35rem 0 0; padding: 0; }
.devlog-entry { padding: 1rem 0; border-top: 1px solid var(--line); }
.devlog-entry:first-child { border-top: none; padding-top: .6rem; }
.devlog-head {
  display: flex; flex-wrap: wrap; gap: .4rem 1rem;
  align-items: baseline; justify-content: space-between; margin-bottom: .45rem;
}
.devlog-head h3 { margin: 0; font-size: 1rem; }
.devlog-when { display: flex; align-items: center; gap: .5rem; font-size: .82rem; color: var(--ink-3); }

/* The composer is a disclosure so the devlog reads as a log by default rather
   than as a form with some entries underneath it. */
.post-composer > summary { display: inline-block; cursor: pointer; list-style: none; }
.post-composer > summary::-webkit-details-marker { display: none; }

/* ── Tag rows and stage pills ───────────────────────────────────────────── */

.tag-row { display: flex; flex-wrap: wrap; gap: .35rem; align-items: center; }
.pill-row { display: flex; flex-wrap: wrap; gap: .35rem; align-items: center; }

/* Development stage: the second pill on a game card, so it has to read as a
   different axis from the genre pill beside it. --neon on --neon-soft is
   4.6:1 in light and 6.1:1 in dark, so it stays AA at pill size. */
.pill-stage { background: var(--neon-soft); border-color: var(--neon-line); color: var(--neon); }

/* ── Avatar sizes ───────────────────────────────────────────────────────── */

.avatar-sm { width: 26px; height: 26px; font-size: .68rem; }
.avatar-xl { width: 84px; height: 84px; font-size: 1.6rem; }

/* A member's picture with the presence dot pinned to its corner. */
.member-avatar { position: relative; display: inline-flex; flex: none; }
.online-dot {
  position: absolute; right: -2px; bottom: -2px; width: 10px; height: 10px; border-radius: 50%;
  background: var(--ok); border: 2px solid var(--surface);
}

/* ── Link-styled buttons and chip rows ──────────────────────────────────── */

.link-button {
  background: none; border: none; padding: 0; font: inherit; font-size: .82rem;
  color: var(--accent); cursor: pointer; text-decoration: underline; text-underline-offset: 2px;
}
.link-button:hover { color: var(--accent-deep); }
/* Destructive sibling of .link-button — kicking a member is not an accent-coloured
   action, and it sits right next to a harmless one in the roster. */
.link-danger { color: var(--danger); }
.link-danger:hover { color: var(--danger); filter: brightness(.85); }

.chip-row { display: flex; flex-wrap: wrap; gap: .35rem; }

/* Inline SVG marks used in place of emoji, so nothing depends on an emoji font
   being installed. Colour comes from currentColor. */
.menu-mark { display: inline-flex; flex: none; width: 1.05rem; height: 1.05rem; color: var(--ink-3); }
.menu-mark .icon-mark { width: 100%; height: 100%; }
.meta-icon { display: inline-flex; align-items: center; gap: .3rem; }
.meta-icon .icon-mark { width: .95rem; height: .95rem; flex: none; opacity: .7; }

/* ── Profile header and inbox ───────────────────────────────────────────── */

.profile-head { padding: 1.4rem 1.5rem; }
.profile-head h1 { margin-top: 0; }

.notification-list { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: .5rem; }
.notification {
  background: var(--surface); border: 1px solid var(--line); border-radius: var(--radius);
  padding: .75rem .9rem;
}
.notification.is-unread { border-color: var(--accent-line); background: var(--accent-soft); }

/* ── Game cards and the "more from" mini cards ──────────────────────────── */

.game-card-body { display: flex; flex-direction: column; gap: .55rem; flex: 1; padding: 1rem 1.1rem 1.15rem; }
.game-card-body h3 { margin: 0; }

.mini-card {
  display: flex; gap: .6rem; align-items: center; padding: .5rem;
  border: 1px solid var(--line); border-radius: var(--radius); background: var(--surface-2); color: var(--ink);
}
.mini-card:hover { border-color: var(--accent-line); background: var(--accent-soft); text-decoration: none; }
.mini-thumb {
  position: relative; flex: none; width: 58px; height: 44px; border-radius: var(--radius-s);
  overflow: hidden; background: var(--surface); border: 1px solid var(--line); display: grid; place-items: center;
}
.mini-thumb img { width: 100%; height: 100%; object-fit: cover; display: block; }
.mini-thumb-fallback {
  display: grid; place-items: center; width: 100%; height: 100%;
  color: #fff; font-size: .72rem; font-weight: 700; letter-spacing: -.02em;
  background-image: linear-gradient(155deg, rgba(12, 9, 31, .18) 0%, rgba(12, 9, 31, .58) 100%);
}
.mini-body { min-width: 0; display: flex; flex-direction: column; gap: .1rem; }
.mini-body strong { font-size: .9rem; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.mini-body .small { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

/* The developer / seller card rides along as the page scrolls, so the way back
   to a profile is always one click away. */
.author-card { position: sticky; top: 1rem; }

/* ── Drop zone states ───────────────────────────────────────────────────── */

.dropzone { cursor: pointer; }
.dropzone:hover { border-color: var(--accent-line); background: var(--accent-soft); }
.dropzone[data-dragging] { border-color: var(--accent); background: var(--accent-soft); }
.dropzone[data-has-file] { border-style: solid; border-color: var(--accent-line); }
.dropzone-title { display: block; font-weight: 600; font-size: .9rem; }
.dropzone-note { display: block; margin-top: .25rem; font-size: .8rem; color: var(--ink-3); }
.dropzone input[type="file"] { border: none; padding: 0; background: none; margin-top: .5rem; }

@media print {
  .topbar, .site-footer, .cookie-banner, .chat-compose, .dm-compose { display: none; }
  body { background: #fff; }
}

/* ── Floating translate tool ────────────────────────────────────────────── */
/* Lives in the corner on every page. It sits above the cookie banner's strip
   (--banner-h is measured by app.js) and below the banner's own z-index.
   Styled like the app-store FABs younger audiences know: a squircle with a
   candy gradient, a soft bob, and a playful tilt on hover. */

.tr-fab {
  position: fixed; right: 1.1rem; bottom: calc(var(--banner-h, 0px) + 1.1rem);
  z-index: 55;
  width: 3.3rem; height: 3.3rem; border-radius: 1.05rem;
  border: 2px solid rgb(255 255 255 / .55);
  background: linear-gradient(135deg, #a06bff 0%, #7c3aed 55%, #c95fe0 120%);
  color: #fff;
  box-shadow: 0 8px 20px rgb(124 58 237 / .35), 0 2px 6px rgb(124 58 237 / .25);
  display: grid; place-items: center; cursor: pointer;
  transition: transform .18s cubic-bezier(.34,1.56,.64,1), box-shadow .18s;
  animation: tr-bob 3.4s ease-in-out infinite;
}
.tr-fab:hover {
  animation: none;
  transform: translateY(-3px) rotate(-6deg) scale(1.06);
  box-shadow: 0 12px 28px rgb(124 58 237 / .45);
}
.tr-fab:active { transform: scale(.94); }
.tr-fab:focus-visible { outline: 2px solid var(--ink); outline-offset: 2px; }
.tr-fab-on {
  background: linear-gradient(135deg, #ff8ac2 0%, #b06bff 60%, #7c3aed 120%);
  border-color: rgb(255 255 255 / .8);
  box-shadow: 0 8px 20px rgb(233 109 199 / .4), 0 0 0 3px rgb(255 138 194 / .25);
}
@keyframes tr-bob {
  0%, 100% { transform: translateY(0); }
  50% { transform: translateY(-4px); }
}
@media (prefers-reduced-motion: reduce) {
  .tr-fab { animation: none; }
  .tr-fab:hover { transform: none; }
}

.tr-panel {
  position: fixed; right: 1.1rem; bottom: calc(var(--banner-h, 0px) + 4.7rem);
  z-index: 55;
  width: min(18rem, calc(100vw - 2.2rem));
  background: var(--surface); border: 1px solid var(--line);
  border-radius: var(--radius-l);
  box-shadow: 0 10px 30px rgb(22 21 42 / .18);
  padding: .9rem 1rem 1rem;
}
.tr-panel-head { display: flex; align-items: center; justify-content: space-between; margin-bottom: .35rem; }
.tr-panel-head strong { font-size: .98rem; }
.tr-close { border: none; background: none; color: var(--ink-3); font-size: 1.15rem; line-height: 1; cursor: pointer; padding: .1rem .3rem; border-radius: .35rem; }
.tr-close:hover { color: var(--ink); background: var(--surface-2); }
.tr-note { margin: 0 0 .6rem; font-size: .8rem; color: var(--ink-3); }
.tr-picker { display: flex; align-items: center; gap: .5rem; margin-bottom: .65rem; }
.tr-picker label { font-size: .84rem; color: var(--ink-2); white-space: nowrap; }
.tr-picker select { flex: 1; min-width: 0; }
.tr-toggle { width: 100%; }
.tr-status { margin: .5rem 0 0; font-size: .78rem; color: var(--ink-3); }
.tr-status:empty { display: none; }

/* The translated copy itself: quieter than the original, marked by an accent
   rule so the eye can separate the pair at a glance. */
.tr-out {
  margin: .3rem 0 .2rem; padding: .1rem 0 .1rem .6rem;
  border-left: 2px solid var(--accent-line);
  color: var(--ink-2); font-size: .92em;
}
.chat-msg-text + .tr-out { margin-top: .15rem; font-size: .85rem; }

/* ── User ID chip (#N, sequential from 1) ────────────────────────────────── */
/* Shown on the profile page only, next to the @handle. */
.uid-chip {
  font-size: .72em;
  font-weight: 500;
  color: var(--ink-3);
  background: var(--surface-2);
  border: 1px solid var(--line);
  border-radius: 999px;
  padding: .05em .5em;
  vertical-align: middle;
  white-space: nowrap;
}

/* ── 2026 refresh: global polish ─────────────────────────────────────────── */
/* Small cross-cutting touches that sit on top of the component styles above.
   Everything here is additive: selection colour, scrollbars, media treatment,
   and a consistent motion curve for interactive elements. */

::selection { background: color-mix(in srgb, var(--accent) 22%, transparent); }

* { scrollbar-width: thin; scrollbar-color: var(--line-strong) transparent; }
*::-webkit-scrollbar { width: 8px; height: 8px; }
*::-webkit-scrollbar-thumb { background: var(--line-strong); border-radius: 999px; }
*::-webkit-scrollbar-thumb:hover { background: var(--ink-3); }
*::-webkit-scrollbar-track { background: transparent; }

/* Softer empty states: a tinted well instead of an empty dashed box. */
.empty { background: color-mix(in srgb, var(--surface-2) 55%, transparent); border-color: var(--line); }

/* Media keeps its shape everywhere; covers get a hairline so light images do
   not melt into a light card. */
.game-cover-img, .game-strip-cover, img.cover { outline: 1px solid rgba(23, 26, 38, .06); outline-offset: -1px; }

/* Pills keep their shape but gain a quiet hover affordance where they are
   interactive-adjacent (filters, tags). */
.pill { transition: border-color .15s ease, background .15s ease; }

/* Headings track slightly tighter on large sizes for a cleaner hierarchy. */
h1 { letter-spacing: -.025em; }
h2 { letter-spacing: -.02em; }

@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after { transition-duration: .01ms !important; animation-duration: .01ms !important; }
}

/* Day dividers in chat read as small centred pills rather than bare text, so
   a busy channel scans by date at a glance. */
.chat-day {
  align-self: center; display: inline-block; max-width: 90%;
  padding: .1rem .7rem; border-radius: 999px;
  background: var(--surface-2); border: 1px solid var(--line);
  font-size: .74rem; color: var(--ink-3);
}

/* Admin and settings tables: a quiet row hover makes wide rows easier to track. */
table.data tbody tr { transition: background .12s ease; }
table.data tbody tr:hover { background: color-mix(in srgb, var(--surface-2) 60%, transparent); }

/* Hero: a touch more air under the headline before the page body begins. */
.hero { padding: 4rem 0 3.25rem; }

/* ── Chat reactions and message recall ─────────────────────────────────── */

/* The pill row under a message. Pills read as tiny counters, not buttons,
   until hover makes the affordance visible. */
.chat-react-row { display: flex; flex-wrap: wrap; gap: .3rem; margin-top: .3rem; }
.chat-react {
  background: var(--surface-2); border: 1px solid var(--line);
  border-radius: 999px; padding: .05rem .5rem;
  font-size: .74rem; line-height: 1.4; cursor: pointer; font-family: inherit;
  color: var(--ink-2); transition: border-color .12s ease, background .12s ease;
}
.chat-react:hover { border-color: var(--accent-line); background: var(--accent-soft); }
.chat-react.is-mine { border-color: var(--accent-line); background: var(--accent-soft); color: var(--accent-deep); }

/* The react + delete actions live in the meta row with Report; they follow
   the same show-on-hover rule already set for .chat-msg-actions. */
.chat-msg-react:hover { color: var(--accent-deep) !important; text-decoration: none !important; }
.chat-msg-delete:hover { color: var(--danger) !important; }

/* The shared palette, parked inside whichever message is being reacted to.
   Pops as a small rounded row anchored in the message body. */
.chat-react-palette {
  position: absolute; z-index: 30;
  display: flex; gap: .15rem; padding: .3rem;
  background: var(--surface); border: 1px solid var(--line-strong);
  border-radius: 12px; box-shadow: var(--shadow-lift);
}
/* `display: flex` above outranks the user-agent rule for [hidden], so without
   this the palette would sit on screen from page load instead of waiting for
   a click on a message's reaction button. Same trap as .chat-attach. */
.chat-react-palette[hidden] { display: none; }
.chat-react-pick {
  background: none; border: none; border-radius: 8px;
  font-size: 1.05rem; line-height: 1; padding: .25rem .35rem; cursor: pointer;
  font-family: inherit; transition: background .12s ease, transform .12s ease;
}
.chat-react-pick:hover { background: var(--accent-soft); transform: scale(1.15); }
.chat-msg-body { position: relative; }

/* Honeypot field on the registration form: present in the markup so bot
   scripts fill it, off-screen and out of tab order so no person ever does. */
.hp-field { position: absolute; left: -9999px; top: -9999px; height: 1px; width: 1px; overflow: hidden; }

/* Profile posts list: tighter vertical rhythm than the page stack because the
   rows are cards that already carry their own padding. */
.stack-sm > * + * { margin-top: .6rem; }
.profile-post .meta-line a { color: var(--ink-2); }
.profile-post .meta-line a:hover { color: var(--accent-deep); }

/* ── Likes ─────────────────────────────────────────────────────────────── */

/* A heart and a number, reading as a quiet counter until hovered or owned. */
.like-btn {
  display: inline-flex; align-items: center; gap: .3rem;
  background: none; border: 1px solid var(--line);
  border-radius: 999px; padding: .15rem .6rem;
  font-size: .82rem; line-height: 1.4; cursor: pointer;
  font-family: inherit; color: var(--ink-2); text-decoration: none;
  transition: border-color .12s ease, background .12s ease, color .12s ease;
}
.like-btn:hover { border-color: var(--danger, #d24a6a); color: var(--danger, #d24a6a); }
.like-heart { font-size: .95em; line-height: 1; }
.like-btn.is-liked { border-color: var(--danger, #d24a6a); background: color-mix(in srgb, var(--danger, #d24a6a) 10%, transparent); color: var(--danger, #d24a6a); }
.like-btn.is-liked .like-heart { transform: scale(1.08); }

/* Count-only chip for list cards (no toggle there). */
.like-chip {
  display: inline-flex; align-items: center; gap: .25rem;
  font-size: .78rem; color: var(--ink-2);
}
.like-chip::before { content: '♥'; font-size: .9em; color: var(--danger, #d24a6a); }

/* Hosted video-player embed (trailer links from the game form). The iframe
   keeps a 16:9 box so the layout does not jump while the player loads. */
.media-embed {
  position: relative; width: 100%; aspect-ratio: 16 / 9;
  background: #000; border-radius: 10px; overflow: hidden;
}
.media-embed iframe {
  position: absolute; inset: 0; width: 100%; height: 100%; border: 0;
}
