/*
 * Theme is driven entirely by these six variables; the rest of the stylesheet
 * never names a colour, so switching is a single block. Dark is the default.
 * Set before first paint in the inline head script to avoid a white flash.
 */
:root,
:root[data-theme="dark"] {
  --ink: #ededed;
  --paper: #0b0b0b;
  --fog: #161616;
  --fog-2: #202020;
  --line: #333333;
  --mute: #8f8f8f;
  --danger: #e2564a;
  --sans: -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
  --mono: "SFMono-Regular", "Cascadia Mono", Consolas, "Liberation Mono", monospace;
  --rail: 236px;
}

:root[data-theme="light"] {
  --ink: #0b0b0b;
  --paper: #ffffff;
  --fog: #f2f2f2;
  --fog-2: #e8e8e8;
  --line: #d4d4d4;
  --mute: #6f6f6f;
  --danger: #a3311f;
}

* { box-sizing: border-box; }

html, body { height: 100%; }

body {
  margin: 0;
  background: var(--paper);
  color: var(--ink);
  font: 15px/1.55 var(--sans);
  -webkit-font-smoothing: antialiased;
}

::selection { background: var(--ink); color: var(--paper); }

a { color: inherit; text-decoration: none; }
/* deslop-ignore-next-line 09 links need an underline: the palette has no colour to signal them */
a:hover { text-decoration: underline; text-underline-offset: 3px; }

h1, h2, h3 { margin: 0; font-weight: 600; letter-spacing: -0.02em; }
h1 { font-size: 30px; line-height: 1.15; }
h2 { font-size: 19px; line-height: 1.25; }
p { margin: 0; }

.sprite { position: absolute; width: 0; height: 0; overflow: hidden; }

.i {
  width: 18px; height: 18px; flex: none;
  fill: none; stroke: currentColor; stroke-width: 1.5;
  stroke-linecap: round; stroke-linejoin: round;
}
.i-sm { width: 15px; height: 15px; }
.i-lg { width: 22px; height: 22px; }

:focus-visible { outline: 2px solid var(--ink); outline-offset: 2px; }

/* ---- shell ---- */
.app { display: grid; grid-template-columns: var(--rail) minmax(0, 1fr); min-height: 100%; }

.rail {
  position: sticky; top: 0; align-self: start;
  height: 100vh; display: flex; flex-direction: column;
  padding: 26px 20px; border-right: 1px solid var(--ink);
}

.brand { display: flex; align-items: center; gap: 10px; text-decoration: none; }
.brand:hover { text-decoration: none; }
.brand img { display: block; object-fit: contain; background: var(--paper); }
.brand-name { font-size: 17px; font-weight: 600; letter-spacing: -0.02em; }
.brand-name b { font-weight: 600; }

.nav { display: flex; flex-direction: column; margin-top: 34px; }

.nav-link {
  display: flex; align-items: center; gap: 10px;
  padding: 8px 10px; margin-left: -10px;
  font-size: 14px; color: var(--mute);
  border-left: 2px solid transparent;
  transition: color 0.14s ease, background 0.14s ease, border-color 0.14s ease;
}
.nav-link:hover { color: var(--ink); background: var(--fog); text-decoration: none; }
.nav-link[aria-current="page"] { color: var(--ink); font-weight: 600; border-left-color: var(--ink); }

.rail-foot { margin-top: auto; padding-top: 24px; border-top: 1px solid var(--line); }
.rail-note { font-size: 12px; line-height: 1.5; color: var(--mute); }
.rail-stat { display: inline-block; margin-top: 12px; font: 12px var(--mono); color: var(--ink); }

/* The account control sits between the nav and the footer so the mobile layout,
   which drops .rail-foot entirely, can still reach sign-in. */
.rail-account { margin-top: 18px; }
.rail-account .rail-stat {
  display: flex; align-items: center; justify-content: center; gap: 8px; width: 100%; margin-top: 0;
  padding: 8px 10px; border: 1px solid var(--ink); background: var(--ink); color: var(--paper);
  text-decoration: none;
  transition: background 0.14s ease, color 0.14s ease, transform 0.08s ease;
}
.rail-account .rail-stat:hover { background: var(--paper); color: var(--ink); text-decoration: none; }
.rail-account .rail-stat:active { transform: translateY(1px); }
.rail-account .spacer { flex: 1; }

/* The theme control is a real button, so it needs the button resets plus the
   hover affordance the sibling link already has. */
.theme-toggle {
  display: flex; align-items: center; gap: 8px;
  padding: 0; border: 0; background: none;
  font: inherit; color: inherit; cursor: pointer; text-align: left;
}
.theme-toggle:hover { text-decoration: underline; }
.theme-toggle .i { width: 14px; height: 14px; }

.main { display: flex; flex-direction: column; min-width: 0; }

.topbar {
  position: sticky; top: 0; z-index: 5;
  display: flex; align-items: center; gap: 16px;
  padding: 14px 32px; background: var(--paper); border-bottom: 1px solid var(--ink);
}

.search { display: flex; align-items: center; gap: 9px; flex: 1; max-width: 460px; color: var(--mute); }
.search input {
  width: 100%; border: 0; padding: 6px 0; background: none;
  font: 14px var(--sans); color: var(--ink);
}
.search input:focus { outline: none; }
.search input::placeholder { color: var(--mute); }
.search input::-webkit-search-cancel-button { display: none; }

.topbar-actions { margin-left: auto; display: flex; gap: 8px; }

.view { flex: 1; padding: 34px 32px 60px; max-width: 1080px; width: 100%; }

.foot {
  display: flex; flex-wrap: wrap; gap: 12px 18px; align-items: center;
  padding: 16px 32px; border-top: 1px solid var(--line);
  font: 12px var(--mono); color: var(--mute);
}
/* deslop-ignore-next-line 09 footer link, not decoration */
.foot a { text-decoration: underline; text-underline-offset: 3px; }
/* The legal documents are grouped, not strung across the row: they are read
   once and referred to by name, and a word cloud in a footer is neither. */
.foot-links { display: flex; flex-wrap: wrap; gap: 14px; margin-left: auto; }

/* ---- controls ---- */
.btn {
  display: inline-flex; align-items: center; gap: 7px;
  padding: 8px 14px; border: 1px solid var(--ink); border-radius: 0;
  background: var(--paper); color: var(--ink);
  font: 500 12px/1 var(--sans); letter-spacing: 0.04em; text-transform: uppercase;
  cursor: pointer; white-space: nowrap;
  transition: background 0.14s ease, color 0.14s ease, border-color 0.14s ease, transform 0.08s ease;
}
.btn:hover { background: var(--ink); color: var(--paper); text-decoration: none; }
.btn:active:not(:disabled) { transform: translateY(1px); }
.btn:disabled { opacity: 0.4; cursor: not-allowed; }
.btn:disabled:hover { background: var(--paper); color: var(--ink); }
.btn-primary { background: var(--ink); color: var(--paper); }
.btn-primary:hover { background: var(--paper); color: var(--ink); }
.btn-sm { padding: 5px 9px; font-size: 11px; }
.btn-ghost { border-color: var(--line); color: var(--mute); }
.btn-ghost:hover { border-color: var(--ink); background: var(--ink); color: var(--paper); }
.btn-danger:hover { background: var(--ink); color: var(--paper); border-color: var(--ink); }

.field { display: flex; flex-direction: column; gap: 6px; }
.label {
  font: 500 11px/1 var(--sans); letter-spacing: 0.09em;
  text-transform: uppercase; color: var(--mute);
}
.input, .select, .textarea {
  width: 100%; padding: 9px 11px;
  border: 1px solid var(--line); border-radius: 0; background: var(--paper);
  font: 14px/1.5 var(--sans); color: var(--ink);
}
.input:focus, .select:focus, .textarea:focus { border-color: var(--ink); outline: none; }
.textarea { font: 13px/1.65 var(--mono); resize: vertical; min-height: 320px; }
.select { appearance: none; padding-right: 30px; cursor: pointer; }

.hint { font-size: 12px; color: var(--mute); }

/* ---- layout blocks ---- */
.page-head {
  display: flex; align-items: flex-end; gap: 20px; flex-wrap: wrap;
  padding-bottom: 18px; margin-bottom: 26px; border-bottom: 1px solid var(--ink);
}
.page-head .spacer { margin-left: auto; }
.page-sub { margin-top: 6px; font-size: 13px; color: var(--mute); }

.tabs { display: flex; gap: 0; border-bottom: 1px solid var(--line); margin-bottom: 22px; }
.tab {
  padding: 8px 14px; margin-bottom: -1px; font-size: 13px; color: var(--mute);
  border-bottom: 2px solid transparent;
  transition: color 0.16s ease, border-color 0.16s ease, background 0.16s ease;
}
.tab:hover { color: var(--ink); text-decoration: none; background: var(--fog); }
.tab[aria-selected="true"] { color: var(--ink); font-weight: 600; border-bottom-color: var(--ink); }

.split { display: grid; grid-template-columns: minmax(0, 1fr) 260px; gap: 40px; align-items: start; }
.panel { border: 1px solid var(--line); padding: 16px; }
.panel + .panel { margin-top: 16px; }
.panel-title {
  font: 500 11px/1 var(--sans); letter-spacing: 0.09em;
  text-transform: uppercase; color: var(--mute); margin-bottom: 12px;
}

.kv { display: grid; grid-template-columns: 96px 1fr; gap: 6px 12px; font-size: 13px; }
.kv dt { color: var(--mute); }
.kv dd { margin: 0; font: 12px var(--mono); word-break: break-all; }

/* ---- list ---- */
.list { border-top: 1px solid var(--ink); }
.row {
  display: grid; grid-template-columns: 26px minmax(0, 1fr) auto;
  gap: 14px; align-items: baseline;
  padding: 15px 4px; border-bottom: 1px solid var(--line);
  transition: background 0.14s ease;
}
.row:hover { background: var(--fog); }
.row-mark { color: var(--mute); padding-top: 2px; }
.row-thumb {
  display: block; width: 56px; height: 56px; flex: 0 0 56px;
  border: 1px solid var(--line); background: var(--fog); overflow: hidden;
}
.row-thumb img {
  display: block; width: 100%; height: 100%; object-fit: cover;
  filter: grayscale(100%); transition: filter 0.12s linear;
}
.row:hover .row-thumb { border-color: var(--ink); }
.row:hover .row-thumb img { filter: none; }
.row-title { font-size: 15px; font-weight: 500; letter-spacing: -0.01em; }
.row-meta {
  display: flex; flex-wrap: wrap; gap: 12px; align-items: center;
  margin-top: 5px; font: 11px var(--mono); color: var(--mute);
}
.row-meta span { display: inline-flex; align-items: center; gap: 4px; }
.row-right { display: flex; align-items: center; gap: 8px; font: 11px var(--mono); color: var(--mute); }

.tag {
  display: inline-block; padding: 1px 6px;
  border: 1px solid var(--line); font: 11px var(--mono); color: var(--ink);
}
.tag:hover { border-color: var(--ink); text-decoration: none; }
.tags { display: flex; flex-wrap: wrap; gap: 6px; }

.pill {
  display: inline-flex; align-items: center; gap: 5px;
  font: 500 11px/1 var(--sans); letter-spacing: 0.07em; text-transform: uppercase;
}
.pill-mute { color: var(--mute); }
/* A ban is the one status that must not read as decoration. */
.pill-danger { color: var(--danger); }

.empty { padding: 56px 0; text-align: left; }
.empty h2 { margin-bottom: 8px; }
.empty p { color: var(--mute); max-width: 46ch; }
.empty .btn { margin-top: 20px; }

.notice {
  display: flex; gap: 10px; align-items: flex-start;
  padding: 12px 14px; border: 1px solid var(--ink); background: var(--fog);
  font-size: 13px;
}
.notice + .notice { margin-top: 10px; }
.notice code { font: 12px var(--mono); word-break: break-all; }

/* Inverted against --ink so a code block stays readable when the theme flips. */
.on-ink { color: var(--paper); }

/* ---- code ---- */
.code {
  margin: 0; border: 1px solid var(--ink); background: var(--ink); color: var(--paper);
  font: 13px/1.7 var(--mono); overflow: auto;
}
.code-bar {
  display: flex; align-items: center; gap: 12px;
  padding: 9px 12px; border: 1px solid var(--ink); border-bottom: 0;
  background: var(--paper); font: 11px var(--mono); color: var(--mute);
}
.code-bar + .code { border-top: 1px solid var(--ink); }
.code-bar .spacer { margin-left: auto; }
.code pre { margin: 0; padding: 16px 18px; white-space: pre; }
.code-inline { font: 12px var(--mono); background: var(--fog); padding: 1px 5px; }

/* ------------------------------------------------------------ media previews
   Square corners, hairline rules and greyscale until hover, matching the rest
   of the sheet. Images cap at 900px so a 4K screenshot cannot stretch a column;
   video uses an intrinsic 16/9 box so the page does not jump while metadata
   loads. */

.media { display: flex; flex-direction: column; gap: 16px; margin-bottom: 20px; }
.media-item { margin: 0; border: 1px solid var(--ink); background: var(--paper); }
.media-item + .media-item { margin-top: 0; }
.media-item > figcaption {
  display: flex; align-items: center; gap: 10px;
  padding: 8px 10px; border-top: 1px solid var(--ink);
  font: 12px var(--mono); color: var(--mute);
}
.media-item > figcaption a { color: var(--ink); word-break: break-all; }
.media-item > figcaption .spacer { margin-left: auto; }
.media-img {
  display: block; width: 100%; max-width: 900px; height: auto;
  background: var(--fog); filter: grayscale(100%);
  transition: filter 0.12s linear;
}
.media-item:hover .media-img { filter: none; }
.media-video {
  display: block; width: 100%; max-width: 900px; aspect-ratio: 16 / 9;
  background: var(--ink); border: 0;
}
.media-audio { display: block; width: 100%; padding: 16px; background: var(--fog); }

/* ---- carousel ---- */

/* One picture at a time, not a peek at the next. The whole reason this exists is
   that a long strip of screenshots hides the last one - the one with the answer -
   below the fold on a phone. */
.carousel { border: 1px solid var(--ink); background: var(--paper); }
.carousel:focus-visible { outline: 2px solid var(--ink); outline-offset: 2px; }
.carousel-head {
  display: flex; align-items: center; gap: 10px;
  padding: 8px 10px; border-bottom: 1px solid var(--ink);
  font: 12px var(--mono); color: var(--mute);
}
.carousel-head .spacer { margin-left: auto; }
.carousel-count { font-variant-numeric: tabular-nums; }
.carousel-box { position: relative; }
.carousel-stage {
  display: flex;
  /* The slides are siblings in a flex row, so the track is offset rather than
     scrolled: a scroller keeps its own scroll position, and two of them out of
     sync is the classic carousel bug. */
  transform: translateX(calc(var(--carousel-index, 0) * -100%));
  transition: transform 0.18s ease-out;
}
.carousel-stage .media-item {
  flex: 0 0 100%; min-width: 0; border: 0; margin: 0;
}
.carousel-stage .media-img {
  max-width: 100%;
  /* The muted-until-hover treatment is for a picture you have not chosen yet.
     The slide on screen is the one the reader navigated to, so it is in colour. */
  filter: none;
}
.carousel-nav {
  position: absolute; top: 50%; transform: translateY(-50%);
  display: grid; place-items: center;
  width: 40px; height: 40px; padding: 0;
  border: 1px solid var(--ink); border-radius: 50%;
  background: var(--paper); color: var(--ink);
  cursor: pointer; opacity: 0.85;
}
.carousel-nav:hover { opacity: 1; }
.carousel-nav[hidden] { display: none; }
.carousel-prev { left: 10px; }
.carousel-next { right: 10px; }
.carousel-dots {
  display: flex; justify-content: center; flex-wrap: wrap; gap: 8px;
  padding: 10px; border-top: 1px solid var(--ink);
}
.carousel-dot {
  width: 9px; height: 9px; padding: 0;
  border: 1px solid var(--ink); border-radius: 50%;
  background: transparent; cursor: pointer;
}
.carousel-dot[aria-current="true"] { background: var(--ink); }
.carousel .media-name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

@media (max-width: 640px) {
  .carousel-nav { width: 34px; height: 34px; }
}

@media (prefers-reduced-motion: reduce) {
  .carousel-stage { transition: none; }
}

@media (max-width: 640px) {
  .media-img, .media-video { max-width: 100%; }
  .media-item > figcaption { flex-wrap: wrap; }
}

/* ---- files ---- */
.files { width: 100%; border-collapse: collapse; font-size: 13px; }
.files th {
  padding: 8px 10px; text-align: left; border-bottom: 1px solid var(--ink);
  font: 500 11px/1 var(--sans); letter-spacing: 0.09em; text-transform: uppercase; color: var(--mute);
}
.files td { padding: 11px 10px; border-bottom: 1px solid var(--line); vertical-align: middle; }
.files tr:hover td { background: var(--fog); }
.files .name { font: 13px var(--mono); word-break: break-all; }
.files .num { font: 12px var(--mono); color: var(--mute); white-space: nowrap; text-align: right; }
.files .act { text-align: right; white-space: nowrap; }

.drop {
  display: flex; align-items: center; gap: 10px;
  padding: 18px; border: 1px dashed var(--mute); background: var(--paper);
  font-size: 13px; color: var(--mute); cursor: pointer;
}
.drop:hover, .drop.is-over { border-color: var(--ink); color: var(--ink); background: var(--fog); }
.drop input { display: none; }

.queue { margin-top: 12px; display: flex; flex-direction: column; gap: 6px; }
.queue-row {
  display: flex; align-items: center; gap: 10px;
  padding: 7px 10px; border: 1px solid var(--line); font: 12px var(--mono);
}
.queue-row .spacer { margin-left: auto; }
.bar { position: relative; overflow: hidden; width: 90px; height: 3px; background: var(--fog-2); }
.bar > span { display: block; height: 100%; background: var(--ink); }

.toggle { display: flex; align-items: center; gap: 8px; font-size: 13px; cursor: pointer; }
.toggle input { accent-color: var(--ink); width: 15px; height: 15px; }

.actions { display: flex; gap: 8px; flex-wrap: wrap; }

/* ---- toast ---- */
.toast {
  position: fixed; right: 20px; bottom: 20px; z-index: 40;
  display: flex; align-items: center; gap: 9px;
  padding: 11px 15px; border: 1px solid var(--ink); background: var(--paper);
  font: 12px var(--mono); max-width: 420px;
  animation: toast-in 0.18s ease-out;
}
.toast.is-bad { border-width: 2px; }

/* ---- motion ---- */

.spin { animation: spin 0.7s linear infinite; }
@keyframes spin { to { transform: rotate(360deg); } }

/* Each routing pass replaces the children of .view, so the animation runs on
   every navigation - a page switch is a page change, not a paint job. */
.view > * { animation: view-in 0.22s ease-out; }
@keyframes view-in {
  from { opacity: 0; transform: translateY(6px); }
  to   { opacity: 1; transform: none; }
}

@keyframes fade-in { from { opacity: 0; } to { opacity: 1; } }
@keyframes pop-in {
  from { opacity: 0; transform: scale(0.97) translateY(6px); }
  to   { opacity: 1; transform: none; }
}
@keyframes toast-in {
  from { opacity: 0; transform: translateY(8px); }
  to   { opacity: 1; transform: none; }
}

@media (prefers-reduced-motion: reduce) {
  .spin, .view > *, .toast, .modal-backdrop, .modal-backdrop .auth-card { animation: none; }
  .btn, .nav-link, .tab, .row { transition: none; }
}

/* ---- accounts, profiles, captcha ---- */

/* Rows of controls that belong together: a primary action and its escape
   hatch, or an input and its submit button. */
.field-row { display: flex; flex-wrap: wrap; align-items: center; gap: 10px; }
.field-row .btn { flex: 0 0 auto; }
.auth-card { max-width: 460px; margin-top: 8px; }
.auth-card h1 { margin-bottom: 6px; }
.auth-card .field { margin-top: 16px; }
.auth-card .field-row { margin-top: 18px; }

/* Empty by default: a collapsed element must not leave a gap above the submit. */
.form-error {
  margin: 12px 0 0; min-height: 0; font-size: 12px; line-height: 1.5; color: var(--ink);
}
.form-error:empty { display: none; }
.form-error:not(:empty) {
  padding: 8px 10px; border-left: 2px solid var(--ink); background: var(--fog);
}

/* Profile counters, sized like the feed's row rhythm rather than as hero
   numbers - a profile is a list, not a landing page. */
.stats-row { display: flex; flex-wrap: wrap; gap: 28px; }
.stat-box { display: flex; flex-direction: column; gap: 2px; text-decoration: none; }
a.stat-box:hover .stat-label { color: var(--ink); }
.stat-value { font: 20px var(--mono); letter-spacing: -0.02em; color: var(--ink); }
.stat-label { font-size: 11px; color: var(--mute); transition: color 0.12s linear; }
.rows { display: block; }

/* A moderation queue row carries text, then status, then the actions for that
   one row. Actions sit at the end and wrap rather than being pinned to a fixed
   column, because a narrow phone is the only place the queue gets read while
   actually doing something about it. */
.queue-row {
  display: flex; flex-wrap: wrap; gap: 8px 12px; align-items: center;
  padding: 12px 4px; border-bottom: 1px solid var(--line);
}
.queue-row:last-child { border-bottom: 0; }
.queue-row p { margin: 4px 0 0; }
.queue-row strong { font-weight: 500; }
.queue-row .btn { flex: 0 0 auto; }
/* A file being decoded and re-encoded in the browser. Without a state here the
   row sits at 0% looking like a hung upload, which for a two-second downscale on
   a phone is a long time to look broken. */
.queue-row.is-working { opacity: 0.55; }
.queue-row.is-working .bar::after {
  content: ''; position: absolute; inset: 0;
  background: linear-gradient(90deg, transparent, var(--line), transparent);
  animation: bar-sweep 1.1s linear infinite;
}
@keyframes bar-sweep {
  from { transform: translateX(-100%); }
  to { transform: translateX(100%); }
}
@media (prefers-reduced-motion: reduce) {
  .queue-row.is-working .bar::after { animation: none; }
}
.queue-row .err { color: var(--danger); }

/* Legal prose: a measure that stays readable and a section rhythm, since these
   pages are read rather than scanned. */
.legal { max-width: 68ch; }
.legal section { margin-top: 22px; padding-top: 18px; border-top: 1px solid var(--line); }
.legal h2 { margin: 0 0 8px; font-size: 15px; font-weight: 600; letter-spacing: -0.01em; }
.legal p { margin: 0 0 10px; color: var(--ink); }
.legal .page-sub, .legal .hint { color: var(--mute); }

/* Avatar preview next to its own file input. */
.avatar-picker { flex: 0 0 auto; display: flex; align-items: center; }

/* Comments sit at the end of the post column, separated by a rule rather than a
   card: they are the tail of the publication, not a second document. */
.textarea-short { min-height: 76px; }
.comment-list { margin-top: 18px; border-top: 1px solid var(--line); }
.comment { padding: 14px 4px; border-bottom: 1px solid var(--line); }
.comment-head { display: inline-flex; align-items: center; gap: 8px; color: inherit; }
.comment-head:hover { text-decoration: none; }
.comment-head:hover .comment-nick { text-decoration: underline; text-underline-offset: 3px; }
.comment-nick { font-weight: 500; }
.comment-body { margin: 8px 0 10px; white-space: pre-wrap; overflow-wrap: anywhere; }

/* ---- locked publications ----
   The lock screen is deliberately not a modal: the title, tags and counters
   around it are public, and hiding them would break the feed-to-post link that
   makes a locked publication worth clicking. A short fade over the top edge
   reads as "content continues past this line" without faking the content. */
.chip {
  display: inline-flex; align-items: center; gap: 5px;
  padding: 2px 7px; border: 1px solid var(--line);
  font: 11px var(--mono); color: var(--mute); white-space: nowrap;
}
.chip-lock { max-width: 180px; overflow: hidden; text-overflow: ellipsis; }

/* ---- game chip ----
   A cover thumbnail plus the game's name and studio. The two text lines are
   what carry the meaning, so the chip grows sideways rather than upward, and the
   name truncates while the studio - usually short - is allowed to stay whole. */
.chip-game {
  gap: 8px; padding: 4px 8px 4px 4px; max-width: 100%;
  border-color: var(--line); background: var(--paper);
}
.chip-cover {
  flex: 0 0 auto; width: 28px; height: 28px;
  object-fit: cover; border: 1px solid var(--line); background: var(--fog);
}
.chip-cover-empty { display: grid; place-items: center; color: var(--mute); }
.chip-text { display: flex; flex-direction: column; min-width: 0; line-height: 1.25; }
.chip-game-name {
  color: var(--ink); font-weight: 500;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap; max-width: 220px;
}
.chip-game-name:hover { text-decoration: underline; }
.chip-game-author {
  color: var(--mute); overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.game-preview:not(:empty) { padding-top: 2px; }
.row-game { display: block; margin-top: 6px; }
.row-game .chip { max-width: 100%; }
/* A game's page is headed by the chip itself, inside the h1 - so it is sized from
   the heading rather than at the chip's usual body size, and it sits on the text's
   baseline instead of stretching the line box. */
.page-title-row { display: inline-flex; align-items: center; gap: 12px; margin: 0; vertical-align: middle; }
.page-title-row .chip { font-size: inherit; padding: 6px 14px 6px 6px; }
.page-title-row .chip-cover { width: 48px; height: 48px; }
.page-title-row .chip-game-name { font-size: inherit; max-width: min(60ch, 78vw); }

.lock-screen { position: relative; margin-top: 20px; }
.lock-fade {
  position: absolute; inset: -6px -10px 46% -10px; z-index: 0; pointer-events: none;
  background: linear-gradient(180deg, var(--fog) 0%, transparent 100%);
}
.lock-panel { position: relative; z-index: 1; background: var(--paper); }
.lock-head { display: flex; align-items: flex-start; gap: 12px; }
.lock-head h2 { margin: 0 0 4px; font-size: 16px; }
.lock-panel .input { flex: 1 1 180px; min-width: 0; }
.lock-panel .field-row { align-items: stretch; }

/* ---- like ----
   Outlined until liked, then filled: a filled heart reads as state at a glance
   without needing the count to be non-zero. */
.btn-like { gap: 6px; }
.btn-like .like-count { font: 11px var(--mono); }
.btn-like .like-count:empty { display: none; }
.btn-like.is-on { color: var(--paper); background: var(--ink); border-color: var(--ink); }
.btn-like.is-on:hover { background: var(--ink); }
.btn-like:disabled { opacity: 0.5; }

/* ---- profile design ---- */
.profile-card { margin-bottom: 18px; }
.profile-head { display: flex; align-items: center; gap: 18px; flex-wrap: wrap; }
.avatar {
  position: relative; display: inline-flex; align-items: center; justify-content: center;
  flex: 0 0 auto; overflow: hidden; border: 1px solid var(--line);
  background: var(--fog); color: var(--mute);
}
.avatar-img { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; }
.avatar-letter { font: 600 1em var(--sans); }

/* Server-set admin flag only - the client cannot grant itself one. */
.admin-badge {
  display: inline-flex; align-items: center; gap: 4px; vertical-align: middle;
  margin-left: 8px; padding: 2px 7px; border: 1px solid var(--ink);
  font: 11px var(--mono); text-transform: lowercase; color: var(--ink);
}
.admin-badge-sm { margin-left: 0; padding: 1px 4px; }

/* The operator's editorial mark, kept visually distinct from "admin": one is a
   claim about a person, the other is a claim about their access. */
.popular-badge {
  display: inline-flex; align-items: center; gap: 4px; vertical-align: middle;
  margin-left: 6px; padding: 2px 7px; border: 1px dashed var(--mute);
  font: 11px var(--mono); text-transform: lowercase; color: var(--mute);
}
.popular-badge-sm { margin-left: 0; padding: 1px 4px; }

.design-row { display: flex; flex-wrap: wrap; gap: 14px; margin-top: 14px; }
.design-row .field { flex: 1 1 200px; margin-top: 0; }
.design-row .label { display: inline-flex; align-items: center; gap: 6px; }

/* Author line on an item page, doubling as the follow control. */
.author-row {
  display: flex; align-items: center; gap: 10px; margin-top: 12px;
  color: inherit;
}
.author-link {
  display: flex; align-items: center; gap: 10px; min-width: 0;
  text-decoration: none; color: inherit;
}
.author-link:hover .author-nick { text-decoration: underline; }
.author-link > span:last-child { display: flex; flex-direction: column; min-width: 0; }
.author-nick { font-size: 14px; color: var(--ink); }
.author-sub { font: 11px var(--mono); color: var(--mute); }
.author-row .btn { flex: 0 0 auto; }

.modal-backdrop {
  position: fixed; inset: 0; z-index: 40;
  display: flex; align-items: center; justify-content: center;
  padding: 20px; background: rgba(0, 0, 0, 0.55);
  animation: fade-in 0.15s ease-out;
}
.modal-backdrop .auth-card { width: 100%; margin: 0; background: var(--paper); animation: pop-in 0.2s cubic-bezier(0.2, 0.9, 0.3, 1.2); }
.captcha-answer { flex: 1 1 120px; min-width: 0; }
.modal-backdrop .btn-ghost { margin-top: 14px; }

/* ---- responsive ---- */
@media (max-width: 900px) {
  .app { grid-template-columns: 1fr; }
  .rail {
    position: static; height: auto; padding: 14px 18px;
    border-right: 0; border-bottom: 1px solid var(--ink);
  }
  .nav { flex-direction: row; flex-wrap: wrap; margin-top: 14px; gap: 4px; }
  .nav-link { border-left: 0; border-bottom: 2px solid transparent; padding: 6px 8px; }
  .nav-link[aria-current="page"] { border-bottom-color: var(--ink); }
  .rail-account { margin-top: 12px; }
  .rail-account .rail-stat { width: auto; }
  .rail-foot { display: none; }
  .topbar { padding: 12px 18px; }
  .view { padding: 24px 18px 48px; }
  .foot { padding: 14px 18px; }
  .split { grid-template-columns: minmax(0, 1fr); gap: 26px; }
}

/* ---- chat ---- */
/* The log is its own scroll container rather than the page, so reading history
   never scrolls the header or the nav out of reach on a phone. */
.chat-wrap {
  position: relative;
  display: flex;
  flex-direction: column;
  gap: 12px;
  min-height: 0;
}
.chat-log {
  height: min(66vh, 640px);
  overflow-y: auto;
  overscroll-behavior: contain;
  background: var(--fog);
  border: 1px solid var(--line);
  border-radius: 10px;
  padding: 16px;
}
.chat-msgs { display: flex; flex-direction: column; gap: 14px; }

/* A run of messages from one person is one block: the bubbles touch, the gap
   before the first of them is the gap that separates two speakers. Grouping is
   decided in JS, this is only how the gap is drawn. */
.chat-msgs > .chat-msg.is-grouped { margin-top: -8px; }

.chat-day {
  align-self: center;
  position: sticky;
  top: 0;
  z-index: 1;
  margin: 6px 0 2px;
  padding: 3px 12px;
  font-size: 12px;
  color: var(--mute);
  background: var(--fog);
  border: 1px solid var(--line);
  border-radius: 999px;
}

/* The edge of the unread part, drawn where it starts rather than described. */
.chat-new {
  display: flex;
  align-items: center;
  gap: 10px;
  margin: 4px 0;
  font-size: 12px;
  color: var(--danger);
}
.chat-new::before, .chat-new::after { content: ''; flex: 1; height: 1px; background: currentColor; opacity: .5; }

.chat-msg { max-width: 78%; }
.chat-msg.is-mine { align-self: flex-end; }
.chat-msg-head {
  display: flex;
  align-items: baseline;
  gap: 8px;
  margin-bottom: 3px;
}
.chat-msg.is-mine .chat-msg-head { justify-content: flex-end; }
/* In a run the head keeps only the stamp (the nick is left out in JS), so it
   stays short without having to be hidden - which would also hide the retract
   control and take it out of reach. */
.chat-msg.is-grouped .chat-msg-head { margin-bottom: 1px; }
.chat-nick { font-weight: 600; font-size: 13px; }
.chat-time { color: var(--mute); font-size: 12px; }
/* Hidden until the message is under the pointer or focused from the keyboard -
   a trash icon on every line is a line of noise for a control used rarely. */
.chat-del {
  border: 0; background: none; color: var(--mute); cursor: pointer;
  padding: 2px; line-height: 0; border-radius: 4px;
  opacity: 0;
}
.chat-msg:hover .chat-del, .chat-del:focus-visible { opacity: 1; }
.chat-del:hover { color: var(--danger); }

.chat-body {
  margin: 0;
  padding: 9px 12px;
  background: var(--fog-2);
  border: 1px solid var(--line);
  /* Asymmetric corners: the tight pair points at the speaker, which is what
     makes a stack of bubbles read as speech rather than as a form. */
  border-radius: 14px 14px 14px 4px;
  /* Long words and pasted urls wrap instead of widening the column. */
  white-space: pre-wrap;
  overflow-wrap: anywhere;
}
.chat-msg.is-mine .chat-body {
  background: var(--ink);
  color: var(--paper);
  border-color: var(--ink);
  border-radius: 14px 14px 4px 14px;
}
.chat-msg.is-grouped .chat-body { border-top-left-radius: 4px; }
.chat-msg.is-mine.is-grouped .chat-body { border-top-right-radius: 4px; }
.chat-body.is-deleted { color: var(--mute); font-style: italic; }

/* Live indicator next to the room title. */
.chat-top { position: sticky; top: 0; z-index: 3; background: var(--paper); }
.chat-title { min-width: 0; }
.chat-title .page-sub { display: flex; align-items: center; gap: 8px; }
.chat-live {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  padding: 1px 8px;
  font-size: 12px;
  color: var(--mute);
  border: 1px solid var(--line);
  border-radius: 999px;
}
.chat-live .dot { width: 6px; height: 6px; border-radius: 50%; background: var(--mute); }
.chat-live.is-on { color: var(--green, #1f8b4c); }
.chat-live.is-on .dot { background: currentColor; }

/* Floats over the log, centred, out of the way until there is something to say. */
.chat-jump {
  position: absolute;
  left: 50%;
  bottom: 14px;
  transform: translateX(-50%);
  z-index: 2;
  padding: 6px 16px;
  font: inherit;
  font-size: 13px;
  color: var(--paper);
  background: var(--ink);
  border: 0;
  border-radius: 999px;
  box-shadow: 0 4px 14px rgb(0 0 0 / .18);
  cursor: pointer;
}
.chat-jump:hover { opacity: .88; }

.chat-game {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  margin-top: 6px;
  padding: 5px 10px 5px 5px;
  border: 1px solid var(--line);
  border-radius: 8px;
  font-size: 13px;
}
.chat-game:hover { border-color: var(--ink); text-decoration: none; }
.chat-game-cover { width: 30px; height: 30px; object-fit: cover; border-radius: 5px; }

.chat-files { display: flex; flex-direction: column; gap: 4px; margin-top: 6px; }
.chat-file {
  display: inline-flex; align-items: center; gap: 6px;
  font-size: 13px; color: var(--mute);
}

.chat-more {
  align-self: center;
  border: 1px dashed var(--line);
  background: none;
  color: var(--mute);
  border-radius: 999px;
  padding: 5px 14px;
  cursor: pointer;
  font: inherit;
  font-size: 13px;
}
.chat-more:hover { color: var(--ink); border-color: var(--ink); }

.chat-composer { display: flex; gap: 8px; align-items: flex-end; flex-wrap: wrap; }
/* The field and the picker share a row so the send button stays aligned with the
   field rather than with the bottom of a two-line composer. */
.chat-compose-row { display: flex; flex: 1; gap: 6px; min-width: 0; }
.chat-attach {
  display: inline-flex;
  align-items: center;
  padding: 0 11px;
  color: var(--muted);
  border: 1px solid var(--line);
  border-radius: 8px;
  cursor: pointer;
}
.chat-attach:hover { color: var(--ink); border-color: var(--ink); }
.chat-attach:focus-within { outline: 2px solid var(--ink); outline-offset: 1px; }
.chat-pending {
  flex: 1 0 100%;
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 6px 10px;
  font-size: 13px;
  color: var(--muted);
  background: var(--fog);
  border: 1px solid var(--line);
  border-radius: 8px;
}
.chat-pending span { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; }
/* Available to the screen reader and not to the eye. The file input has to be in
   the form for the label to work, and a visible one would be a second, uglier
   control sitting next to the real one. */
.sr-only {
  position: absolute;
  width: 1px;
  height: 1px;
  margin: -1px;
  padding: 0;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}
.chat-input, .chat-kind {
  flex: 1;
  min-width: 0;
  padding: 10px 12px;
  background: var(--fog);
  color: var(--ink);
  border: 1px solid var(--line);
  border-radius: 8px;
  font: inherit;
}
.chat-input:focus, .chat-kind:focus { outline: 2px solid var(--ink); outline-offset: 1px; }
.chat-input:disabled { opacity: .55; }
/* The composer grows with what is typed into it (see chatAutoGrow) instead of
   scrolling inside itself: a message being written should stay visible in full. */
.chat-field {
  resize: none;
  overflow-y: auto;
  max-height: 160px;
  line-height: 1.45;
}
.chat-create { display: flex; gap: 8px; margin-bottom: 18px; }
/* The rows of people a name matched. They sit under the field rather than over
   it, so nothing that was typed is covered while a choice is made. */
.chat-hits { display: flex; flex-wrap: wrap; gap: 6px; margin: -10px 0 18px; }
.chat-hits:empty { display: none; }
.chat-chip-self { color: var(--muted); font-size: 12px; }

.chat-list { display: flex; flex-direction: column; border: 1px solid var(--line); border-radius: 10px; overflow: hidden; }
.chat-row {
  display: flex; align-items: center; justify-content: space-between; gap: 12px;
  padding: 13px 15px;
  border-bottom: 1px solid var(--line);
}
.chat-row:last-child { border-bottom: 0; }
.chat-row:hover { background: var(--fog); text-decoration: none; }
.chat-row-main { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
.chat-row-title { font-weight: 600; }
.chat-row-sub { color: var(--mute); font-size: 13px; }
.chat-row.is-unread .chat-row-title { font-weight: 700; }
.chat-badge {
  flex: none;
  min-width: 22px;
  height: 22px;
  padding: 0 7px;
  display: inline-flex; align-items: center; justify-content: center;
  background: var(--ink); color: var(--paper);
  border-radius: 999px; font-size: 12px; font-weight: 700;
}

@media (max-width: 900px) {
  .chat-msg { max-width: 90%; }
  .chat-log { height: 58vh; }
  .chat-create { flex-wrap: wrap; }
  .chat-create .chat-input { flex: 1 1 100%; order: 3; }
}
/* On a phone the log takes the screen instead of a slice of it: a chat that ends
   under the browser chrome is a chat with a composer you cannot reach. */
@media (max-width: 640px) {
  .chat-top { padding-top: 10px; }
  .chat-log { height: calc(100dvh - 260px); min-height: 240px; }
  .chat-log { padding-bottom: 56px; }
  .chat-jump { bottom: 10px; }
}
.chat-discover { margin-bottom: 18px; }
.chat-discover summary { cursor: pointer; color: var(--mute); font-size: 14px; }
.chat-discover-list { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 10px; }
.chat-chip {
  padding: 5px 11px;
  border: 1px solid var(--line);
  border-radius: 999px;
  font-size: 13px;
}
.chat-chip:hover { border-color: var(--ink); text-decoration: none; }
.chat-join { display: flex; justify-content: flex-end; }

/* ---- CheatLab Mail workspace ---- */
.mail-shell{display:grid;grid-template-columns:250px minmax(0,1fr);gap:18px;min-height:480px}
.mail-side,.mail-main{min-width:0;border:1px solid var(--line);background:color-mix(in srgb,var(--fog) 76%,transparent);backdrop-filter:blur(18px);-webkit-backdrop-filter:blur(18px)}
.mail-side{padding:14px;display:flex;flex-direction:column;gap:7px}
.mail-side-title{font:11px var(--mono);letter-spacing:.09em;color:var(--mute);padding:4px 7px 9px}
.mail-address,.mail-folder{display:flex;align-items:center;gap:9px;width:100%;text-align:left;padding:10px;border:1px solid transparent;background:transparent;color:var(--ink);cursor:pointer;font:13px var(--sans);min-width:0}
.mail-address span{min-width:0;overflow-wrap:anywhere}
.mail-address small{display:block;color:var(--mute);font-size:11px;margin-top:2px}
.mail-address:hover,.mail-folder:hover,.mail-address.is-active,.mail-folder.is-active{background:var(--fog-2);border-color:var(--line)}
.mail-add{width:100%;justify-content:center;margin-top:5px}
.mail-side-rule{height:1px;background:var(--line);margin:9px 0}
.mail-main{padding:20px;min-height:480px}
.mail-heading{display:flex;align-items:flex-start;justify-content:space-between;gap:16px}
.mail-limit{font:12px var(--mono);color:var(--mute);border:1px solid var(--line);padding:7px 10px}
.mail-toolbar{display:flex;align-items:center;justify-content:space-between;gap:12px;margin-bottom:18px}
.mail-toolbar h2{font-size:18px}
.mail-toolbar .hint{margin-top:4px}
.mail-server-pending,.mail-server-ok{display:flex;gap:8px;align-items:flex-start;padding:11px 13px;margin:0 0 14px;border:1px solid var(--line);font-size:12px;color:var(--mute);background:var(--paper)}
.mail-server-ok{color:var(--ink)}
.mail-list{border-top:1px solid var(--line)}
.mail-row{display:grid;grid-template-columns:minmax(100px,1fr) minmax(120px,2fr) auto;gap:14px;align-items:center;width:100%;padding:14px 9px;border:0;border-bottom:1px solid var(--line);background:transparent;color:var(--ink);text-align:left;cursor:pointer}
.mail-row:hover{background:var(--fog-2)}
.mail-row-from,.mail-row-subject{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-size:13px}
.mail-row-date{font:11px var(--mono);color:var(--mute);white-space:nowrap}
.mail-empty{min-height:360px;display:flex;flex-direction:column;align-items:center;justify-content:center;text-align:center;gap:12px;color:var(--mute);padding:30px}
.mail-empty h2{color:var(--ink)}
.mail-empty p{max-width:430px;font-size:13px}
.mail-empty-small{min-height:250px}
.mail-create,.mail-compose,.mail-message{max-width:760px;display:grid;gap:16px}
.mail-create h2{font-size:18px}
.mail-address-input{display:flex;align-items:center;gap:8px}
.mail-address-input .input{min-width:0;flex:1}
.mail-address-input span{font:12px var(--mono);white-space:nowrap;color:var(--mute)}
.mail-message h2{margin-bottom:10px}
.mail-message-body{white-space:pre-wrap;overflow-wrap:anywhere;line-height:1.65;margin-top:20px}
.mail-compose .textarea{min-height:180px;resize:vertical}
@media(max-width:760px){.mail-shell{grid-template-columns:1fr}.mail-side{display:grid;grid-template-columns:1fr 1fr}.mail-side-title,.mail-side-rule{grid-column:1/-1}.mail-add{grid-column:1/-1}.mail-main{padding:14px}.mail-row{grid-template-columns:1fr auto}.mail-row-subject{grid-column:1/-1;grid-row:2}.mail-row-date{grid-column:2;grid-row:1}.mail-heading{flex-wrap:wrap}.mail-address-input{flex-wrap:wrap}}

.mail-code-card{display:flex;align-items:center;gap:12px;flex-wrap:wrap;margin:18px 0;padding:15px;border:1px solid var(--line);background:var(--fog)}
.mail-code-card .label{margin-right:auto}
.mail-code-value{font:600 25px var(--mono);letter-spacing:.12em}


/* Gmail-like mail improvements */
.mail-search{width:100%;margin:0 0 14px}
.mail-actions{display:flex;align-items:center;gap:8px}
.mail-row{display:flex;gap:8px;align-items:center;padding:0 8px}
.mail-row-open{display:grid;grid-template-columns:minmax(110px,1fr) minmax(120px,2.2fr) auto;gap:14px;align-items:center;flex:1;min-width:0;padding:14px 0;border:0;background:transparent;color:var(--ink);text-align:left;cursor:pointer}
.mail-row.is-unread .mail-row-from,.mail-row.is-unread .mail-row-subject{font-weight:750}
.mail-row-subject{color:var(--mute)}
.mail-star{flex:none;border:0;background:transparent;color:var(--mute);padding:8px;cursor:pointer}
.mail-star.is-starred{color:#c28b19}
.mail-star.is-starred svg{fill:currentColor}
.mail-meta{display:grid;gap:4px;color:var(--mute);font-size:13px}
.mail-html-frame{width:100%;min-height:520px;border:1px solid var(--line);border-radius:12px;background:#fff;margin-top:18px}
.mail-message-body{padding:16px;border:1px solid var(--line);border-radius:12px;background:var(--paper)}
.mail-message{max-width:920px}
@media(max-width:760px){.mail-row-open{grid-template-columns:1fr auto;gap:5px}.mail-row-subject{grid-column:1/-1;grid-row:2}.mail-row-date{grid-column:2;grid-row:1}.mail-actions{gap:5px}.mail-html-frame{min-height:480px}}


/* Composer attachments and project sharing */
.mail-compose-tools{display:grid;grid-template-columns:1fr;gap:14px;padding:14px;border:1px solid var(--line);background:var(--fog)}
.mail-file-button{position:relative;display:inline-flex;align-items:center;gap:8px;width:max-content;max-width:100%;cursor:pointer}
.mail-file-button input[type=file]{position:absolute;inset:0;width:100%;height:100%;opacity:0;cursor:pointer}
.mail-project-share{display:grid;grid-template-columns:minmax(0,1fr) auto auto;gap:8px;align-items:center}
.mail-project-share strong,.mail-project-share .hint{grid-column:1/-1}
.mail-attachment-list{display:flex;flex-wrap:wrap;gap:8px}
.mail-attachment-chip{display:flex;align-items:center;gap:8px;max-width:100%;padding:7px 9px;border:1px solid var(--line);background:var(--paper);font-size:12px}
.mail-attachment-chip span{overflow-wrap:anywhere}
.mail-attachment-thumb{width:34px;height:34px;object-fit:cover;border-radius:5px}
.mail-attachment-remove{border:0;background:transparent;color:var(--mute);font-size:18px;cursor:pointer}
.mail-compose-footer{display:flex;justify-content:space-between;align-items:center;gap:10px}
@media(max-width:650px){.mail-project-share{grid-template-columns:1fr}.mail-project-share strong,.mail-project-share .hint{grid-column:1}.mail-compose-footer{align-items:flex-start;flex-direction:column}}
