/* ==========================================================================
   Splitscreen — application shell
   Desktop-first. The app is a fixed frame, not a scrolling page: three panes
   that scroll independently inside 100dvh. Flat throughout, per base.css —
   no gradients, no shadows. Depth comes from three planes of value:
   ink rail → bone-2 sidebar → bone canvas, with paper cards on top.
   ========================================================================== */

/* ---------- room accent ----------
   One variable drives every accent in a room, so a pillar is recoloured by
   the data-room attribute alone rather than by per-component overrides. */
.app[data-room="groups"] { --room: var(--p1); --room-ink: var(--p1-ink); --room-wash: var(--p1-wash); }
.app[data-room="social"] { --room: var(--p3); --room-ink: var(--p3-ink); --room-wash: var(--p3-wash); }
.app[data-room="dating"] { --room: var(--p2); --room-ink: var(--p2-ink); --room-wash: var(--p2-wash); }
/* messages belong to dating and wear its colour — they only exist because of a match */
.app[data-room="messages"] { --room: var(--p2); --room-ink: var(--p2-ink); --room-wash: var(--p2-wash); }

:root {
  --side-w: 268px;
  --bar-h:  62px;
  /* The nav strip that took the rail's place, and the bar's own side padding —
     the strip bleeds to the bar's edges with a negative margin of exactly it. */
  --nav-h:   46px;
  --bar-pad: clamp(1rem, 1.8vw, 1.9rem);

  /* How much of the bottom of the window the strip is holding. Zero here,
     because on a wide screen it is a line inside the top bar and holds none.
     Below the sidebar breakpoint it becomes a fixed tab bar and this is what
     everything else that lives near the bottom edge — the voice dock, the
     impersonation banner, the account page's save bar, the scroll pane's own
     last inch — subtracts to stay clear of it. One number, redefined once. */
  --tabbar: 0px;

  /* Default room accent, so components that reference --room (buttons, focus
     rings, field borders) still resolve on pages with no room — auth and
     onboarding load this file without an .app wrapper. */
  --room:      var(--p1);
  --room-ink:  var(--p1-ink);
  --room-wash: var(--p1-wash);
}

/* ==========================================================================
   Frame
   ========================================================================== */

html, body { height: 100%; }
/* `clip`, not `hidden`. Both paint the same, but `hidden` still makes body a
   scroll CONTAINER — it just has no scrollbar — so /account#dating_min_age
   scrolled the body and dragged the whole 100dvh frame off the top of the
   window, with no scrollbar left to bring it back. It looked like the page had
   broken. `clip` creates no scrollport at all, so the browser scrolls
   .main__scroll, which is the pane that is genuinely scrollable.
   The plain value stays in front as the fallback. */
body { overflow: hidden; }
body { overflow: clip; }

.app {
  display: grid;
  /* Two columns since 2026-08-22. The rooms used to be a 76px ink column here;
     they are a strip inside the top bar now, so the sidebar starts at the
     window's left edge and the room switcher is one piece of chrome, not two. */
  grid-template-columns: var(--side-w) minmax(0, 1fr);
  /* An explicit shrinkable row. Without it the single implicit row is sized
     auto, which means max-content — the shell then grows to the height of the
     longest pane instead of the viewport, and the panes never overflow because
     nothing ever constrained them. */
  grid-template-rows: minmax(0, 1fr);
  height: 100dvh;
  overflow: hidden;
}

/* Grid and flex items default to min-height:auto, which refuses to shrink
   below content. Every pane that is supposed to scroll needs this or it
   silently becomes as tall as its content and the scrollbar never appears. */
.rail, .side, .main { min-height: 0; }

.main { overflow: hidden; }
.side__body, .main__scroll { min-height: 0; }

/* With body no longer scrollable, an anchor scrolls this pane instead — so the
   target needs to clear the top bar rather than landing under it. */
.main__scroll [id] { scroll-margin-top: 1.2rem; }

@media (max-width: 1180px) { .app { grid-template-columns: 232px minmax(0, 1fr); } }
/* The sidebar goes away on a phone. `.app > .side` rather than `.side`, and
   that is load-bearing: a media query adds NO specificity, so the plain
   `.side { display: grid }` that defines the component further down this file
   used to win on source order alone and the sidebar never actually hid.

   Three in-flow children then went into a two-column grid — main wrapped onto
   an implicit auto row, that row took the whole 100dvh, and the explicit 1fr
   row holding the rail collapsed to 0. The nav survived only as padding with
   its buttons spilling out, which is what "cannot navigate on mobile" was.
   There are two children now and the nav is inside main, so the failure mode
   is gone — the selector stays, because hiding the sidebar still needs it. */
@media (max-width: 940px)  { .app { grid-template-columns: minmax(0, 1fr); } .app > .side { display: none; } }

/* Below the sidebar breakpoint the top bar has ~314px to hold a title, a
   search box and whatever action the room offers, and it was never going to.

   `main` is a grid that only ever declared its ROWS, so its single implicit
   column was auto — that is max-content, which sized itself to the bar's
   natural 403px. main's own box stayed 314px and `overflow: hidden` simply
   cropped the difference, taking the right-hand end of the bar with it. On
   Groups that is the button that starts a squad: present, laid out, and
   permanently past the right edge of the screen.

   Naming the column is the fix. The bar then has to actually fit, so it wraps
   and the search drops to a line of its own rather than being hidden — a
   search you cannot reach and a search that is not there look identical from
   the outside, but only one of them is honest. */
@media (max-width: 940px) {
  .main { grid-template-columns: minmax(0, 1fr); }

  /* `.main > .main__bar`, not `.main__bar`. The component rule below fixes the
     bar at exactly var(--bar-h), and a media query adds no specificity — so
     the bare selector lost, the bar stayed 62px, and the search it was told to
     wrap simply spilled out underneath and drew over the content. */
  .main > .main__bar {
    height: auto;
    min-height: var(--bar-h);
    flex-wrap: wrap;
    row-gap: 0.5rem;
    padding-top: 0.55rem;
    padding-bottom: 0.55rem;
  }

  /* The strip leaves this bar entirely below here — see the tab bar block in
     the nav section — so it is an ordinary one-line bar again. What that needs
     undoing is two rules down beside the ones it undoes, because they are the
     same specificity and source order is all that separates them. */

  /* A squad name is as long as its host made it, and the title does not wrap.
     On a phone that pushed the bar to 367px inside 314 and cropped the right
     of it. Wrapping beats an ellipsis here: the name IS the page, and the bar
     is already auto-height at this width. */
  .main > .main__bar .main__title { min-width: 0; white-space: normal; }

  /* Last, and full width, so the two things that were competing for one line
     now have one each. `.main__bar .search` and not `.search`: the component
     rule further down sets `flex: 0 1 320px`, and this is inside a media query
     — which adds no specificity — so the bare selector loses on source order.
     That is the same trap that hid the sidebar bug above. */
  .main__bar .search { order: 3; flex: 1 1 100%; }
}

/* shared scroll treatment for the panes */
.pane-scroll {
  /* A scrolling pane must be the containing block for what is inside it.

     Without this, any `position: absolute` descendant with no positioned
     ancestor resolves against the page instead, and an element positioned
     against the page is NOT clipped by this pane — it sticks out into the
     document at its static position, thousands of pixels down.

     That is not hypothetical. `.sr-only` is `position: absolute`, and the
     account page has three of them near the bottom of the form. They are one
     pixel tall and invisible, and they were stretching the document to 6260px
     against an 841px window. The frame is `height: 100dvh`, so nothing looked
     wrong until something scrolled the page: /account#dating_min_age jumped
     the viewport 62px — exactly one --bar-h — and dragged the top bar, the
     sidebar head and the rail's brand off the top of the window with no
     scrollbar to bring them back. It read as the page being broken.

     `body { overflow: clip }` below was the previous attempt at this and does
     not work: when body's overflow propagates to the viewport, `clip` is not
     carried across, so the viewport stays scrollable and body stops clipping.
     Measured, not assumed — every combination of hidden/clip on html and body
     leaves scrollY at 62. Containing the absolutes is the fix; the document is
     then exactly as tall as the window and there is nothing to scroll. */
  position: relative;
  overflow-y: auto;
  overscroll-behavior: contain;
  scrollbar-width: thin;
  scrollbar-color: var(--line-hard) transparent;
}
.pane-scroll::-webkit-scrollbar { width: 10px; }
.pane-scroll::-webkit-scrollbar-thumb {
  background: var(--line-hard);
  border: 3px solid transparent;
  background-clip: content-box;
  border-radius: 99px;
}
.pane-scroll::-webkit-scrollbar-thumb:hover { background: var(--ink-4); background-clip: content-box; }

/* ==========================================================================
   Nav — the room switcher. Switches rooms, nothing else.

   It was a 76px ink column down the left edge until 2026-08-22: icon-only,
   with the name of each room hidden in a hover tooltip. A tooltip is not a
   label — a phone has no hover, and an icon nobody can name is a control
   nobody presses. So the rooms say what they are, and the strip moved into
   the top bar rather than keeping a column of its own beside it.

   The class names did not change. `.rail` is what every theme, the admin
   layout and three separate places in app.js already reach for; renaming it
   would have been a rename in five files to describe the same element.
   ========================================================================== */

.rail {
  /* First child of .main__bar and the full width of it: the strip takes a line
     of its own and the title/search/actions row is unchanged underneath. The
     negative margin is exactly the bar's own padding, so the underline reaches
     the edges of the room instead of stopping inside the gutter.

     `flex: none` with an explicit width rather than `flex: 1 1 100%`: a
     growable item leaves the two gutters as free space on its line, and a page
     whose bar-title happens to be empty would squeeze onto this line beside
     it. Filling the padding box exactly leaves nothing to squeeze into — the
     next item costs the bar's 1.1rem gap before it costs anything of its own,
     so it always wraps. */
  flex: none;
  width: calc(100% + 2 * var(--bar-pad));
  height: var(--nav-h);
  margin: 0 calc(-1 * var(--bar-pad));
  padding: 0 var(--bar-pad);
  display: flex;
  align-items: center;
  gap: 0.75rem;
  border-bottom: 1px solid var(--line);
  background: var(--band);
  z-index: 3;
}

.rail__brand {
  width: 30px;
  height: 30px;
  border-radius: 8px;
  overflow: hidden;
  display: block;
  flex: none;
}
.rail__brand img { width: 100%; height: 100%; object-fit: cover; }

.rail__group { display: flex; align-items: center; gap: 0.3rem; }

/* The rooms give way first. Below the width where five named buttons fit they
   scroll sideways inside their own group — the account group beside them is
   `flex: none`, so sign out and your own settings stay reachable at every
   width. That is what the old rail's vertical scroll was for. */
.rail__group--rooms {
  flex: 1 1 auto;
  min-width: 0;
  overflow-x: auto;
  overscroll-behavior-x: contain;
  scrollbar-width: none;
}
.rail__group--rooms::-webkit-scrollbar { width: 0; height: 0; }

/* Pushed to the far end whatever the rooms do with the space in between. */
.rail__group--end { flex: none; margin-left: auto; }

.rail__btn {
  position: relative;
  flex: none;
  height: 34px;
  min-width: 34px;
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 0.44rem;
  padding: 0;
  border: 0;
  border-radius: 9px;
  background: transparent;
  /* bone-4 was legible as an icon and is not as a word: 5B616C on void is
     about 3:1, and these are the labels the whole change exists to show. */
  color: var(--bone-3);
  cursor: pointer;
  text-decoration: none;
  white-space: nowrap;
  transition: background 0.22s var(--ease), color 0.22s var(--ease);
}
.rail__btn svg { width: 19px; height: 19px; display: block; flex: none; }
.rail__btn:hover { background: var(--panel-2); color: var(--bone); }

/* a button that says its own name pays for the word with side padding */
.rail__btn--said { padding: 0 0.7rem; }
.rail__word {
  font-family: var(--font-body);
  font-size: 0.775rem;
  font-weight: 500;
  letter-spacing: 0.005em;
}

/* active room: solid colour block. The vertical rail also drew a bar bleeding
   off its left edge; horizontally that bar would hang below the strip and be
   clipped by the rooms' own scroll container, and the filled block was always
   the thing doing the work. */
.rail__btn.is-active { background: var(--accent); color: #fff; }
.rail__btn.is-active::before { content: none; }
.rail__btn--groups { --accent: var(--p1); }
.rail__btn--social { --accent: var(--p3); }
.rail__btn--dating { --accent: var(--p2); }
.rail__btn--messages { --accent: var(--p2); }

/* unread / activity pip — a chip after the word, not a badge over the icon.
   There is room for it on a line, and a count sitting on top of a label is a
   count covering the label. */
.rail__pip {
  min-width: 16px;
  height: 16px;
  padding: 0 4px;
  border-radius: 99px;
  background: var(--p2);
  color: #fff;
  font-family: var(--font-mono);
  font-size: 0.56rem;
  line-height: 16px;
  text-align: center;
  flex: none;
}
.rail__btn.is-active .rail__pip { background: var(--bone); color: var(--void); }

/* was a 26x2 line between two stacks; the same rule turned on its side */
.rail__rule { flex: none; width: 2px; height: 20px; background: #2C3037; margin: 0 0.35rem; }

/* tooltips — only the icon-only controls need one now, and it drops below the
   strip rather than out to the right, because there is nothing to its right */
.rail__btn::after {
  content: attr(data-label);
  position: absolute;
  top: calc(100% + 0.55rem);
  left: 50%;
  transform: translateX(-50%) translateY(-4px);
  white-space: nowrap;
  font-family: var(--font-body);
  font-size: 0.700rem;
  font-weight: 500;
  letter-spacing: 0.01em;
  padding: 0.42em 0.72em;
  border-radius: 5px;
  background: var(--panel-2);
  border: 1px solid var(--line-hard);
  color: var(--bone);
  opacity: 0;
  pointer-events: none;
  z-index: 20;
  transition: opacity 0.2s var(--ease), transform 0.2s var(--ease);
}
/* .main is overflow:hidden, so a centred tooltip on the last button would be
   cropped at the right edge of the room. The end group hangs its own inside. */
.rail__group--end .rail__btn::after { left: auto; right: 0; transform: translateY(-4px); }
.rail__btn:hover::after, .rail__btn:focus-visible::after { opacity: 1; transform: translateX(-50%) translateY(0); }
.rail__group--end .rail__btn:hover::after,
.rail__group--end .rail__btn:focus-visible::after { transform: translateY(0); }
/* a button with a visible word has nothing left to say on hover */
.rail__btn--said::after { content: none; }

/* the account button wears the user's own accent as its monogram tile */
.rail__initials {
  position: relative;
  width: 26px;
  height: 26px;
  border-radius: 7px;
  display: grid;
  place-items: center;
  background: var(--accent, var(--p1));
  color: #fff;
  font-family: var(--font-display);
  font-weight: 600;
  font-size: 0.70rem;
  letter-spacing: -0.01em;
}
.rail__btn.is-active .rail__initials { background: var(--bone); color: var(--void); }

/* the nav holds a logout <form>; without this it becomes a layout box and
   pushes the button out of the row */
.rail form { display: contents; }

/* ---------- the same strip, at the bottom of a phone ----------------------

   Below the sidebar breakpoint the strip leaves the top bar and pins itself to
   the bottom edge of the window as a tab bar. That is where a thumb is: the
   top of a phone held one-handed is the part of the screen the hand has to
   let go of the phone to reach, and the room switcher is the control this
   product asks for most often.

   Six tabs and no words: the five rooms and your own account. The strip
   carries ten things on a wide screen and every one of the other four is
   dropped here rather than scrolled — a tab bar that scrolls its first tab out
   of view is worse than one that names nothing, and ten icons across a 390px
   window is a scrolling tab bar however it is sliced. What goes and why is on
   the rule that drops them, sign out included.

   `position: fixed` and not a grid row of its own. Nothing between here and
   the viewport carries a transform, so the fixed box is genuinely the
   window's — .main's `overflow: hidden` clips descendants of .main, and a
   fixed element's containing block is not .main. Check that before adding a
   transform to the shell: it would trap this bar inside a 100dvh box that is
   already scrolled and the tabs would go with the page.
   -------------------------------------------------------------------------- */
@media (max-width: 940px) {
  /* Redefined once, read by everything that has to clear it.

     The safe-area term is part of the number rather than a term repeated at
     each call site. It resolves to 0 today: `env(safe-area-inset-*)` is only
     non-zero under `viewport-fit=cover`, which this layout's viewport meta
     does not ask for, so the browser letterboxes the home indicator itself
     and the bar already sits clear of it. It is here so that adding cover
     later moves the tabs, the dock, the banner and the save bar together
     instead of one at a time. */
  :root { --tabbar: calc(54px + env(safe-area-inset-bottom, 0px)); }

  .rail {
    position: fixed;
    left: 0; right: 0; bottom: 0;
    width: auto;
    height: var(--tabbar);
    margin: 0;
    padding: 0 0.4rem env(safe-area-inset-bottom, 0px);
    gap: 0.3rem;
    border-bottom: 0;
    border-top: 1px solid var(--line-hard);
    /* Over the voice dock, under the impersonation banner and under the
       deck's match dialog — a dialog that the nav could be tapped through is
       not a dialog. */
    z-index: 70;
  }

  /* Icons only down here. `.rail__btn--said` pays for its word with side
     padding, and there is no word to pay for. */
  .rail__word { display: none; }
  .rail__btn--said { padding: 0; }

  /* Six tabs, and they are the five rooms and you.

     The logo goes because a tab bar is for destinations and the logo is the
     way out to the marketing page — nobody signed in is looking for it, and
     it was costing a tab's worth of width. Incognito and the staff shield go
     because a phone is not where either gets used, and incognito is on the
     account page anyway, spelled out, which the icon never was.

     Sign out goes too, and that one has a cost worth writing down: there is
     now NO way to sign out on a phone. It is not on the account page — the
     Leave section there is the hard delete, which is a different thing
     entirely. If somebody asks for it back, the account page is where it
     should land rather than back in this bar. */
  .rail__brand,
  .rail__rule,
  .rail .a-scope,
  .rail__group--end .rail__btn:not(.rail__btn--me) { display: none; }

  /* The two groups stop being boxes and their buttons become direct children
     of the strip, so all six share one row and one set of gaps — five wide
     tabs with a small one pinned to the edge is what two groups would have
     drawn instead. Losing `--rooms`' sideways scroll with it is safe at six:
     they fit at their floor down to about 265px, narrower than any phone.
     Put anything back in this bar and the scroll has to come back with it. */
  .rail__group { display: contents; }

  /* 44px tall is the touch target the rest of this breakpoint already uses,
     and the width is shared evenly across whatever is left in the row.
     `flex-basis: 0` and not `auto`: the monogram tile is 28px against a 21px
     icon, so growing from content width would leave your own tab 7px wider
     than every room beside it. From zero they come out exactly equal. */
  .rail__btn { flex: 1 1 0; height: 44px; min-width: 40px; border-radius: 10px; }
  .rail__btn svg { width: 21px; height: 21px; }
  .rail__initials { width: 28px; height: 28px; }

  /* Back to a badge over the icon's corner. Inline after the word is right
     while there is a word; with the word gone it is a number sharing the box
     with the icon, and neither of them fits.

     Anchored to the middle of the button and not to its right edge: a tab is
     as wide as a sixth of the window here, and a badge in the corner of that
     would float a finger's width away from the icon it is counting for. The
     ring is the bar's own colour, so it reads as cut out rather than stuck
     on. */
  .rail__pip {
    position: absolute;
    top: 5px;
    left: calc(50% + 5px);
    right: auto;
    min-width: 15px;
    height: 15px;
    padding: 0 3px;
    border: 2px solid var(--band);
    font-size: 0.52rem;
    line-height: 11px;
  }

  /* The tooltip has to come up rather than down: below the button is off the
     bottom of the window. It is only reachable by keyboard focus at this
     width anyway — a phone has no hover — but a control whose name renders
     off-screen is one nobody can check. */
  .rail__btn::after { top: auto; bottom: calc(100% + 0.5rem); transform: translateX(-50%) translateY(4px); }
  .rail__btn:hover::after,
  .rail__btn:focus-visible::after { transform: translateX(-50%) translateY(0); }
  .rail__group--end .rail__btn::after { transform: translateY(4px); }
  .rail__group--end .rail__btn:hover::after,
  .rail__group--end .rail__btn:focus-visible::after { transform: translateY(0); }
}

/* ==========================================================================
   Sidebar — contextual to the room. Never shows another room's content.
   ========================================================================== */

.side {
  background: var(--band);
  border-right: 1px solid var(--line);
  display: grid;
  grid-template-rows: auto minmax(0, 1fr) auto;
  min-width: 0;
}

.side__head {
  padding: 1.15rem 1.15rem 1rem;
  border-bottom: 1px solid var(--line);
}
.side__eyebrow {
  font-family: var(--font-body);
  font-size: 0.670rem;
  font-weight: 500;
  letter-spacing: 0.01em;
  color: var(--room-ink);
  display: flex;
  align-items: center;
  gap: 0.55em;
  margin-bottom: 0.5rem;
}
.side__eyebrow::before { content: ''; width: 1.3em; height: 2px; background: var(--room); flex: none; }

.side__title {
  font-family: var(--font-display);
  font-weight: 600;
  font-size: 1.24rem;
  letter-spacing: -0.02em;
  line-height: 1.1;
  margin: 0;
}
.side__sub { font-size: 0.78rem; color: var(--ink-3); margin: 0.3rem 0 0; line-height: 1.4; }

.side__body { padding: 0.9rem 0.65rem 1.2rem; }

.side__sec { margin-bottom: 1.3rem; }
.side__sec:last-child { margin-bottom: 0; }

.side__label {
  font-family: var(--font-body);
  font-size: 0.670rem;
  font-weight: 500;
  letter-spacing: 0.01em;
  color: var(--ink-4);
  padding: 0 0.5rem 0.5rem;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 0.5rem;
}
.side__label b { font-weight: 400; color: var(--ink-4); }

.side__item {
  display: grid;
  grid-template-columns: auto minmax(0, 1fr) auto;
  align-items: center;
  gap: 0.6rem;
  width: 100%;
  /* the sidebar mixes <a> and <button>; without this the buttons arrive with
     UA border, background and centred text and break the row rhythm */
  font: inherit;
  text-align: left;
  background: none;
  border: 0;
  padding: 0.46rem 0.55rem;
  border-radius: 7px;
  border-left: 2px solid transparent;
  text-decoration: none;
  color: var(--ink-2);
  font-size: 0.875rem;
  line-height: 1.35;
  cursor: pointer;
  transition: background 0.2s var(--ease), color 0.2s var(--ease);
}
.side__item:hover { background: var(--paper); color: var(--ink); }
.side__item.is-active {
  background: var(--paper);
  color: var(--ink);
  font-weight: 500;
  border-left-color: var(--room);
}
.side__item span { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

/* the little square that stands in for a game / community icon */
.side__mark {
  position: relative;
  width: 22px;
  height: 22px;
  border-radius: 5px;
  flex: none;
  display: grid;
  place-items: center;
  background: var(--mk, var(--mk-ink));
  color: #fff;
  font-family: var(--font-mono);
  font-size: 0.58rem;
  font-weight: 500;
  letter-spacing: 0.02em;
}

/* the game mark — same box as .side__mark, but it may hold real artwork.
   Doubled class so it outranks .choice__mark, which lands later in the
   cascade because auth.css is linked after this file. */
.gmark {
  width: 22px;
  height: 22px;
  border-radius: 5px;
  flex: none;
  display: grid;
  place-items: center;
  overflow: hidden;
  background: var(--mk, var(--mk-ink));
  color: #fff;
  font-family: var(--font-mono);
  font-size: 0.58rem;
  font-weight: 500;
  letter-spacing: 0.02em;
}
/* artwork sits on a neutral tile: six brands' palettes side by side down a
   rail fight each other, and every one of them assumes it owns the page */
.gmark.gmark--art {
  background: var(--paper);
  box-shadow: inset 0 0 0 1px var(--line-soft);
  padding: 2px;
  color: transparent;
}
/* a complete app icon brings its own background — let it fill the tile
   rather than floating a small square inside a slightly bigger one */
.gmark.gmark--bleed { padding: 0; box-shadow: none; }
.gmark img { width: 100%; height: 100%; object-fit: contain; display: block; }
.gmark--lg { width: 24px; height: 24px; }
.gmark--inline { display: inline-grid; vertical-align: middle; }
/* inside a fact pill, where it takes the place of an icon */
.gmark--pill { width: 14px; height: 14px; border-radius: 3px; font-size: 0.44rem; }
.gmark--pill.gmark--art { padding: 1px; }

.side__count {
  font-family: var(--font-mono);
  font-size: 0.66rem;
  color: var(--ink-4);
  flex: none;
}
.side__item.is-active .side__count { color: var(--room-ink); }

/* Icons in the sidebar rows. These used to be geometric glyphs (◆ ◉ ⊘) on a
   coloured square — placeholder furniture that read as unfinished next to real
   game artwork. A drawn icon that takes the row's colour states the same thing
   without pretending to be a logo. */
.side__ico {
  width: 15px;
  height: 15px;
  flex: none;
  color: var(--bone-4);
  transition: color 0.2s var(--ease);
}
.side__item:hover .side__ico { color: var(--bone-2); }
.side__item.is-active .side__ico { color: var(--room); }

/* a live-now dot */
.dot-live {
  width: 7px; height: 7px;
  border-radius: 99px;
  background: var(--p3);
  flex: none;
}

/* sidebar footer — your own status, always visible */
.side__foot {
  border-top: 1px solid var(--line);
  padding: 0.8rem 0.9rem 0.9rem;
  background: var(--band);
}
.status {
  display: grid;
  grid-template-columns: auto minmax(0, 1fr);
  gap: 0.65rem;
  align-items: center;
}
.status__av {
  position: relative;
  width: 34px; height: 34px;
  border-radius: 8px;
  display: grid;
  place-items: center;
  background: var(--panel-2);
  color: var(--bone);
  font-family: var(--font-display);
  font-weight: 600;
  font-size: 0.82rem;
  flex: none;
}
.status__name { font-size: 0.85rem; font-weight: 500; line-height: 1.2; }
.status__meta {
  font-family: var(--font-body);
  font-size: 0.670rem;
  font-weight: 500;
  letter-spacing: 0.01em;
  color: var(--ink-3);
  display: flex;
  align-items: center;
  gap: 0.35em;
  margin-top: 0.2rem;
}

/* ==========================================================================
   Main pane
   ========================================================================== */

.main {
  display: grid;
  grid-template-rows: auto minmax(0, 1fr);
  min-width: 0;
  background: var(--void);
}

.main__bar {
  height: var(--bar-h);
  display: flex;
  align-items: center;
  gap: 1.1rem;
  padding: 0 var(--bar-pad);
  border-bottom: 1px solid var(--line);
  background: var(--void);
}

/* The bar that also carries the room switcher. Two lines by construction, not
   by wrapping: the strip is `flex: 1 1 100%` so it takes the first, and the
   title, search and actions keep the --bar-h row they have always had.
   `align-content: flex-start` and not the default stretch — stretch would
   share the leftover height between the lines and float the strip's underline
   away from the strip. */
.main__bar--nav {
  height: auto;
  min-height: calc(var(--nav-h) + var(--bar-h));
  flex-wrap: wrap;
  align-content: flex-start;
  row-gap: 0;
  padding-bottom: 0;
}
/* The second line has no box of its own to give a height to. The spacer is
   already an empty inert span sitting on it, so it is the one that carries the
   height — every other item then centres inside the line rather than being
   stretched to 62px and painting its contents against the top of it. */
.main__bar--nav .main__spacer { min-height: var(--bar-h); }

/* Below the sidebar breakpoint the strip is a fixed tab bar at the bottom of
   the window, so this is a one-line bar like every other one again.

   Both of these have to sit HERE, after the two rules they undo, and not up in
   the frame's own breakpoint block near the top of the file: they carry the
   same specificity as what they are overriding, and source order is the only
   thing that separates them. `stretch` is what the bar had before it learned
   to carry a second line — one line grown to the full 62px, with its contents
   centred in it by align-items rather than packed against the top. */
@media (max-width: 940px) {
  .main__bar--nav { align-content: stretch; }
  .main__bar--nav .main__spacer { min-height: 0; }
}

.main__title {
  font-family: var(--font-display);
  font-weight: 600;
  font-size: 1.02rem;
  letter-spacing: -0.014em;
  display: flex;
  align-items: baseline;
  gap: 0.6rem;
  white-space: nowrap;
}
.main__title em {
  font-style: normal;
  font-family: var(--font-body);
  font-size: 0.730rem;
  font-weight: 500;
  letter-spacing: 0.01em;
  color: var(--ink-3);
}

.main__spacer { flex: 1 1 auto; min-width: 0.5rem; }

/* search */
.search {
  position: relative;
  flex: 0 1 320px;
  min-width: 0;
}
.search input {
  width: 100%;
  font: inherit;
  font-size: 0.85rem;
  padding: 0.5em 0.8em 0.5em 2.1em;
  border-radius: 7px;
  border: 1px solid var(--line-hard);
  background: var(--paper);
  color: var(--ink);
  transition: border-color 0.22s var(--ease);
}
.search input::placeholder { color: var(--ink-4); }
.search input:focus { outline: none; border-color: var(--room); }
.search svg {
  position: absolute;
  left: 0.62em; top: 50%;
  transform: translateY(-50%);
  width: 14px; height: 14px;
  color: var(--ink-4);
  pointer-events: none;
}
.search kbd {
  position: absolute;
  right: 0.5em; top: 50%;
  transform: translateY(-50%);
  font-family: var(--font-mono);
  font-size: 0.6rem;
  padding: 0.25em 0.42em;
  border-radius: 4px;
  border: 1px solid var(--line);
  background: var(--band);
  color: var(--ink-4);
  pointer-events: none;
}

.main__scroll { padding: clamp(1.1rem, 2vw, 1.9rem) clamp(1rem, 1.8vw, 1.9rem) 3.5rem; }
.main__inner { max-width: 1720px; }

/* section head inside the scroll area */
/* The rule under a section head used to be 2px of near-white across the full
   width. One of those is a strong opening; the account page stacks thirteen,
   and they read as a ladder of bright bars with the content trapped between
   them. A hairline carries the division, and the seam — a short bar in the
   room's own colour, sitting on the rule at the left edge — is what marks the
   start of a section. It is the house motif doing the work the weight was
   doing, at a fraction of the volume. */
.blockhead {
  position: relative;
  display: flex;
  align-items: baseline;
  gap: 0.9rem;
  margin: 0 0 0.95rem;
  padding-bottom: 0.6rem;
  border-bottom: 1px solid var(--line);
}
.blockhead::after {
  content: '';
  position: absolute;
  left: 0;
  bottom: -1px;
  width: 34px;
  height: 2px;
  background: var(--room);
}
.blockhead h2 {
  font-family: var(--font-display);
  font-weight: 600;
  font-size: 1.12rem;
  letter-spacing: -0.016em;
  margin: 0;
}
.blockhead p {
  margin: 0;
  font-size: 0.82rem;
  color: var(--ink-3);
  max-width: none;
}
.blockhead .spacer { flex: 1 1 auto; }

.block { margin-bottom: 2.4rem; }
.block:last-child { margin-bottom: 0; }

/* ==========================================================================
   Filter bar
   ========================================================================== */

.filters {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.45rem;
  margin-bottom: 1.2rem;
}
.filter {
  font-family: var(--font-body);
  font-size: 0.780rem;
  font-weight: 500;
  letter-spacing: 0;
  padding: 0.46em 0.75em;
  border-radius: 6px;
  border: 1px solid var(--line-hard);
  background: var(--paper);
  color: var(--ink-2);
  cursor: pointer;
  white-space: nowrap;
  display: inline-flex;
  align-items: center;
  gap: 0.45em;
  transition: border-color 0.22s var(--ease), color 0.22s var(--ease), background 0.22s var(--ease);
}
.filter { text-decoration: none; }
.filter:hover { border-color: var(--room); color: var(--room-ink); }
.filter.is-on { background: var(--room); border-color: var(--room); color: #fff; }
.filter i { font-style: normal; opacity: 0.55; }
.filter.is-on i { opacity: 0.8; }

.filters__div { width: 1px; height: 20px; background: var(--line); margin: 0 0.3rem; }

/* ==========================================================================
   Card grids
   ========================================================================== */

/* `min(330px, 100%)` and not a bare 330px. A bare floor is a floor even when
   the container is narrower than it, so on a 282px phone pane every one of
   these grids laid out a track wider than the room it had and handed the
   overflow to a parent that clips. Wrapping the floor in min() lets the track
   collapse to the container instead, which is the difference between a card
   that reflows and a card with its right-hand third off screen. */
.cards { display: grid; gap: 0.9rem; grid-template-columns: repeat(auto-fill, minmax(min(330px, 100%), 1fr)); }
.cards--wide { grid-template-columns: repeat(auto-fill, minmax(min(410px, 100%), 1fr)); }
.cards--narrow { grid-template-columns: repeat(auto-fill, minmax(min(268px, 100%), 1fr)); }

/* ==========================================================================
   Squad card — Groups
   ========================================================================== */

.squad {
  display: grid;
  grid-template-rows: auto 1fr auto;
  /* grid items default to min-content width — without this a wide footer row
     pushes the card past its track and the right edge gets clipped */
  min-width: 0;
  border-radius: var(--radius);
  border: 1px solid var(--line);
  background: var(--paper);
  overflow: hidden;
  transition: border-color 0.24s var(--ease), transform 0.24s var(--ease);
}
.squad:hover { border-color: var(--line-hard); transform: translateY(-2px); }

/* The header used to be a solid block of the game's own colour. Those colours
   were chosen against a pale page — half of them are near-white on the dark
   theme, which put white text on a white banner. Now that every title has real
   artwork, the logo carries the identity and the header is a quiet raised
   strip, so six cards in a grid read as one system rather than six brands
   competing. */
.squad__top {
  display: flex;
  align-items: center;
  gap: 0.6rem;
  padding: 0.62rem 0.9rem;
  background: var(--panel-2);
  border-bottom: 1px solid var(--line);
}
.squad__game {
  font-family: var(--font-body);
  font-size: 0.82rem;
  font-weight: 500;
  letter-spacing: 0.005em;
  color: var(--bone);
}
.squad__mode {
  font-family: var(--font-body);
  font-size: 0.76rem;
  color: var(--bone-3);
  margin-left: auto;
}

.squad__body { padding: 0.95rem 1rem 0.6rem; display: grid; gap: 0.75rem; align-content: start; }

.squad__title {
  font-family: var(--font-display);
  font-weight: 600;
  font-size: 1.02rem;
  letter-spacing: -0.012em;
  line-height: 1.2;
  margin: 0;
}
.squad__host {
  font-size: 0.78rem;
  color: var(--ink-3);
  display: flex;
  align-items: center;
  gap: 0.4rem;
  flex-wrap: wrap;
}

/* roster: filled seats then empty ones, so "2 of 5" is readable at a glance */
.roster { display: flex; align-items: center; gap: 0.32rem; }
.seat {
  position: relative;
  width: 30px; height: 30px;
  border-radius: 7px;
  display: grid;
  place-items: center;
  font-family: var(--font-display);
  font-weight: 600;
  font-size: 0.74rem;
  color: #fff;
  background: var(--ink-3);
  flex: none;
}
.seat--open {
  background: transparent;
  border: 1.5px dashed var(--line-hard);
  color: var(--ink-4);
  font-family: var(--font-mono);
  font-size: 0.8rem;
  font-weight: 400;
}
.roster__n {
  font-family: var(--font-mono);
  font-size: 0.66rem;
  color: var(--ink-3);
  margin-left: 0.35rem;
}

/* meta grid: the four things that actually decide a good session */
.meta { display: grid; grid-template-columns: 1fr 1fr; gap: 0.5rem 0.9rem; }
.meta__k {
  font-family: var(--font-body);
  font-size: 0.640rem;
  font-weight: 500;
  letter-spacing: 0.01em;
  color: var(--ink-4);
  margin-bottom: 0.12rem;
}
.meta__v { font-size: 0.82rem; color: var(--ink); line-height: 1.3; }

/* availability overlap — weighted first in matching, so it gets a real bar */
.overlap { display: grid; gap: 0.3rem; }
.overlap__bar {
  height: 6px;
  border-radius: 99px;
  background: var(--band);
  overflow: hidden;
}
.overlap__fill { height: 100%; background: var(--p3); border-radius: 99px; }
.overlap__label {
  display: flex;
  justify-content: space-between;
  gap: 0.5rem;
  font-family: var(--font-body);
  font-size: 0.650rem;
  font-weight: 500;
  letter-spacing: 0.01em;
  color: var(--ink-3);
}
.overlap__label b { color: var(--p3-ink); font-weight: 500; }

.squad__foot {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.45rem;
  padding: 0.7rem 1rem 0.85rem;
  border-top: 1px solid var(--line-soft);
  background: var(--paper-2);
}

/* ==========================================================================
   Action panel + two-column body
   Shared between the squad pages and the admin area. They live here rather
   than in admin.css because the player app never loads that file — putting
   them there made the squad detail page silently lose its layout.
   ========================================================================== */

.a-act {
  border: 1px solid var(--line);
  border-radius: var(--radius);
  background: var(--paper);
  padding: 1.05rem 1.15rem 1.15rem;
  margin-bottom: 0.9rem;
}
.a-act--danger { border-color: var(--p2); }

/* Flex because base.css makes every svg display:block, which would otherwise
   drop an icon onto its own line above the heading rather than beside it.
   Harmless for the text-only headings, which are most of them. */
.a-act__h {
  font-family: var(--font-display);
  font-weight: 600;
  font-size: 1rem;
  letter-spacing: -0.012em;
  margin: 0 0 0.3rem;
  display: flex;
  align-items: center;
  gap: 0.45rem;
}
.a-act__h svg { width: 15px; height: 15px; flex: none; }
.a-act__d { font-size: 0.82rem; color: var(--ink-3); line-height: 1.5; margin: 0 0 0.9rem; max-width: 60ch; }

.a-grid2 { display: grid; gap: 1rem; }
@media (min-width: 1000px) {
  .a-grid2 { grid-template-columns: minmax(0, 1.35fr) minmax(0, 1fr); align-items: start; }
}

/* why a seat is not available to you — stated, not implied by a grey button */
.seatwhy {
  display: flex;
  align-items: flex-start;
  gap: 0.45rem;
  margin: 0;
  padding: 0.5rem 1rem 0.75rem;
  background: var(--paper-2);
  border-top: 1px solid var(--line-soft);
  font-size: 0.78rem;
  line-height: 1.4;
  color: var(--ink-3);
  max-width: none;
}
.seatwhy svg { width: 13px; height: 13px; flex: none; margin-top: 0.1rem; }

.squad__title a { text-decoration: none; color: inherit; }
.squad__title a:hover { color: var(--p1-ink); }

/* ==========================================================================
   Trust badges
   Colour alone never carries the meaning — every badge has a glyph and a
   word, because pine also reads as the Social room accent.
   ========================================================================== */

.badges { display: flex; flex-wrap: wrap; gap: 0.3rem; }

.badge {
  display: inline-flex;
  align-items: center;
  gap: 0.34em;
  font-family: var(--font-body);
  font-size: 0.700rem;
  font-weight: 500;
  letter-spacing: 0;
  padding: 0.3em 0.5em;
  border-radius: 5px;
  border: 1px solid var(--p3);
  background: var(--p3-wash);
  color: var(--p3-ink);
  white-space: nowrap;
}
.badge svg { width: 11px; height: 11px; flex: none; }
.badge--id { background: var(--bone); border-color: var(--bone); color: var(--void); }
.badge--muted { background: var(--paper); border-color: var(--line-hard); color: var(--ink-3); }

/* a plain fact pill — not a trust claim */
.pill {
  display: inline-flex;
  align-items: center;
  gap: 0.34em;
  font-family: var(--font-body);
  font-size: 0.720rem;
  font-weight: 500;
  letter-spacing: 0;
  padding: 0.3em 0.55em;
  border-radius: 5px;
  border: 1px solid var(--line);
  background: var(--band);
  color: var(--ink-2);
  white-space: nowrap;
}
.pill svg { width: 11px; height: 11px; flex: none; }
.pill--mic { border-color: var(--p1); color: var(--p1-ink); background: var(--p1-wash); }
.pill--text { border-color: var(--line-hard); }

/* An artist both people listen to. On the room accent rather than pine,
   because pine is reserved for verification — a shared taste in music is
   something to talk about, not something that has been checked. */
.pill--music { border-color: var(--room); color: var(--room-ink); background: var(--room-wash); }

/* A self-declared handle. Deliberately the quietest pill on the card: nothing
   here has been checked, so it must not borrow the look of anything that has.
   The platform name carries the weight; the handle is the detail. */
/* One square per platform, all identical. A row of equal tiles reads as one
   control repeated; a row of pills sized by the length of each platform's name
   reads as six unrelated things, and wrapped onto three lines to do it. */
.socials {
  display: flex;
  flex-wrap: wrap;
  gap: 0.4rem;
  align-items: center;
}

.smark-btn {
  position: relative;
  width: 34px;
  height: 34px;
  flex: none;
  display: grid;
  place-items: center;
  padding: 0;
  border-radius: 8px;
  border: 1px solid var(--line-hard);
  background: var(--band);
  color: var(--ink-3);
  text-decoration: none;
  cursor: pointer;
  appearance: none;
  font: inherit;
  transition: border-color 0.2s var(--ease), background 0.2s var(--ease);
}
.smark-btn:hover { border-color: var(--ink-3); background: var(--panel-2); }
.smark-btn:focus-visible { outline: 2px solid var(--room); outline-offset: 2px; }

/* The platform's own mark, in the platform's own colour.

   This is the one place a colour outside the palette is correct. Everywhere
   else a hue means something in OUR system — p1 is Groups, p2 is Dating, p3 is
   trust — but a logo does not belong to that system, and Discord rendered in
   cobalt is a lookalike rather than Discord. The colour is confined to the
   glyph: the pill, its border and the handle stay in the house palette, so the
   row still sits inside the design rather than becoming a sticker sheet. */
.smark { width: 16px; height: 16px; flex: none; display: block; }

.smark--discord   { color: #5865F2; }
.smark--twitch    { color: #9146FF; }
.smark--youtube   { color: #FF0000; }
.smark--instagram { color: #E4405F; }
.smark--tiktok    { color: #25F4EE; }
/* X's mark is black on light and white on dark; on this theme that is bone */
.smark--x         { color: var(--bone); }

/* With the handle gone there is no text to swap, so the confirmation is the
   tile itself: the logo steps aside for a tick. Both marks are stacked in the
   same cell so nothing moves and the row cannot reflow mid-copy. */
.smark-btn__ok {
  position: absolute;
  width: 16px;
  height: 16px;
  color: var(--p3);
  opacity: 0;
  transform: scale(0.7);
  transition: opacity 0.15s var(--ease), transform 0.15s var(--ease);
}
.smark-btn.is-copied { border-color: var(--p3); }
.smark-btn.is-copied .smark { opacity: 0; }
.smark-btn.is-copied .smark-btn__ok { opacity: 1; transform: scale(1); }

.smark-btn .smark { transition: opacity 0.15s var(--ease); }

@media (prefers-reduced-motion: reduce) {
  .smark-btn, .smark-btn .smark, .smark-btn__ok { transition: none; }
}

/* ==========================================================================
   Top artists — the Spotify strip on a card
   ========================================================================== */

.artists,
.played { display: grid; gap: 0.4rem; }

.artists__label,
.played__label {
  display: flex;
  align-items: center;
  gap: 0.4em;
  font-size: 0.68rem;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--ink-4);
}
.artists__label svg,
.played__label svg { width: 12px; height: 12px; flex: none; }
.artists__label b,
.played__label b { color: var(--room-ink); font-weight: 500; letter-spacing: 0; text-transform: none; }

/* The photo itself, in the one square both callers draw it in. Square rather
   than round: Spotify's artist images are square crops, and a circle takes a
   bite out of half of them. A tile takes `--radius-sm`, the same corner the
   rest of the page uses; the chip's own radius matches the pill it sits in. */
.artface {
  display: block;
  flex: none;
  object-fit: cover;
  background: var(--band);
  border: 1px solid var(--line);
}

/* A monogram, for an artist Spotify has no picture of and for every list read
   before the art existed. Centred with grid so one letter sits true in the box
   at either size. */
.artface--blank {
  display: grid;
  place-items: center;
  color: var(--ink-4);
  font-family: var(--font-display, inherit);
  font-weight: 600;
  line-height: 1;
  text-transform: uppercase;
}

.artface--tile { width: 100%; aspect-ratio: 1; border-radius: var(--radius-sm); }
.artface--tile.artface--blank { font-size: 1.6rem; }

.artface--chip { width: 16px; height: 16px; border-radius: 3px; margin-left: -0.12em; }
.artface--chip.artface--blank { font-size: 0.58rem; }

/* The account page's row: the art is the point there, so it gets tiles with the
   name underneath rather than a thumbnail in a pill. Equal columns that wrap on
   their own, so ten artists reflow to whatever the panel is wide — and a last
   row of three keeps its tiles the size of the ones above rather than growing
   to fill the space, which is what a flex row would have done. */
/* One grid, two lists. Music and played games sit one under the other on the
   account page, and two tile grids that almost match read worse than either
   would alone — so they are the same rules rather than a copy that drifts. */
.artists__tiles,
.played__tiles {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(86px, 1fr));
  gap: 0.85rem 0.7rem;
}

/* The tile is now a frame rather than the link itself: the eye that hides one
   entry is a form, and a form inside an anchor is not valid HTML — a button
   inside one is a button people cannot press. So the anchor moved inward and
   the tile became the thing they are both positioned against. */
.artists__tile,
.played__tile {
  position: relative;
  min-width: 0;
}

.tile__link {
  display: grid;
  gap: 0.4rem;
  text-decoration: none;
  color: inherit;
  min-width: 0;
}

/* The tile links out to the artist on Spotify, so it lifts the way the rest of
   the site's cards do. A tile with no link (there is no URL stored for every
   artist) is the same tile without the hover, which is correct. */
a.tile__link:hover .artface,
a.tile__link:hover .gameface { border-color: var(--room); }
a.tile__link:hover .artists__name,
a.tile__link:hover .played__name { color: var(--ink); }
.tile__link .artface,
.tile__link .gameface { transition: border-color 0.15s var(--ease); }

/* ---- hiding one entry ---------------------------------------------------

   The switch under each list is all-or-nothing, which is the right control for
   "I would rather not have my music on here" and the wrong one for the usual
   complaint, which is one entry. Left with only the big switch, the answer to
   one awkward row is to hide nine good ones with it. */

.tile__eye {
  position: absolute;
  top: 5px;
  right: 5px;
  line-height: 0;
}

/* Always drawn, never hover-only: a control that appears on hover is a control
   that does not exist on a phone. It sits on its own scrim because it lands on
   artwork whose colours nothing here chooses — a bare glyph is invisible on
   half the box art in a Steam library. */
.eyeb {
  display: grid;
  place-items: center;
  width: 22px;
  height: 22px;
  padding: 0;
  border: 0;
  border-radius: 50%;
  cursor: pointer;
  color: #fff;
  background: rgba(6, 8, 12, 0.62);
  backdrop-filter: blur(2px);
  opacity: 0.82;
  transition: opacity 0.15s var(--ease), background 0.15s var(--ease);
}
.eyeb svg { width: 13px; height: 13px; }
.eyeb:hover { opacity: 1; background: rgba(6, 8, 12, 0.86); }
.eyeb:focus-visible { opacity: 1; outline: 2px solid var(--p1); outline-offset: 2px; }

/* A hidden entry stays on this page — it is the only place it can be turned
   back on — and says so by going grey rather than by leaving a gap. Its own
   eye stays at full strength, because it is the way back. */
.tile--off .artface,
.tile--off .gameface { opacity: 0.3; filter: grayscale(1); }
.tile--off .artists__name,
.tile--off .played__name { opacity: 0.55; }
.tile--off .played__hrs { opacity: 0.55; font-weight: 500; }
.tile--off .eyeb { opacity: 1; }

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

/* A section waiting on the server. Dimmed rather than blanked or spinner'd:
   what is on screen is still true until the answer lands, and a settings page
   that empties a list for 200ms every time an eye is pressed flickers. */
.block.is-working { opacity: 0.62; transition: opacity 0.12s var(--ease); }
.block.is-working .eyeb { cursor: progress; }

@media (prefers-reduced-motion: reduce) {
  .block.is-working { transition: none; }
}

.artists__name,
.played__name {
  font-size: 0.72rem;
  font-weight: 500;
  color: var(--ink-2);
  line-height: 1.25;
  /* Two lines, then an ellipsis: a long name must not push its tile taller
     than the one beside it and stagger the row. */
  display: -webkit-box;
  -webkit-line-clamp: 2;
  line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
  /* A name with no spaces in it is still narrower than its tile. */
  overflow-wrap: anywhere;
}

@media (prefers-reduced-motion: reduce) {
  .tile__link .artface,
  .tile__link .gameface { transition: none; }
}

/* ==========================================================================
   Buttons
   ========================================================================== */

.b {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 0.45em;
  font-family: var(--font-body);
  font-size: 0.740rem;
  font-weight: 500;
  letter-spacing: 0.01em;
  text-decoration: none;
  padding: 0.66em 1em;
  border-radius: 6px;
  border: 1px solid var(--bone);
  background: var(--bone);
  color: var(--void);
  cursor: pointer;
  white-space: nowrap;
  transition: background 0.24s var(--ease), border-color 0.24s var(--ease), color 0.24s var(--ease);
}
.b:hover { background: var(--room); border-color: var(--room); }
.b svg { width: 13px; height: 13px; }

.b--room { background: var(--room); border-color: var(--room); }
.b--room:hover { background: var(--bone); border-color: var(--bone); color: var(--void); }

.b--ghost { background: transparent; color: var(--ink-2); border-color: var(--line-hard); }
.b--ghost:hover { background: var(--paper); color: var(--room-ink); border-color: var(--room); }

.b--sm { padding: 0.5em 0.75em; font-size: 0.63rem; }
.b--grow { flex: 1 1 auto; }
.b:disabled { opacity: 0.4; cursor: not-allowed; }
.b:disabled:hover { background: var(--bone); border-color: var(--bone); }

/* ==========================================================================
   Person card — Dating
   Gamertag before identity: the avatar is a monogram, never a photo, and
   clips stand in for selfies.
   ========================================================================== */

.person {
  border-radius: var(--radius);
  border: 1px solid var(--line);
  background: var(--paper);
  overflow: hidden;
  display: grid;
  grid-template-rows: auto 1fr auto;
  min-width: 0;
  transition: border-color 0.24s var(--ease), transform 0.24s var(--ease);
}
.person:hover { border-color: var(--line-hard); transform: translateY(-2px); }

/* clip strip stands where a photo would be on any other dating product */
.person__clip {
  height: 118px;
  background: var(--band);
  position: relative;
  display: grid;
  place-items: center;
  border-bottom: 1px solid var(--line-soft);
}
.person__clip .glyph {
  width: 100%;
  height: 100%;
  background:
    linear-gradient(90deg, var(--p1) 0 34%, transparent 34%) 0 26% / 100% 10px no-repeat,
    linear-gradient(90deg, var(--ink-4) 0 62%, transparent 62%) 0 50% / 100% 10px no-repeat,
    linear-gradient(90deg, var(--p2) 0 46%, transparent 46%) 0 74% / 100% 10px no-repeat;
  opacity: 0.5;
}
.person__cliptag {
  position: absolute;
  left: 0.6rem; bottom: 0.6rem;
  font-family: var(--font-body);
  font-size: 0.650rem;
  font-weight: 500;
  letter-spacing: 0.01em;
  padding: 0.3em 0.55em;
  border-radius: 4px;
  background: var(--void);
  color: var(--bone);
}

.person__body { padding: 0.9rem 1rem 0.7rem; display: grid; gap: 0.7rem; align-content: start; }

.person__id { display: grid; grid-template-columns: auto minmax(0, 1fr); gap: 0.7rem; align-items: center; }
.person__av {
  position: relative;
  width: 42px; height: 42px;
  border-radius: 9px;
  display: grid;
  place-items: center;
  background: var(--p2);
  color: #fff;
  font-family: var(--font-display);
  font-weight: 600;
  font-size: 1rem;
  flex: none;
}

/* A face in the avatar once somebody has uploaded one — the monogram was still
   showing over their own photograph, which read as the upload not having
   worked. It lies on top of the initials rather than replacing them, so a shot
   that fails to load, or one the viewer is not entitled to fetch, falls back to
   the letters instead of leaving a hole.

   `inherit` keeps each avatar its own shape — a circle on the deck card, a
   rounded square in the grid. Deliberately not clipped with `overflow: hidden`:
   the deck card's age badge hangs off the edge of the circle by design. */
.av__shot {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  border-radius: inherit;
  display: block;
  /* the deck drags the whole card to like or pass; the browser's native image
     drag would start a ghost drag and swallow the gesture */
  -webkit-user-drag: none;
  user-select: none;
}
.person__tag {
  font-family: var(--font-display);
  font-weight: 600;
  font-size: 1rem;
  letter-spacing: -0.012em;
  line-height: 1.2;
  margin: 0;
}
.person__sub {
  font-family: var(--font-body);
  font-size: 0.720rem;
  font-weight: 500;
  letter-spacing: 0;
  color: var(--ink-3);
  margin-top: 0.18rem;
}

/* age sits with the gamertag, in the same weight as a fact rather than a
   headline — it is information, not a selling point */
.person__age {
  font-family: var(--font-mono);
  font-size: 0.8rem;
  font-weight: 400;
  color: var(--ink-3);
  letter-spacing: 0.02em;
  margin-left: 0.15em;
}

/* the three prompts replace a bio */
.prompt {
  border-left: 3px solid var(--room);
  padding: 0.1rem 0 0.1rem 0.65rem;
}
.prompt__q {
  font-family: var(--font-body);
  font-size: 0.640rem;
  font-weight: 500;
  letter-spacing: 0.01em;
  color: var(--ink-4);
  margin-bottom: 0.16rem;
}
.prompt__a { font-size: 0.86rem; line-height: 1.42; color: var(--ink-2); }

/* the quiet way out of a roster card — present, never the loudest thing on it */
.card__report {
  display: inline-flex;
  align-items: center;
  gap: 0.4em;
  margin-top: 0.7rem;
  font-size: 0.68rem;
  color: var(--ink-4);
  text-decoration: none;
}
.card__report:hover { color: var(--p2); }
.card__report svg { width: 11px; height: 11px; flex: none; }

.person__foot {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.45rem;
  padding: 0.7rem 1rem 0.85rem;
  border-top: 1px solid var(--line-soft);
  background: var(--paper-2);
}

/* ==========================================================================
   Community card — Social
   ========================================================================== */

.commu {
  border-radius: var(--radius);
  border: 1px solid var(--line);
  background: var(--paper);
  overflow: hidden;
  display: grid;
  grid-template-rows: auto 1fr auto;
  min-width: 0;
  transition: border-color 0.24s var(--ease), transform 0.24s var(--ease);
}
.commu:hover { border-color: var(--line-hard); transform: translateY(-2px); }

/* The card's header. With no game behind it this is the 8px colour strip it
   has always been; with one it grows to hold that game's key art.
   The aspect is Steam's capsule ratio, which every file in
   assets/img/games/hero was cropped to, so `cover` never actually has anything
   to cut — it is there for the half-second before the picture arrives and for
   the day somebody adds a file at the wrong size. The colour underneath is
   what fills the banner until then, so a slow connection sees the card it
   used to see rather than a hole. */
.commu__banner {
  height: 8px;
  background: var(--c, var(--p3));
}
.commu__banner--art {
  height: auto;
  aspect-ratio: 460 / 215;
  overflow: hidden;
}
.commu__banner .ghero {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
}
.commu__body { padding: 0.95rem 1rem 0.7rem; display: grid; gap: 0.62rem; align-content: start; }
.commu__name {
  font-family: var(--font-display);
  font-weight: 600;
  font-size: 1.02rem;
  letter-spacing: -0.012em;
  line-height: 1.2;
  margin: 0;
}
.commu__desc { font-size: 0.85rem; color: var(--ink-2); line-height: 1.45; margin: 0; max-width: none; }
.commu__stats {
  display: flex;
  flex-wrap: wrap;
  gap: 0.28rem 1rem;
  font-family: var(--font-body);
  font-size: 0.720rem;
  font-weight: 500;
  letter-spacing: 0;
  color: var(--ink-3);
}
.commu__stats b { color: var(--ink); font-weight: 500; }
.commu__foot {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.45rem;
  padding: 0.7rem 1rem 0.85rem;
  border-top: 1px solid var(--line-soft);
  background: var(--paper-2);
}

/* next casual night — a standing weekly slot */
.night {
  display: grid;
  grid-template-columns: auto minmax(0, 1fr);
  gap: 0.7rem;
  align-items: center;
  padding: 0.6rem 0.7rem;
  border-radius: var(--radius-sm);
  background: var(--band);
  border: 1px solid var(--line-soft);
}
.night__when {
  width: 46px;
  text-align: center;
  font-family: var(--font-mono);
  flex: none;
}
.night__d {
  font-size: 0.56rem;
  letter-spacing: 0.13em;
  text-transform: uppercase;
  color: var(--ink-3);
}
.night__t { font-size: 0.84rem; color: var(--ink); font-weight: 500; }
.night__what { font-size: 0.82rem; line-height: 1.35; }
.night__what b { font-weight: 600; }
.night__what em { font-style: normal; color: var(--ink-3); display: block; font-size: 0.75rem; margin-top: 0.1rem; }

/* ==========================================================================
   Session room strip — the scheduled weekly window
   ========================================================================== */

.session {
  display: grid;
  grid-template-columns: auto minmax(0, 1fr) auto;
  gap: 1.1rem;
  align-items: center;
  padding: 1rem 1.15rem;
  border-radius: var(--radius);
  background: var(--bone);
  color: var(--void);
}
.session__clock {
  font-family: var(--font-mono);
  font-size: 1.35rem;
  letter-spacing: -0.01em;
  color: var(--void);
  text-align: center;
  line-height: 1.1;
}
.session__clock em {
  display: block;
  font-style: normal;
  font-size: 0.56rem;
  letter-spacing: 0.15em;
  text-transform: uppercase;
  color: var(--on-ink-2);
  margin-top: 0.25rem;
}
.session__t {
  font-family: var(--font-display);
  font-weight: 500;
  font-size: 1.1rem;
  letter-spacing: -0.016em;
  margin: 0 0 0.22rem;
  color: var(--void);
}
.session__s { font-size: 0.83rem; color: var(--on-ink-2); margin: 0; max-width: 62ch; }
.session .b { border-color: var(--void); background: var(--void); color: var(--bone); }
.session .b:hover { background: var(--p2-deep); border-color: var(--p2-deep); color: #fff; }

/* ==========================================================================
   Empty / utility
   ========================================================================== */

.empty {
  padding: 2.6rem 1.5rem;
  text-align: center;
  border-radius: var(--radius);
  border: 1px solid var(--line);
  background: var(--paper-2);
  color: var(--ink-3);
  font-size: 0.88rem;
}

.roomnote {
  display: flex;
  align-items: flex-start;
  gap: 0.7rem;
  padding: 0.75rem 0.95rem;
  border-radius: var(--radius-sm);
  background: var(--room-wash);
  border-left: 3px solid var(--room);
  font-size: 0.82rem;
  line-height: 1.5;
  color: var(--ink-2);
  margin-bottom: 1.2rem;
}
.roomnote svg { width: 15px; height: 15px; color: var(--room-ink); flex: none; margin-top: 0.12rem; }
.roomnote b { color: var(--ink); font-weight: 600; }

/* ==========================================================================
   Voice room (§10)
   ========================================================================== */

.vc__status {
  font-family: var(--font-body);
  font-size: 0.710rem;
  font-weight: 500;
  letter-spacing: 0.01em;
  color: var(--ink-3);
  margin: 0 0 0.8rem;
  display: flex;
  align-items: center;
  gap: 0.5em;
}
.vc__status::before {
  content: '';
  width: 7px; height: 7px;
  border-radius: 99px;
  background: var(--ink-4);
  flex: none;
}
[data-state="live"] .vc__status { color: var(--p3-ink); }
[data-state="live"] .vc__status::before { background: var(--p3); }
[data-state="connecting"] .vc__status::before { background: var(--gold); }

.vc__panel { margin-bottom: 0.9rem; }

.vc__roster { list-style: none; margin: 0; padding: 0; display: grid; gap: 0.35rem; }
.vc__person {
  display: grid;
  grid-template-columns: auto minmax(0, 1fr) auto;
  align-items: center;
  gap: 0.6rem;
  padding: 0.4rem 0.55rem;
  border-radius: 7px;
  background: var(--band);
}
.vc__person.is-muted { opacity: 0.6; }

.vc__av {
  position: relative;
  width: 26px; height: 26px;
  border-radius: 6px;
  display: grid;
  place-items: center;
  color: #fff;
  font-family: var(--font-display);
  font-weight: 600;
  font-size: 0.66rem;
  flex: none;
}
.vc__tag { font-size: 0.86rem; font-weight: 500; min-width: 0; }
.vc__tag em { font-style: normal; color: var(--ink-4); font-weight: 400; }
.vc__mic {
  font-family: var(--font-body);
  font-size: 0.650rem;
  font-weight: 500;
  letter-spacing: 0.01em;
  color: var(--p3-ink);
}
.vc__person.is-muted .vc__mic { color: var(--ink-4); }

.vc__controls { display: flex; flex-wrap: wrap; gap: 0.45rem; }
.vc__controls .is-on { background: var(--p2); border-color: var(--p2); color: #fff; }

/* ==========================================================================
   Impersonation banner
   Lives here rather than in admin.css because it appears over the player app,
   which is the whole point: an admin borrowing an account must never be able
   to forget whose screen they are looking at.
   ========================================================================== */

.imp {
  position: fixed;
  left: 0; right: 0; bottom: 0;
  z-index: 80;
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.6rem 1rem;
  padding: 0.6rem clamp(1rem, 2vw, 1.6rem);
  background: var(--p2);
  color: #fff;
  font-family: var(--font-body);
  font-size: 0.750rem;
  font-weight: 500;
  letter-spacing: 0.01em;
}
.imp b { font-weight: 500; }
.imp svg { width: 14px; height: 14px; flex: none; }
.imp form { margin-left: auto; }
.imp button {
  font-family: var(--font-body);
  font-size: 0.720rem;
  font-weight: 500;
  letter-spacing: 0.01em;
  padding: 0.45em 0.9em;
  border-radius: 5px;
  border: 1px solid #fff;
  background: #fff;
  color: var(--p2-ink);
  cursor: pointer;
}
.imp button:hover { background: var(--void); border-color: var(--void); color: var(--bone); }

/* the banner must not cover the last row of a scrolling pane */
body:has(.imp) .main__scroll { padding-bottom: 5rem; }

/* ==========================================================================
   Messages — the inbox, and one thread
   ========================================================================== */

.threads { display: grid; gap: 0.5rem; }

.thread {
  display: grid;
  grid-template-columns: auto 1fr auto;
  gap: 0.85rem;
  align-items: center;
  padding: 0.8rem 0.9rem;
  border: 1px solid var(--line);
  border-radius: var(--radius-sm);
  background: var(--paper);
  text-decoration: none;
  color: inherit;
  transition: border-color 0.2s var(--ease), transform 0.2s var(--ease);
}
.thread:hover { border-color: var(--line-hard); transform: translateY(-1px); }
/* expired threads stay readable and stay put — a match that vanished with no
   explanation is worse than one that says it ran out */
.thread--gone { opacity: 0.6; }

.thread__body { min-width: 0; display: grid; gap: 0.18rem; }
.thread__tag { display: flex; align-items: center; gap: 0.5em; font-size: 0.9rem; }
.thread__last {
  font-size: 0.8rem;
  color: var(--ink-3);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.thread__when { font-size: 0.68rem; color: var(--ink-4); white-space: nowrap; }
.thread__new {
  font-family: var(--font-mono);
  font-size: 0.55rem;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--p2-ink);
  background: var(--p2-wash);
  border-radius: 3px;
  padding: 0.25em 0.5em;
}

.msgs { display: grid; gap: 0.7rem; margin-bottom: 1.4rem; }

.msg { max-width: min(42rem, 82%); display: grid; gap: 0.28rem; }
.msg--mine { margin-left: auto; justify-items: end; }

.msg__body {
  padding: 0.65rem 0.85rem;
  border-radius: var(--radius-sm);
  border: 1px solid var(--line);
  background: var(--paper-2);
  font-size: 0.9rem;
  line-height: 1.55;
  white-space: pre-wrap;
  overflow-wrap: anywhere;
}
.msg--mine .msg__body { background: var(--room-wash); border-color: var(--room); }
.msg__meta { font-size: 0.65rem; color: var(--ink-4); }

.compose textarea { resize: vertical; min-height: 5rem; }

/* ==========================================================================
   Group chat — squads and communities

   Shares .msgs / .msg with the matched-thread view, because a message is a
   message. What differs is that this one is a live log INSIDE a page rather
   than being the page: it scrolls in its own box, so the compose field stays
   put while the conversation moves, and the rest of the squad stays reachable.
   ========================================================================== */

.chat__log {
  max-height: min(26rem, 55vh);
  overflow-y: auto;
  /* room for the scrollbar so the last character of a long line is not under
     it */
  padding-right: 0.5rem;
  margin-bottom: 1rem;
}
.chat__log:focus-visible { outline-offset: 4px; }

/* Written by the poller, never by the server — see the note in the partial. */
.chat__state {
  font-family: var(--font-mono);
  font-size: 0.55rem;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--ink-4);
}

/* Many voices, so who said it is a chip rather than a name in faint ink. The
   DM thread has two people in it and needs neither. */
.msg__meta { display: flex; align-items: center; gap: 0.4rem; }
.msg--mine .msg__meta { justify-content: flex-end; }

.msg__who {
  position: relative;
  width: 18px;
  height: 18px;
  border-radius: 4px;
  flex: none;
  display: grid;
  place-items: center;
  background: var(--mk, var(--mk-ink));
  color: #fff;
  font-family: var(--font-mono);
  font-size: 0.5rem;
  font-weight: 500;
  overflow: hidden;
}

/* A dealbreaker with no off position — age and gender.

   Deliberately quieter than a ticked switch rather than louder. It is saying
   "this one is already on and there is nothing to decide", so it should not
   compete for attention with the four next to it that do want a decision. The
   box is filled the way a checked one is, because it IS on; the card behind it
   is not, because there is nothing to click. */
.choice--locked {
  cursor: default;
  border-style: dashed;
  background: var(--band);
}
.choice--locked:hover { border-color: var(--line); }
.choice--locked .choice__box {
  background: var(--line-hard);
  border-color: var(--line-hard);
}
.choice--locked .choice__box svg { opacity: 1; }
.choice--locked .choice__t { color: var(--ink-2); }

.chat__compose textarea { min-height: 3.4rem; }
.chat__compose .form__actions { margin-top: 0.9rem; padding-top: 0.9rem; }
.chat__hint { font-size: 0.65rem; color: var(--ink-4); }

/* ==========================================================================
   Dating age range — two boxes and a two-handled slider over the same pair

   The handles are two overlaid <input type="range">, which is what buys arrow
   keys, Home/End and a value announcement for nothing. The trick that makes
   two of them behave as one control: the inputs themselves take no pointer
   events, only their thumbs do, so a press between the handles falls through
   to the page instead of teleporting whichever input happens to be on top.

   --lo-pct / --hi-pct are written by agerange.js. Everything positioned below
   reads them, so one assignment moves the fill, both bubbles and nothing else.
   ========================================================================== */

.agerange__slider {
  --thumb: 20px;
  position: relative;
  height: 74px;
  margin: 0.1rem 0 1.2rem;
}

/* The thumb's centre does not travel the full width — it starts half a thumb
   in and ends half a thumb short — so a plain percentage drifts away from the
   handle at both ends. This is that same journey. */
.agerange__slider {
  --lo-x: calc(var(--lo-pct) * 1% - var(--lo-pct) * var(--thumb) / 100 + var(--thumb) / 2);
  --hi-x: calc(var(--hi-pct) * 1% - var(--hi-pct) * var(--thumb) / 100 + var(--thumb) / 2);
}

.agerange__rail {
  position: absolute;
  left: 0;
  right: 0;
  top: 38px;
  height: 5px;
  border-radius: 99px;
  background: var(--line-hard);
}

.agerange__fill {
  position: absolute;
  top: 0;
  bottom: 0;
  left: var(--lo-x);
  right: calc(100% - var(--hi-x));
  border-radius: 99px;
  background: var(--p2);
}

/* The number rides the handle. Two of them meeting in the middle overlap
   rather than dodge — a bubble that jumps sideways to avoid its neighbour
   moves while the value under it does not, which reads as a bug. */
.agerange__bubble {
  position: absolute;
  top: 0;
  transform: translateX(-50%);
  min-width: 2.4rem;
  padding: 0.2rem 0.4rem;
  border-radius: var(--radius-sm);
  background: var(--p2);
  color: #fff;
  font-family: var(--font-mono);
  font-size: 0.72rem;
  font-weight: 500;
  text-align: center;
  line-height: 1.4;
}
.agerange__bubble--lo { left: var(--lo-x); }
.agerange__bubble--hi { left: var(--hi-x); }

.agerange__grab {
  position: absolute;
  top: 30px;
  left: 0;
  width: 100%;
  height: var(--thumb);
  margin: 0;
  padding: 0;
  background: none;
  border: 0;
  -webkit-appearance: none;
  appearance: none;
  /* see the note at the top of this block */
  pointer-events: none;
}
.agerange__grab--lo { z-index: 3; }
.agerange__grab--hi { z-index: 4; }
/* Both handles at the ceiling stack, and the one underneath cannot be
   grabbed — so at that one position the lower handle comes to the top. */
.is-lo-on-top .agerange__grab--lo { z-index: 5; }

.agerange__grab::-webkit-slider-runnable-track { background: transparent; border: 0; height: var(--thumb); }
.agerange__grab::-moz-range-track { background: transparent; border: 0; height: var(--thumb); }

.agerange__grab::-webkit-slider-thumb {
  -webkit-appearance: none;
  appearance: none;
  pointer-events: auto;
  width: var(--thumb);
  height: var(--thumb);
  border-radius: 50%;
  background: var(--paper);
  border: 3px solid var(--p2);
  box-shadow: 0 1px 4px rgb(0 0 0 / 0.35);
  cursor: grab;
}
.agerange__grab::-moz-range-thumb {
  pointer-events: auto;
  width: var(--thumb);
  height: var(--thumb);
  border-radius: 50%;
  background: var(--paper);
  border: 3px solid var(--p2);
  box-shadow: 0 1px 4px rgb(0 0 0 / 0.35);
  cursor: grab;
}
.agerange__grab:active::-webkit-slider-thumb { cursor: grabbing; }
.agerange__grab:active::-moz-range-thumb { cursor: grabbing; }

/* The global focus rule would ring the invisible full-width input, which is
   the wrong shape entirely. The ring belongs on the thumb. */
.agerange__grab:focus-visible { outline: none; }
.agerange__grab:focus-visible::-webkit-slider-thumb { outline: 2px solid var(--p1); outline-offset: 2px; }
.agerange__grab:focus-visible::-moz-range-thumb { outline: 2px solid var(--p1); outline-offset: 2px; }

.agerange__ends {
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  display: flex;
  justify-content: space-between;
  font-family: var(--font-mono);
  font-size: 0.6rem;
  letter-spacing: 0.08em;
  color: var(--ink-4);
}

/* ==========================================================================
   Focus — this is a keyboard app, so focus must always be obvious
   ========================================================================== */

a:focus-visible, button:focus-visible, input:focus-visible {
  outline: 2px solid var(--p1);
  outline-offset: 2px;
  border-radius: 4px;
}
.rail a:focus-visible, .rail button:focus-visible { outline-color: #fff; }

/* ==========================================================================
   Account — the editor, and the card it is editing
   ========================================================================== */

.acct { display: grid; gap: 1.4rem; align-items: start; }
@media (min-width: 1100px) {
  /* the editor keeps a readable measure; the preview takes what is left */
  .acct { grid-template-columns: minmax(0, 44rem) minmax(19rem, 24rem); gap: 2rem; }
}
/* `grid-template-columns` named, for the same reason .main needs it: a grid
   that only declares gap gets one implicit `auto` column, and auto means
   max-content. The editor box was correctly 282px on a phone while its column
   was 489px, so every section inside it — games, rooms, the photo grid — hung
   off the right and the whole account page scrolled sideways. */
.acct__editor { min-width: 0; display: grid; grid-template-columns: minmax(0, 1fr); gap: 1.4rem; align-content: start; }
.acct__preview { min-width: 0; }

/* Follows you down the form on a wide screen. On a narrow one it simply sits
   above the sections, because a card pinned to a phone screen is a card in the
   way of the thing you are editing.

   Sticky on the GRID ITEM, not on the div inside it. A sticky element can only
   travel inside its own containing block, and .acct__preview is start-aligned
   in a grid, so its box is the height of the card — about 880px. Stuck to the
   inner div, the card therefore came unstuck 880px down a form that runs to
   nearly nine thousand, and the whole right-hand half of the account page was
   empty from there on. As the grid item its containing block is .acct itself,
   which is as tall as the editor, so it follows the form the whole way — which
   is what this comment claimed all along. */
@media (min-width: 1100px) {
  .acct__preview { position: sticky; top: 0.5rem; align-self: start; }
  .acct__sticky { position: static; }
}
@media (max-width: 1099px) {
  .acct__preview { order: -1; }
}

.pv {
  border-radius: var(--radius);
  border: 1px solid var(--line);
  background: var(--paper);
  overflow: hidden;
}
.pv__clip {
  position: relative;
  aspect-ratio: 16 / 9;
  background: var(--paper-2);
  border-bottom: 1px solid var(--line-soft);
  display: grid;
  place-items: center;
}
/* With a photo the preview takes the deck card's taller crop, so the thing
   being previewed is framed the way the deck will actually frame it. */
.pv__clip--shot { aspect-ratio: 4 / 3; }
.pv__shot { width: 100%; height: 100%; object-fit: cover; display: block; }

.pv__body { padding: 0.95rem 1rem 1.1rem; display: grid; gap: 0.7rem; }
.pv__tag {
  font-family: var(--font-display);
  font-weight: 600;
  font-size: 1.02rem;
  letter-spacing: -0.016em;
  display: flex;
  align-items: baseline;
  gap: 0.45em;
  margin: 0;
}
.prompt__a--empty { color: var(--ink-4); font-style: italic; }

/* ---------- dating photos, on the account page ---------- */

/* Six slots in two rows of three, filled and empty alike. The empty ones are
   the add button, so the grid is the instruction as well as the result — you
   can see at a glance both what you have and how much of the profile is still
   blank, which a list that grows one tile at a time never tells you. */
.pgrid {
  display: grid;
  /* minmax(0, 1fr), not 1fr: `1fr` is shorthand for minmax(auto, 1fr) and that
     auto floor is min-content, so three photo tiles refused to go below their
     natural width and the grid stood 489px wide inside a 282px pane. The whole
     Photos block was reachable only by scrolling the page sideways. */
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: 0.5rem;
  margin-bottom: 0.7rem;
}

.pgrid__cell {
  position: relative;
  margin: 0;
  aspect-ratio: 3 / 4;
  border-radius: var(--radius-sm);
  overflow: hidden;
  background: var(--band);
  border: 1px solid var(--line);
}

.pgrid__cell img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
  -webkit-user-drag: none;
  user-select: none;
}

/* An empty slot is a target, not a tile — dashed, so it reads as somewhere a
   photo goes rather than a photo that failed to load. */
.pgrid__cell--empty {
  display: grid;
  place-items: center;
  align-content: center;
  gap: 0.15rem;
  border-style: dashed;
  border-color: var(--line-hard);
  background: transparent;
  cursor: pointer;
  transition: border-color 0.2s var(--ease), background 0.2s var(--ease);
}
.pgrid__cell--empty:hover {
  border-color: var(--room);
  background: var(--band);
}
.pgrid__plus { font-size: 1.35rem; line-height: 1; color: var(--ink-3); }
.pgrid__slot {
  font-family: var(--font-body);
  font-size: 0.62rem;
  font-weight: 500;
  letter-spacing: 0.04em;
  color: var(--ink-4);
}

/* The badge. "Main" earns the Dating colour because that is the one photo the
   deck actually leads with; the rest are just positions and stay quiet. */
.pgrid__no {
  position: absolute;
  left: 0.4rem;
  bottom: 0.4rem;
  font-family: var(--font-body);
  font-size: 0.6rem;
  font-weight: 600;
  letter-spacing: 0.03em;
  line-height: 1;
  padding: 0.36em 0.55em;
  border-radius: 999px;
  background: var(--void);
  border: 1px solid var(--line-hard);
  color: var(--ink-2);
}
.pgrid__cell:first-child .pgrid__no {
  background: var(--p2);
  border-color: var(--p2);
  color: var(--void);
}

.pgrid__x {
  position: absolute;
  top: 0.35rem;
  right: 0.35rem;
  width: 24px;
  height: 24px;
  display: grid;
  place-items: center;
  padding: 0;
  border-radius: 999px;
  border: 1px solid var(--line-hard);
  background: var(--void);
  color: var(--ink-2);
  cursor: pointer;
  transition: background 0.2s var(--ease), color 0.2s var(--ease), border-color 0.2s var(--ease);
}
.pgrid__x svg { width: 13px; height: 13px; }
.pgrid__x:hover { background: var(--p2); border-color: var(--p2); color: var(--void); }

/* dragging */
.pgrid__cell[draggable="true"] { cursor: grab; touch-action: none; }
.pgrid__cell.is-lifted {
  cursor: grabbing;
  opacity: 0.35;
  border-color: var(--room);
}
.pgrid__cell.is-target { border-color: var(--room); }
.pgrid.is-dragging .pgrid__x { pointer-events: none; opacity: 0; }

.pgrid__hint {
  font-family: var(--font-body);
  font-size: 0.69rem;
  font-weight: 500;
  letter-spacing: 0.01em;
  color: var(--ink-4);
  margin: 0 0 1.15rem;
}

/* The file input never shows: an empty slot is what opens the picker. It stays
   in the DOM and focusable-adjacent rather than `display: none`, so the label
   still drives it and the no-JS button still submits it. */
.pgrid__file {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip: rect(0 0 0 0);
  white-space: nowrap;
  border: 0;
}
.pgrid__ghost { display: contents; }

/* `.photo` is the plain stacked tile still used where photos appear OUTSIDE
   the account grid — the admin person page lists them in a column with a
   reason box under each. The Remove button spans its tile rather than sitting
   in the middle looking like a caption. */
.photo { margin: 0; display: grid; gap: 0.4rem; }
.photo img {
  width: 100%;
  aspect-ratio: 3 / 4;
  object-fit: cover;
  display: block;
  border-radius: var(--radius-sm);
  border: 1px solid var(--line);
  background: var(--band);
}
.photo .b { width: 100%; }

/* ---------- disclosure rows ---------- */

/* A fact, its current value, and a way in. Five stacked labelled inputs make a
   section you scroll; five rows make one you read. The value on the right is
   the point — it answers "what is this set to" without opening anything, which
   is the question people actually come to an account page with.

   Native <details>, so it works with the script off and the browser handles the
   state. A row whose field failed validation is rendered `open`, or the error
   would be sitting behind a closed summary. */
.rows {
  border: 1px solid var(--line);
  border-radius: var(--radius-sm);
  overflow: hidden;
  margin-bottom: 1.15rem;
  background: var(--panel);
}

.row + .row { border-top: 1px solid var(--line); }

.row__head {
  display: grid;
  grid-template-columns: 16px minmax(0, auto) minmax(0, 1fr) 10px;
  align-items: center;
  gap: 0.7rem;
  padding: 0.78rem 0.9rem;
  cursor: pointer;
  list-style: none;
  transition: background 0.18s var(--ease);
}
.row__head::-webkit-details-marker { display: none; }
.row__head:hover { background: var(--panel-2); }
.row[open] .row__head { background: var(--panel-2); }

.row__icon { width: 16px; height: 16px; color: var(--ink-4); }
.row[open] .row__icon { color: var(--room); }

.row__k {
  font-family: var(--font-body);
  font-size: 0.8rem;
  font-weight: 500;
  color: var(--ink);
  display: flex;
  align-items: baseline;
  gap: 0.45em;
  white-space: nowrap;
}

/* public / private, said once, in the place it matters */
.row__tag {
  font-style: normal;
  font-size: 0.58rem;
  font-weight: 600;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--ink-4);
  border: 1px solid var(--line-hard);
  border-radius: 999px;
  padding: 0.2em 0.5em;
}
.row__tag--pub { color: var(--p1-t); border-color: var(--p1-deep); }

.row__v {
  font-size: 0.8rem;
  color: var(--ink-3);
  text-align: right;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* The chevron is drawn rather than iconned: it is one glyph, it has to rotate,
   and an <svg> here would be the only thing in the row that needed a wrapper. */
.row__head::after {
  content: '';
  width: 7px;
  height: 7px;
  border-right: 1.6px solid var(--ink-4);
  border-bottom: 1.6px solid var(--ink-4);
  transform: rotate(-45deg);
  transition: transform 0.2s var(--ease), border-color 0.2s var(--ease);
  justify-self: end;
}
.row[open] .row__head::after {
  transform: rotate(45deg);
  border-color: var(--room);
}

.row__body { padding: 0.2rem 0.9rem 0.95rem; }
.row__body .field:last-child { margin-bottom: 0; }
.row__body .field__hint { margin-bottom: 0; }


/* ==========================================================================
   Linked accounts — one row per provider on the account page
   ========================================================================== */

/* Deliberately not .rows, which is a <details> accordion: the action here is
   one button and hiding it behind a disclosure would put a click in front of
   the only thing the row is for. Same frame, flat contents. */
.linked {
  border: 1px solid var(--line);
  border-radius: var(--radius-sm);
  overflow: hidden;
  background: var(--panel);
  margin-bottom: 1.15rem;
}
.linked__row {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.6rem 0.9rem;
  padding: 0.8rem 0.9rem;
}
.linked__row + .linked__row { border-top: 1px solid var(--line); }
/* A linked provider is the one state worth colouring, and the tint is the
   room accent rather than a new hue — this is the same "confirmed" the safe
   .why note uses two blocks down. */
.linked__row--on { background: var(--p1-wash); }

.linked__who { flex: 1 1 15rem; min-width: 0; }
.linked__name {
  font-family: var(--font-body);
  font-size: 0.85rem;
  font-weight: 500;
  color: var(--ink);
  display: flex;
  align-items: center;
  gap: 0.5em;
}
.linked__note {
  margin: 0.2rem 0 0;
  font-size: 0.78rem;
  line-height: 1.45;
  color: var(--ink-3);
  max-width: 54ch;
}
.linked__note--off { color: var(--ink-4); }

/* The word does the work, not the fill — the same rule the badges follow, and
   for the same reason: pine is also the Social accent. */
.linked__tag {
  font-size: 0.58rem;
  font-weight: 600;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--ink-4);
  border: 1px solid var(--line-hard);
  border-radius: 999px;
  padding: 0.2em 0.55em;
  flex: none;
}
.linked__tag--on { color: var(--p1-t); border-color: var(--p1-deep); }

.linked__act { display: flex; align-items: center; gap: 0.45rem; flex: none; }

/* ---------- postal code + detect ---------- */

/* The button sits beside the field rather than under it: it is an alternative
   to typing, not a step after it, and stacking them reads as the latter. */
.geo__row { display: flex; gap: 0.5rem; align-items: stretch; }
.geo__row input { flex: 1 1 auto; min-width: 0; }

/* `align-items: stretch` above matches the button to the input's height
   whatever the input's padding turns out to be, so the two never drift apart
   when one of them is restyled. */
.geo__btn { flex: 0 0 auto; white-space: nowrap; }
.geo__btn svg { width: 14px; height: 14px; }

/* The attribution line. Quieter than a hint because it is a licence
   obligation rather than something the member has to read. */
.field__hint--fine { font-size: 0.72rem; color: var(--ink-4); margin-top: 0.35rem; }
.field__hint--fine a { color: inherit; text-decoration: underline; }

@media (max-width: 420px) {
  .geo__row { flex-wrap: wrap; }
  .geo__btn { width: 100%; justify-content: center; }
}

@media (prefers-reduced-motion: reduce) {
  .row__head, .row__head::after { transition: none; }
}

/* On a narrow screen the value gives up its column rather than squeezing the
   label into two lines. */
@media (max-width: 520px) {
  .row__head { grid-template-columns: 16px minmax(0, 1fr) 10px; }
  .row__v { grid-column: 2; grid-row: 2; text-align: left; }
}

/* ---------- save bar ---------- */

/* Sticks to the bottom of the editor column, not the viewport: on a wide screen
   the preview card sits alongside, and a bar spanning both would be a bar that
   belongs to neither. Hidden until something is edited — a page nobody has
   touched should carry no chrome at all. */
.savebar {
  position: sticky;
  bottom: 0;
  z-index: 5;
  display: flex;
  align-items: center;
  gap: 0.7rem;
  padding: 0.7rem 0.85rem;
  margin-top: 0.6rem;
  border: 1px solid var(--line-hard);
  border-radius: var(--radius-sm);
  background: var(--panel-2);
}

/* The seam again: the bar is a section head turned upside down, so it gets the
   same short bar in the room's colour, on the edge nearest the content. */
.savebar::before {
  content: '';
  position: absolute;
  left: -1px;
  top: -1px;
  width: 34px;
  height: 2px;
  background: var(--room);
}

.savebar__n {
  flex: 1 1 auto;
  margin: 0;
  min-width: 0;
  font-family: var(--font-body);
  font-size: 0.72rem;
  font-weight: 500;
  letter-spacing: 0.01em;
  color: var(--ink-2);
}

.savebar.is-bad { border-color: var(--p2-deep); }
.savebar.is-bad::before { background: var(--p2); }
.savebar.is-bad .savebar__n { color: var(--p2-t); }

@media (max-width: 520px) {
  .savebar { flex-wrap: wrap; }
  .savebar__n { flex: 1 0 100%; }
}

/* ---------- bio ---------- */

/* Free text somebody wrote, so it keeps its line breaks rather than collapsing
   into one paragraph — people write two short paragraphs and the second one is
   usually the point. `pre-wrap` and not `pre`: a long line still wraps to the
   card instead of forcing it sideways. */
.bio {
  margin: 0;
  font-size: 0.82rem;
  line-height: 1.55;
  color: var(--ink-2);
  white-space: pre-wrap;
  overflow-wrap: anywhere;
}

/* On the compact Groups and Social cards a bio is context, not the content —
   three lines, then it stops. The deck card shows the whole thing, because
   there the profile IS the decision. */
.bio--clamp {
  display: -webkit-box;
  -webkit-line-clamp: 3;
  -webkit-box-orient: vertical;
  overflow: hidden;
}

/* The counter turns the room's colour at the limit rather than red: running
   out of characters is the field working, not the user making a mistake. */
.field__label i.is-full { color: var(--room); }

/* ---------- voice: device and level ---------- */

.vc__mic-row {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  align-items: center;
  gap: 0.6rem;
}

.vc__select {
  width: 100%;
  font: inherit;
  font-size: 0.72rem;
  color: var(--ink-2);
  padding: 0.45em 0.6em;
  border-radius: 6px;
  border: 1px solid var(--line-hard);
  background: var(--band);
}
.vc__select:focus-visible { outline: 2px solid var(--room); outline-offset: 1px; }

/* One labelled device control. The label used to be sr-only and the picker read
   as decoration; somebody sat through a whole session inaudible with the fix
   one click away. Naming the thing is the fix. */
.vc__dev { margin-bottom: 0.8rem; }

.vc__devk {
  display: block;
  font-family: var(--font-mono);
  font-size: 0.62rem;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--ink-4);
  margin-bottom: 0.3rem;
}

/* Whatever the check has to say. Empty until it says something, so it costs no
   height on a panel that is mostly buttons. */
.vc__devnote {
  margin: 0.35rem 0 0;
  font-size: 0.73rem;
  line-height: 1.45;
  color: var(--ink-3);
}
.vc__devnote:empty { display: none; }

/* Six segments rather than a smooth bar. A continuous fill at idle noise reads
   as "slightly on" and leaves you guessing; discrete blocks either light or
   they do not, which is the whole question being asked. */
.vc__meter {
  --level: 0%;
  width: 74px;
  height: 10px;
  flex: none;
  border-radius: 3px;
  overflow: hidden;
  background:
    repeating-linear-gradient(90deg, var(--panel-2) 0 11px, transparent 11px 13px);
  border: 1px solid var(--line-hard);
  position: relative;
}
.vc__meter i {
  position: absolute;
  inset: 0 auto 0 0;
  width: var(--level);
  background:
    repeating-linear-gradient(90deg, var(--p3) 0 11px, transparent 11px 13px);
  transition: width 0.08s linear;
}
@media (prefers-reduced-motion: reduce) {
  .vc__meter i { transition: none; }
}

/* Published, accepted, and carrying nothing — the state that had no words. */
.vc__alert {
  display: flex;
  gap: 0.6rem;
  align-items: flex-start;
  padding: 0.7rem 0.8rem;
  margin-bottom: 0.8rem;
  border-radius: var(--radius-sm);
  border: 1px solid var(--p2-deep);
  background: var(--p2-wash);
  font-size: 0.76rem;
  line-height: 1.5;
  color: var(--ink-2);
}
.vc__alert svg { width: 15px; height: 15px; flex: none; margin-top: 0.15rem; color: var(--p2-t); }
.vc__alert b { color: var(--ink); display: block; }
.vc__alert .b { margin-top: 0.5rem; }

/* A peer the SFU has no audio for. Not muted — muted is a choice, this is a
   fault, and reading them the same way is how an hour gets wasted. */
.vc__person.is-silent .vc__mic { color: var(--p2-t); }
.vc__person.is-silent .vc__av { opacity: 0.5; }

/* ==========================================================================
   Theme picker — the swatch on a choice card
   ========================================================================== */

/* The choice card is a two-column grid; the swatch needs a third track rather
   than an implicit second row, or it drops under the description. */
.choice--theme { grid-template-columns: auto minmax(0, 1fr) auto; align-items: center; }

/* Page, primary, accent. Three squares are not a preview and are not trying to
   be one — they are only enough to tell two entries apart before committing to
   a reload. The real preview is picking it. */
.thmsw { display: flex; gap: 2px; flex: none; align-self: center; }
.thmsw i {
  width: 16px;
  height: 26px;
  display: block;
  border: 1px solid var(--line-hard);
}
.thmsw i + i { border-left: 0; }

/* ---------- filter sheet ---------- */

/* The trigger. A link, because without the script it is one — see the
   component for why that bargain is deliberate. */
.fbtn {
  display: inline-flex;
  align-items: center;
  gap: 0.45rem;
  padding: 0.5rem 0.8rem;
  border: 1px solid var(--line);
  border-radius: var(--r-sm, 8px);
  background: var(--surface-2, transparent);
  color: var(--ink-2);
  font-size: 0.86rem;
  font-weight: 500;
  text-decoration: none;
  transition: border-color 0.2s var(--ease), color 0.2s var(--ease);
}
.fbtn:hover { border-color: var(--line-hard); color: var(--ink); }
.fbtn svg { width: 15px; height: 15px; }

/* The count of active filters, so the sheet does not have to be opened to
   find out whether anything is on. */
.fbtn__n {
  min-width: 1.15rem;
  padding: 0 0.3rem;
  border-radius: 999px;
  background: var(--room);
  color: #fff;
  font-size: 0.72rem;
  line-height: 1.15rem;
  text-align: center;
}

/* The sheet itself. `dialog` brings its own centring and top layer; what it
   does not bring is any of this. */
.fsheet {
  width: min(30rem, calc(100vw - 2rem));
  max-height: min(38rem, calc(100vh - 3rem));
  padding: 0;
  border: 1px solid var(--line-hard);
  border-radius: var(--r, 14px);
  background: var(--surface);
  color: var(--ink);
  overflow: hidden;
}
.fsheet::backdrop { background: rgb(0 0 0 / 0.55); }

/* Grid rather than block so the body is the only part that scrolls and the
   header stays put — a filter sheet you have to scroll up in to close is a
   filter sheet people close by reloading. */
.fsheet__in { display: grid; grid-template-rows: auto minmax(0, 1fr); max-height: inherit; }

.fsheet__head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 1rem;
  padding: 0.95rem 1.1rem 0.7rem;
  border-bottom: 1px solid var(--line);
}
.fsheet__title { font-family: var(--font-display); font-size: 1.05rem; margin: 0; }

.fsheet__x {
  display: grid;
  place-items: center;
  width: 30px;
  height: 30px;
  border: 0;
  border-radius: 8px;
  background: none;
  color: var(--ink-3);
  cursor: pointer;
}
.fsheet__x:hover { background: var(--surface-2); color: var(--ink); }
.fsheet__x svg { width: 15px; height: 15px; }

.fsheet__body { padding: 1rem 1.1rem 1.15rem; overflow-y: auto; }
.fsheet__body .field:last-of-type { margin-bottom: 0; }

/* The sheet's own actions sit against the bottom of the body rather than in a
   third grid row: the row would be pinned even when the body is short, which
   leaves a gap under two fields and reads as a rendering bug. */
.fsheet__act { display: flex; gap: 0.6rem; margin-top: 1.1rem; }
.fsheet__act .b { flex: 1 1 auto; justify-content: center; }

/* A full-width sheet on a phone, anchored to the bottom where a thumb is. */
@media (max-width: 520px) {
  .fsheet {
    width: 100vw;
    max-width: 100vw;
    max-height: 85vh;
    margin: auto auto 0;
    border-radius: var(--r, 14px) var(--r, 14px) 0 0;
    border-bottom: 0;
  }
}

/* ---------------------------------------------------------------------------
   Clips

   A thumbnail that opens a player. Everything here is written against tokens
   rather than literal colour, so a theme re-pointing --panel and --radius
   re-skins clips without a single rule of its own — see CLAUDE.md on themes.
--------------------------------------------------------------------------- */

.clips {
  display: grid;
  /* min(150px, 100%): a bare 150px floor is still a floor in a container
     narrower than 150px — see the note on .cards. */
  grid-template-columns: repeat(auto-fill, minmax(min(150px, 100%), 1fr));
  gap: 0.6rem;
}

/* On a deck card the row is fixed at three across and never wraps: the card is
   a fixed box being read in a glance, and a grid that reflowed as somebody
   added a clip would move everything under it. */
.clips--compact {
  /* minmax(0, 1fr) so three tiles shrink on a phone rather than standing at
     their min-content width and overflowing the card they sit in. */
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: 0.4rem;
}

.clip {
  display: block;
  width: 100%;
  padding: 0;
  border: 1px solid var(--line);
  border-radius: var(--radius-sm);
  background: var(--panel-2);
  color: inherit;
  font: inherit;
  text-align: left;
  cursor: pointer;
  overflow: hidden;
  transition: border-color 120ms ease, transform 120ms ease;
}

.clip:hover,
.clip:focus-visible {
  border-color: var(--line-hard);
  transform: translateY(-1px);
}

.clip__shot {
  position: relative;
  display: block;
  aspect-ratio: 16 / 9;
  background: var(--band);
  overflow: hidden;
}

.clip__shot img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
}

/* The stand-in when there is no poster: a link clip, which by design never
   loads the provider's own thumbnail. */
.clip__mark {
  position: absolute;
  inset: 0;
  display: grid;
  place-items: center;
  color: var(--bone-4);
}

.clip__mark .smark,
.clip__mark svg {
  width: 30%;
  height: 30%;
  max-width: 44px;
  max-height: 44px;
}

.clip__play {
  position: absolute;
  left: 50%;
  top: 50%;
  transform: translate(-50%, -50%);
  width: 40px;
  height: 40px;
  display: grid;
  place-items: center;
  border-radius: 999px;
  background: rgba(11, 12, 14, 0.62);
  border: 1px solid rgba(242, 239, 232, 0.28);
  color: var(--bone);
  backdrop-filter: blur(2px);
}

.clip__play svg {
  width: 18px;
  height: 18px;
  /* nudged right: an optical centre for a triangle is not its bounding box */
  margin-left: 2px;
}

.clip:hover .clip__play,
.clip:focus-visible .clip__play {
  background: var(--p1-deep);
  border-color: var(--p1-t);
}

.clip__len {
  position: absolute;
  right: 5px;
  bottom: 5px;
  padding: 1px 5px;
  border-radius: 4px;
  background: rgba(11, 12, 14, 0.78);
  color: var(--bone-2);
  font-size: 0.68rem;
  font-variant-numeric: tabular-nums;
}

.clip__cap {
  display: block;
  padding: 0.45rem 0.55rem 0.5rem;
}

.clip__title {
  display: block;
  font-size: 0.8rem;
  color: var(--bone);
  /* one line: a long title must not make one tile taller than its neighbours */
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.clip__game {
  display: flex;
  align-items: center;
  gap: 0.3rem;
  margin-top: 0.15rem;
  font-size: 0.7rem;
  color: var(--bone-3);
}

.clips--compact .clip__cap { padding: 0.35rem 0.4rem 0.4rem; }
.clips--compact .clip__title { font-size: 0.72rem; }
.clips--compact .clip__game { display: none; }
.clips--compact .clip__play { width: 30px; height: 30px; }
.clips--compact .clip__play svg { width: 14px; height: 14px; }

/* ---------- the player ---------- */

.clipbox {
  padding: 0;
  border: 0;
  background: transparent;
  max-width: min(1100px, 94vw);
  width: 100%;
}

.clipbox::backdrop {
  background: rgba(6, 7, 9, 0.82);
}

.clipbox__in {
  border: 1px solid var(--line-hard);
  border-radius: var(--radius);
  background: var(--panel);
  overflow: hidden;
}

.clipbox__stage {
  aspect-ratio: 16 / 9;
  background: #000;
  display: block;
}

.clipbox__stage video,
.clipbox__stage iframe {
  width: 100%;
  height: 100%;
  display: block;
  border: 0;
}

.clipbox__bar {
  display: flex;
  align-items: center;
  gap: 0.6rem;
  padding: 0.55rem 0.7rem;
  border-top: 1px solid var(--line);
}

.clipbox__title {
  flex: 1;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  color: var(--bone);
  font-size: 0.9rem;
}

.clipbox__out,
.clipbox__x {
  flex: none;
  padding: 0.3rem 0.6rem;
  border: 1px solid var(--line-hard);
  border-radius: var(--radius-sm);
  background: var(--panel-2);
  color: var(--bone-2);
  font: inherit;
  font-size: 0.78rem;
  text-decoration: none;
  cursor: pointer;
}

.clipbox__out:hover,
.clipbox__x:hover {
  color: var(--bone);
  border-color: var(--bone-4);
}

/* ---------- clips, on the account page ---------- */

.cliplist {
  display: grid;
  /* Named, not implicit. An implicit column is `auto`, which is max-content —
     so this list stood as wide as its longest clip title instead of as wide as
     the account page, and took the whole form sideways with it. Third time
     this exact mistake appears in this file; the title inside already knows
     how to ellipsis, it was never given the chance. */
  grid-template-columns: minmax(0, 1fr);
  gap: 0.4rem;
  margin-bottom: 0.8rem;
}

.cliprow {
  display: flex;
  align-items: center;
  gap: 0.65rem;
  padding: 0.45rem;
  border: 1px solid var(--line);
  border-radius: var(--radius-sm);
  background: var(--panel-2);
}

/* A row the member has to act on says so in its edge, not only in its text. */
.cliprow--failed { border-color: var(--p2-deep); }
.cliprow--pending { border-style: dashed; }

.cliprow__thumb {
  position: relative;
  flex: none;
  width: 92px;
  aspect-ratio: 16 / 9;
  display: grid;
  place-items: center;
  border-radius: 6px;
  /* a faint lift off the row, so the tile reads as media rather than a gap */
  background: linear-gradient(160deg, var(--panel-2), var(--void));
  border: 1px solid var(--line);
  color: var(--bone-4);
  overflow: hidden;
}

.cliprow__thumb img {
  width: 100%;
  height: 100%;
  object-fit: cover;
}

/* The provider's own mark keeps its own colour here — this is the member's
   own account page, where "which site is this on" is the useful signal. */
.cliprow__thumb .smark--youtube { color: #FF0000; }
.cliprow__thumb .smark--twitch  { color: #9146FF; }

.cliprow__thumb svg,
.cliprow__thumb .smark {
  width: 24px;
  height: 24px;
}

.cliprow__body { flex: 1; min-width: 0; }

.cliprow__title {
  color: var(--bone);
  font-size: 0.86rem;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.cliprow__meta {
  margin-top: 0.1rem;
  color: var(--bone-3);
  font-size: 0.72rem;
}

.cliprow__warn { color: var(--p2-t); font-weight: 600; }

.cliprow__x {
  flex: none;
  width: 30px;
  height: 30px;
  display: grid;
  place-items: center;
  padding: 0;
  border: 1px solid var(--line-hard);
  border-radius: 999px;
  background: transparent;
  color: var(--bone-3);
  cursor: pointer;
}

.cliprow__x:hover { color: var(--bone); border-color: var(--p2); background: var(--p2-wash); }
.cliprow__x svg { width: 14px; height: 14px; }

/* The link field and its button share a line. `min-width: 0` on the input is
   what stops a long placeholder pushing the button off the end — a flex item
   defaults to min-content width, not zero. */
.clipadd__go {
  display: flex;
  gap: 0.45rem;
  align-items: stretch;
}

.clipadd__go input { min-width: 0; flex: 1; }
.clipadd__go .b { flex: none; }

.clipadd__row {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 0.6rem;
}

@media (max-width: 560px) {
  .clipadd__row { grid-template-columns: 1fr; }
}

/* ---------- the upload zone ---------- */

.clipdrop {
  display: grid;
  justify-items: center;
  gap: 0.15rem;
  padding: 0.9rem 1rem;
  margin-bottom: 0.6rem;
  border: 1px dashed var(--line-hard);
  border-radius: var(--radius-sm);
  background: var(--band);
  cursor: pointer;
  text-align: center;
  transition: border-color 140ms ease, background 140ms ease;
}

.clipdrop:hover { border-color: var(--p1); background: var(--p1-wash); }

.clipdrop__ico { width: 20px; height: 20px; color: var(--bone-4); }
.clipdrop:hover .clipdrop__ico { color: var(--p1-t); }
.clipdrop__t { color: var(--bone-2); font-size: 0.84rem; }
.clipdrop__s { color: var(--bone-4); font-size: 0.72rem; }

/* ---------- upload progress ---------- */

.clipup { margin-top: 0.6rem; }

.clipup__bar {
  height: 5px;
  border-radius: 999px;
  background: var(--line-soft);
  overflow: hidden;
}

.clipup__fill {
  height: 100%;
  width: 0;
  border-radius: 999px;
  background: var(--p1);
  transition: width 160ms linear;
}

.clipup__msg {
  margin: 0.3rem 0 0;
  color: var(--bone-3);
  font-size: 0.75rem;
}

.clipup__msg.is-bad { color: var(--p2-t); }

[data-clip-upload-label].is-busy {
  opacity: 0.55;
  pointer-events: none;
}

/* ==========================================================================
   Duo randomizer — Social

   A draw is watched, which is the whole reason it looks different from a
   squad card. Two numbers carry it: how long is left, and how many hands are
   up. Both are set in the mono face at a size you can read from across a room,
   because the case this was built for is a host with the page on a stream and
   an audience reading it off a second monitor.

   Everything below is tokens. base.css is written against custom properties
   and a theme re-points them, so Retro squares the corners and recolours the
   clock from its own sheet without a single rule of its own down here.
   ========================================================================== */

.duo {
  display: grid;
  grid-template-rows: auto 1fr auto;
  min-width: 0;
  border-radius: var(--radius);
  border: 1px solid var(--line);
  background: var(--paper);
  overflow: hidden;
  transition: border-color 0.24s var(--ease), transform 0.24s var(--ease);
}
.duo:hover { border-color: var(--line-hard); transform: translateY(-2px); }

/* A settled post is history: it stops lifting on hover and stops asking for
   attention, because there is nothing left to do to it. */
.duo.is-settled { opacity: 0.72; }
.duo.is-settled:hover { transform: none; border-color: var(--line); }

.duo__top {
  display: flex;
  align-items: center;
  gap: 0.6rem;
  padding: 0.62rem 0.9rem;
  background: var(--panel-2);
  border-bottom: 1px solid var(--line);
}

.duo__chan {
  margin-left: auto;
  font-family: var(--font-mono);
  font-size: 0.62rem;
  letter-spacing: 0.09em;
  text-transform: uppercase;
  color: var(--ink-3);
}
.duo[data-channel="voice"] .duo__chan { color: var(--p1-ink); }

.duo__body { padding: 0.95rem 1rem 0.7rem; display: grid; gap: 0.7rem; align-content: start; }

.duo__title {
  font-family: var(--font-display);
  font-weight: 600;
  font-size: 1.02rem;
  letter-spacing: -0.012em;
  line-height: 1.2;
  margin: 0;
}
.duo__title a { text-decoration: none; color: inherit; }
.duo__title a:hover { color: var(--p1-ink); }

.duo__host {
  margin: 0;
  font-size: 0.78rem;
  color: var(--ink-3);
  display: flex;
  align-items: center;
  gap: 0.4rem;
  flex-wrap: wrap;
}

.duo__note {
  margin: 0;
  font-size: 0.82rem;
  line-height: 1.45;
  color: var(--ink-2);
}

/* the two numbers that are the whole card */
.duo__figs {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 0.5rem 0.9rem;
  padding-top: 0.15rem;
}

.duo__fig {
  font-family: var(--font-mono);
  font-size: 1.35rem;
  line-height: 1;
  letter-spacing: -0.02em;
  color: var(--ink);
  font-variant-numeric: tabular-nums;
}

/* Under a minute the clock is the only thing on the page that matters. */
.duo__fig.is-urgent { color: var(--p2-ink); }

/* Entries are shut and the host has not drawn — the post is waiting on a
   person, not on a clock, and it should stop counting at people. */
.duo__fig.is-done { color: var(--ink-3); font-size: 1.02rem; }

.duo__foot {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.45rem;
  padding: 0.7rem 1rem 0.85rem;
  border-top: 1px solid var(--line-soft);
  background: var(--paper-2);
}

/* the refusal, in the same place and voice a seat's refusal uses */
.duowhy {
  display: flex;
  gap: 0.4rem;
  align-items: flex-start;
  margin: 0;
  padding: 0 1rem 0.85rem;
  font-size: 0.72rem;
  line-height: 1.4;
  color: var(--ink-4);
  background: var(--paper-2);
}
.duowhy svg { width: 13px; height: 13px; flex: none; margin-top: 0.15rem; }

/* --------------------------------------------------------------------------
   The post's own page — built to be looked at by more people than are on it
   -------------------------------------------------------------------------- */

.drawpanel {
  border: 1px solid var(--line);
  border-radius: var(--radius);
  background: var(--paper);
  padding: 1.4rem 1.5rem 1.5rem;
  display: grid;
  gap: 1.1rem;
}

.drawpanel__clock {
  font-family: var(--font-mono);
  font-size: clamp(2.6rem, 9vw, 4.4rem);
  line-height: 0.95;
  letter-spacing: -0.03em;
  font-variant-numeric: tabular-nums;
  color: var(--ink);
  margin: 0;
}
.drawpanel__clock.is-urgent { color: var(--p2-ink); }

.drawpanel__k {
  font-family: var(--font-mono);
  font-size: 0.64rem;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--ink-4);
  margin: 0 0 0.3rem;
}

.drawpanel__row { display: flex; flex-wrap: wrap; align-items: center; gap: 0.6rem; }

/* the entry list, which is the only reason to believe the draw */
/* --------------------------------------------------------------------------
   A person on a duo post, named or not

   This replaced `.pool`, which was a row of 38px faces with gamertags in the
   title attribute — the right drawing of an entry list back when an entry list
   had names on it. A duo post now says what somebody plays and not who they
   are, so a tile has to carry facts rather than a face, and the same tile is
   used at three sizes: the host's queue, the pool grid, and the pair on a
   settled post.
   -------------------------------------------------------------------------- */

.duop {
  display: grid;
  gap: 0.6rem;
  align-content: start;
  min-width: 0;
  padding: 0.8rem 0.9rem;
  border-radius: var(--radius);
  border: 1px solid var(--line);
  background: var(--paper);
}

/* Named is the exception and looks like it: the reveal is the payoff, so the
   card that got there is the one with the accent on it. */
.duop.is-named { border-color: var(--p3); background: var(--p3-wash); }

.duop.is-winner { outline: 2px solid var(--p3); outline-offset: 2px; }

.duop__id { display: grid; grid-template-columns: auto minmax(0, 1fr); gap: 0.6rem; align-items: center; }

.duop__av {
  position: relative;
  width: 34px; height: 34px;
  border-radius: 9px;
  display: grid;
  place-items: center;
  font-family: var(--font-display);
  font-weight: 600;
  font-size: 0.72rem;
  color: #fff;
  background: var(--ink-3);
  flex: none;
}

/* No accent colour and no initials — both are the account. A dashed neutral
   chip with a seat number in it cannot be recognised from one post to the
   next, which is the entire job. */
.duop__av--masked {
  background: transparent;
  border: 1.5px dashed var(--line-hard);
  color: var(--ink-3);
  font-family: var(--font-mono);
}
.duop__av--masked svg { width: 15px; height: 15px; }

.duop__who { min-width: 0; display: grid; gap: 0.25rem; }

.duop__name {
  font-family: var(--font-display);
  font-weight: 600;
  font-size: 0.92rem;
  letter-spacing: -0.01em;
  color: var(--ink);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.duop__marks { display: flex; flex-wrap: wrap; align-items: center; gap: 0.3rem; }

/* The facts, as label-over-value pairs. A definition list rather than a table
   because the set is different per person — somebody with no rank on this game
   has one fewer row, not an empty cell. */
.duop__facts { margin: 0; display: grid; grid-template-columns: repeat(auto-fit, minmax(88px, 1fr)); gap: 0.5rem 0.8rem; }
.duop__facts dt {
  font-family: var(--font-mono);
  font-size: 0.6rem;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--ink-4);
}
.duop__facts dd { margin: 0.1rem 0 0; font-size: 0.82rem; line-height: 1.3; color: var(--ink-2); }

.duop__none { margin: 0; font-size: 0.78rem; color: var(--ink-4); }

.duop__act { display: flex; flex-wrap: wrap; gap: 0.5rem; padding-top: 0.15rem; }

/* The host's queue: full cards, one decision each, one per row on a phone. */
.duop__queue { display: grid; gap: 0.7rem; grid-template-columns: repeat(auto-fill, minmax(min(300px, 100%), 1fr)); }

/* The pool: the same card, smaller, as many as fit. Still one tile per hand —
   a post whose entry list is a number is a post nobody has reason to believe. */
.duop__grid { display: grid; gap: 0.55rem; grid-template-columns: repeat(auto-fill, minmax(min(210px, 100%), 1fr)); }
.duop--sm { padding: 0.6rem 0.7rem; gap: 0.45rem; }
.duop--sm .duop__facts { grid-template-columns: repeat(auto-fit, minmax(74px, 1fr)); gap: 0.35rem 0.6rem; }
.duop--sm .duop__av { width: 28px; height: 28px; font-size: 0.66rem; }

/* Both halves of a settled post, side by side. */
.duop__pair { display: grid; gap: 0.6rem; grid-template-columns: repeat(auto-fit, minmax(min(240px, 100%), 1fr)); }

/* --------------------------------------------------------------------------
   The criteria a post states — the card's own middle, and the panel's
   -------------------------------------------------------------------------- */

.duo__crit { margin: 0; display: grid; grid-template-columns: repeat(auto-fit, minmax(80px, 1fr)); gap: 0.45rem 0.7rem; }
.duo__crit dt {
  font-family: var(--font-mono);
  font-size: 0.6rem;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--ink-4);
}
.duo__crit dd { margin: 0.1rem 0 0; font-size: 0.82rem; line-height: 1.3; color: var(--ink-2); }
.duo__crit--wide { grid-template-columns: repeat(auto-fit, minmax(100px, 1fr)); }

/* The mode, where the gamertag used to be. */
.duo__blurb { font-style: normal; color: var(--ink-4); font-size: 0.72rem; flex: 1 1 100%; }
.duo__host svg { width: 13px; height: 13px; flex: none; }

/* --------------------------------------------------------------------------
   The result
   -------------------------------------------------------------------------- */

.drawn {
  border: 1px solid var(--p3);
  border-radius: var(--radius);
  background: var(--p3-wash);
  padding: 1.2rem 1.4rem;
  display: grid;
  gap: 0.55rem;
}

.drawn__k {
  font-family: var(--font-mono);
  font-size: 0.64rem;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--p3-ink);
  margin: 0;
}

.drawn__who {
  font-family: var(--font-display);
  font-weight: 600;
  font-size: clamp(1.4rem, 5vw, 2.1rem);
  letter-spacing: -0.02em;
  line-height: 1.1;
  margin: 0;
  color: var(--ink);
}

.drawn__s { margin: 0; font-size: 0.85rem; line-height: 1.5; color: var(--ink-2); }

/* One pass, on arrival, and only for people who have not asked for stillness.
   A result that keeps moving is a result nobody can read out. */
@media (prefers-reduced-motion: no-preference) {
  .drawn { animation: drawn-in 420ms var(--ease) both; }
}

@keyframes drawn-in {
  from { opacity: 0; transform: translateY(6px) scale(0.99); }
  to   { opacity: 1; transform: none; }
}

/* ---------------------------------------------------------------------------
   Touch ergonomics

   Every control in this file is sized in em against a small type scale, which
   is right for a cursor and produces 28-37px boxes for a finger. Apple asks
   for 44pt and WCAG 2.5.8 for 24px; the buttons that matter most here — take a
   seat, leave a squad, filter the board — were the smallest of the lot at 28px.

   min-height rather than more padding, so the type size, the weight and the
   horizontal rhythm of a row of buttons are all untouched. The control simply
   grows a taller hit box around the same design.

   Scoped to the phone breakpoint: a mouse does not need 44px, and applying it
   everywhere would inflate the dense desktop chrome these were drawn for.
--------------------------------------------------------------------------- */
@media (max-width: 940px) {
  .b,
  .btn,
  .filter {
    min-height: 44px;
  }

  /* .b--sm drops to 0.63rem, which is ~10px — under the size a phone should
     ask anyone to read. It stays smaller than .b, just not that much smaller. */
  .b--sm { font-size: 0.70rem; }

  /* Square controls get a square hit box rather than a min-height, or they
     stretch into rectangles and the row stops reading as a row of tiles. */
  .smark-btn { width: 44px; height: 44px; }
  .cliprow__x { width: 40px; height: 40px; }

  /* A keyboard shortcut hint on a device with no keyboard is furniture in the
     one field a thumb has to hit. */
  .search kbd { display: none; }

  /* Metadata keys sat at ~10px. Small is the point — illegible is not. */
  .meta__k,
  .roster__n { font-size: 0.72rem; }

  /* The search field is on every page and is the one control a thumb aims at
     most often. 38px was the closest near-miss in the audit. */
  .search input { min-height: 44px; }

  /* A squad card's title is the link that opens the squad — the primary action
     on the busiest board in the product, and it was a 39px strip. Padding
     rather than min-height: the link is inline inside a heading, so a height
     would do nothing without also making it a block. */
  .squad__title a { display: block; padding: 0.35rem 0; }

  /* 0.56rem is 9px. Day initials are meant to be quiet, not squinted at. */
  .night__d { font-size: 0.68rem; }
}

/* A provider we have no brand mark for. Its name, set in the same quiet
   register as the marks around it — see ClipLink::MARKED for why there is no
   logo here rather than an approximation of one. */
.clip__pname {
  font-family: var(--font-body);
  font-size: 0.68rem;
  font-weight: 500;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--bone-4);
}

/* ==========================================================================
   Forums — Social
   ==========================================================================

   The one board in the product that is a LIST rather than a grid of cards.
   That is not a style choice: a card grid says "these are alternatives, pick
   one", which is true of squads and nights and false of threads — a thread
   list is read top to bottom, and the ranking IS the information. So the rows
   are separated by hairlines rather than boxed, and the only card here is the
   forum itself on the index, because a forum genuinely is one of a set.

   Everything is built out of the shared tokens, so a theme that re-points
   --paper, --line and --radius re-skins the whole feature without touching a
   selector below.
   ========================================================================== */

/* ---------- a forum, on the index ---------- */

.fcard {
  border-radius: var(--radius);
  border: 1px solid var(--line);
  background: var(--paper);
  overflow: hidden;
  display: grid;
  grid-template-rows: auto 1fr auto;
  min-width: 0;
  transition: border-color 0.24s var(--ease), transform 0.24s var(--ease);
}
.fcard:hover { border-color: var(--line-hard); transform: translateY(-2px); }

/* The forum's colour, as a band rather than a tint. Same 8px the community
   card uses, so the two read as the same kind of object in the same room. */
.fcard__banner { height: 8px; background: var(--c, var(--p3)); }

.fcard__body { padding: 0.95rem 1rem 0.7rem; display: grid; gap: 0.55rem; align-content: start; }

.fcard__name {
  font-family: var(--font-display);
  font-weight: 600;
  font-size: 1.02rem;
  letter-spacing: -0.012em;
  line-height: 1.2;
  margin: 0;
}
.fcard__name a { color: inherit; text-decoration: none; }
.fcard__name a:hover { color: var(--room-ink); }

.fcard__desc { font-size: 0.85rem; color: var(--ink-2); line-height: 1.45; margin: 0; }

.fcard__stats {
  display: flex;
  flex-wrap: wrap;
  gap: 0.28rem 1rem;
  font-family: var(--font-body);
  font-size: 0.720rem;
  font-weight: 500;
  color: var(--ink-3);
}
.fcard__stats b { color: var(--ink); font-weight: 500; }

.fcard__foot {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.45rem;
  padding: 0.7rem 1rem 0.85rem;
  border-top: 1px solid var(--line-soft);
  background: var(--paper-2);
}

/* ---------- the vote control ----------
   An arrow, a number, an arrow. It is a <form> per direction rather than one
   control with JavaScript behind it, because a board where the ranking cannot
   be affected without JS enabled is a board with a quiet second class of
   reader. app.js upgrades both to fetch; neither needs it. */

.vote {
  display: grid;
  justify-items: center;
  align-content: start;
  gap: 0.1rem;
  width: 34px;
  flex: none;
}

.vote__b {
  display: grid;
  place-items: center;
  width: 26px;
  height: 22px;
  padding: 0;
  border: 0;
  border-radius: 5px;
  background: none;
  color: var(--ink-4);
  cursor: pointer;
  transition: color 0.18s var(--ease), background 0.18s var(--ease);
}
.vote__b svg { width: 15px; height: 15px; display: block; }
.vote__b:hover { background: var(--paper-2); color: var(--ink-2); }

/* Up is the room's colour, down is vermilion — the two players, doing the job
   they do everywhere else. A lit arrow is the only saturated thing in a row. */
.vote__b.is-on { color: var(--room-ink); }
.vote__b--down.is-on { color: var(--p2-ink); }

/* Nothing to press on your own post. It is disabled rather than hidden so the
   score still sits where it does on every other row. */
.vote__b:disabled { opacity: 0.25; cursor: default; }
.vote__b:disabled:hover { background: none; color: var(--ink-4); }

.vote__n {
  font-family: var(--font-mono);
  font-size: 0.78rem;
  font-variant-numeric: tabular-nums;
  color: var(--ink);
  line-height: 1;
}
.vote__n.is-up { color: var(--room-ink); }
.vote__n.is-down { color: var(--p2-ink); }

.vote--sm { width: 28px; }
.vote--sm .vote__b { width: 22px; height: 18px; }
.vote--sm .vote__b svg { width: 13px; height: 13px; }
.vote--sm .vote__n { font-size: 0.72rem; }

/* ---------- a thread, on a board ---------- */

.trow {
  display: flex;
  align-items: flex-start;
  gap: 0.85rem;
  padding: 0.9rem 0;
  border-bottom: 1px solid var(--line-soft);
  min-width: 0;
}
.trow:first-child { padding-top: 0.2rem; }
.trow:last-child { border-bottom: 0; }

.trow__main { min-width: 0; flex: 1 1 auto; display: grid; gap: 0.3rem; }

.trow__title {
  font-family: var(--font-display);
  font-weight: 600;
  font-size: 1rem;
  letter-spacing: -0.012em;
  line-height: 1.25;
  margin: 0;
}
.trow__title a { color: inherit; text-decoration: none; }
.trow__title a:hover { color: var(--room-ink); }

.trow__excerpt {
  font-size: 0.82rem;
  color: var(--ink-3);
  line-height: 1.45;
  margin: 0;
  overflow: hidden;
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
}

.trow__meta {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.2rem 0.55rem;
  font-size: 0.72rem;
  font-weight: 500;
  color: var(--ink-4);
}
.trow__meta b { color: var(--ink-2); font-weight: 500; }
.trow__meta svg { width: 12px; height: 12px; }
.trow__meta a { color: var(--ink-3); text-decoration: none; }
.trow__meta a:hover { color: var(--room-ink); }

/* The two states a moderator can put a thread in, said in words. An icon
   alone is a state somebody has to already know the meaning of. */
.tflag {
  display: inline-flex;
  align-items: center;
  gap: 0.28em;
  font-family: var(--font-body);
  font-size: 0.63rem;
  font-weight: 600;
  letter-spacing: 0.07em;
  text-transform: uppercase;
  padding: 0.2em 0.45em;
  border-radius: 4px;
  background: var(--room-wash);
  color: var(--room-ink);
}
.tflag svg { width: 11px; height: 11px; }
.tflag--locked { background: var(--p2-wash); color: var(--p2-ink); }

/* ---------- one thread ---------- */

.post {
  border: 1px solid var(--line);
  border-radius: var(--radius);
  background: var(--paper);
  padding: 1.2rem 1.35rem 1.3rem;
  display: flex;
  align-items: flex-start;
  gap: 1rem;
}

.post__main { min-width: 0; flex: 1 1 auto; }

.post__title {
  font-family: var(--font-display);
  font-weight: 600;
  font-size: 1.45rem;
  letter-spacing: -0.02em;
  line-height: 1.15;
  margin: 0 0 0.5rem;
}

.post__who {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.2rem 0.5rem;
  font-size: 0.75rem;
  color: var(--ink-4);
  margin: 0 0 0.9rem;
}
.post__who b { color: var(--ink-2); font-weight: 500; }

/* The body of a post, and of a reply. Both are plain text with links — see
   App\Support\PostBody — so this is the only prose styling either needs. */
.pbody {
  font-size: 0.9rem;
  line-height: 1.6;
  color: var(--ink);
  margin: 0;
  overflow-wrap: anywhere;
}
.pbody a { color: var(--room-ink); }

/* A removal is shown, not swept away — see ForumThread's migration. It reads
   as an absence rather than as content: no body, and a line saying who. */
.pgone {
  font-size: 0.85rem;
  font-style: italic;
  color: var(--ink-4);
  margin: 0;
}

.post__acts,
.reply__acts {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.35rem 0.75rem;
  margin-top: 0.85rem;
}
.post__acts form,
.reply__acts form { display: contents; }

/* Actions on a post are quiet by default and only colour on hover. A row of
   buttons under every reply would out-shout the replies. */
.pact {
  display: inline-flex;
  align-items: center;
  gap: 0.3em;
  font-family: var(--font-body);
  font-size: 0.72rem;
  font-weight: 500;
  color: var(--ink-4);
  background: none;
  border: 0;
  padding: 0;
  cursor: pointer;
  text-decoration: none;
  transition: color 0.18s var(--ease);
}
.pact:hover { color: var(--room-ink); }
.pact svg { width: 12px; height: 12px; }
.pact--warn:hover { color: var(--p2-ink); }

/* ---------- replies ---------- */

.replies { display: grid; gap: 0.1rem; }

.reply {
  display: flex;
  align-items: flex-start;
  gap: 0.7rem;
  padding: 0.85rem 0;
  border-bottom: 1px solid var(--line-soft);
  min-width: 0;
}
.reply:last-child { border-bottom: 0; }

.reply__main { min-width: 0; flex: 1 1 auto; }

.reply__who {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.35rem 0.5rem;
  font-size: 0.75rem;
  color: var(--ink-4);
  margin: 0 0 0.4rem;
}
.reply__who b { color: var(--ink-2); font-weight: 500; }

.reply__av {
  width: 22px;
  height: 22px;
  border-radius: 6px;
  display: grid;
  place-items: center;
  position: relative;
  overflow: hidden;
  font-family: var(--font-display);
  font-weight: 600;
  font-size: 0.56rem;
  color: #fff;
  background: var(--mk-ink);
  flex: none;
}

/* The second level, and the last one. The rule down the left is what says
   "this answers the thing above it" — see the migration for why there is no
   third level to draw. */
.reply--child {
  margin-left: 1.9rem;
  padding-left: 0.9rem;
  border-left: 2px solid var(--line);
}

/* ---------- writing something ---------- */

.composer {
  border: 1px solid var(--line);
  border-radius: var(--radius);
  background: var(--paper-2);
  padding: 0.9rem 1rem 1rem;
  display: grid;
  gap: 0.7rem;
}
.composer textarea {
  width: 100%;
  min-height: 5.5rem;
  resize: vertical;
  font-family: var(--font-body);
  font-size: 0.88rem;
  line-height: 1.55;
  color: var(--ink);
  background: var(--paper);
  border: 1px solid var(--line-hard);
  border-radius: var(--radius-sm);
  padding: 0.7rem 0.8rem;
}
.composer textarea:focus { outline: none; border-color: var(--room); }
.composer textarea::placeholder { color: var(--ink-4); }
.composer__row { display: flex; flex-wrap: wrap; align-items: center; gap: 0.6rem; }
.composer__note { font-size: 0.72rem; color: var(--ink-4); margin: 0; }

/* An inline reply box, opened under the thing it answers. */
.composer--inline { margin-top: 0.75rem; background: none; border-style: dashed; }

/* The reply box is a <details>, so it opens with no script and closes with no
   state stored anywhere. The marker goes because the summary is already styled
   as one of the row's actions; `list-style` covers Firefox, the -webkit
   pseudo-element covers Safari, and both are needed. */
.reply__answer summary { list-style: none; cursor: pointer; display: inline-flex; }
.reply__answer summary::-webkit-details-marker { display: none; }
.reply__answer summary::marker { content: ''; }

/* Opened, it needs the whole row — the actions strip is a wrap container, so
   without this the textarea tries to share a line with Report and Remove. */
.reply__answer[open] { flex: 1 0 100%; }

/* ---------- pagination ----------
   A forum is the first thing in the product with more rows than fit. Every
   other board is a finite day's worth and is narrowed in the browser. */

.pager { display: flex; flex-wrap: wrap; gap: 0.35rem; align-items: center; margin-top: 1.4rem; }
.pager a, .pager span {
  font-family: var(--font-body);
  font-size: 0.75rem;
  font-weight: 500;
  padding: 0.42em 0.7em;
  border-radius: 5px;
  border: 1px solid var(--line-hard);
  background: var(--paper);
  color: var(--ink-2);
  text-decoration: none;
}
.pager a:hover { border-color: var(--room); color: var(--room-ink); }
.pager .is-on { background: var(--bone); border-color: var(--bone); color: var(--void); }
.pager .is-off { opacity: 0.4; }
.pager .pager__of { border: 0; background: none; color: var(--ink-4); }

@media (max-width: 620px) {
  /* Nesting costs 2.8rem of a 320px screen, which is where the sentence
     breaks. The rule stays — it is what marks the level — and the indent goes. */
  .reply--child { margin-left: 0; padding-left: 0.7rem; }

  .post { padding: 1rem 1rem 1.1rem; }
  .post__title { font-size: 1.2rem; }

  /* Both arrows and the number have to stay a thumb's target. */
  .vote__b { width: 30px; height: 26px; }
  .vote--sm .vote__b { width: 28px; height: 24px; }
}

/* ---------------------------------------------------------------------------
   Played games — hours out of Steam.

   Deliberately not tinted. `--p3` is verification and `--room` is a shared
   artist; hours are neither a badge nor a taste, so they stay on the plain
   fact pill and earn their emphasis from weight and a rule rather than from a
   colour that already means something else.
--------------------------------------------------------------------------- */

/* Box art, in the one square everywhere draws it — the twin of `.artface`.

   Built as a box with a letter in it and the picture laid over the top, rather
   than an image with a fallback. Whether a given Steam app has a capsule cannot
   be known without fetching it, and a page render must not go and look; an
   `alt=""` image that fails renders nothing, so what is underneath is what
   shows. That is the whole fallback, and it costs no request and no JavaScript. */
.gameface {
  position: relative;
  display: grid;
  place-items: center;
  overflow: hidden;
  flex: none;
  background: var(--band);
  border: 1px solid var(--line);
  color: var(--ink-4);
  font-family: var(--font-display, inherit);
  font-weight: 600;
  line-height: 1;
  text-transform: uppercase;
}
.gameface__img {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
}

/* Steam's capsule is a 2:3 portrait. Cropping it square is deliberate: it
   matches the artist tiles beside it, and the capsule is drawn to be read as a
   small box, so its logo survives a centre crop where a wide header would not. */
.gameface--tile { width: 100%; aspect-ratio: 1; border-radius: var(--radius-sm); font-size: 1.6rem; }
.gameface--chip { width: 16px; height: 16px; border-radius: 3px; margin-left: -0.12em; font-size: 0.58rem; }

/* Hours under a tile. Its own line rather than part of the name, because the
   name is clamped to two lines and a number that sometimes lands on the third
   is a number that sometimes vanishes. */
.played__hrs {
  display: flex;
  align-items: center;
  gap: 0.4em;
  margin-top: -0.2rem;
  font-size: 0.68rem;
  font-weight: 600;
  color: var(--ink-3);
  font-variant-numeric: tabular-nums;
}

/* Played in the last fortnight. A dot, for the same reason the pill uses one:
   the palette's accents are spoken for, and "on it lately" is the smallest
   claim on the page. */
.played__hrs i {
  width: 5px;
  height: 5px;
  border-radius: 50%;
  background: var(--p1);
  flex: none;
}

/* The count. Tabular figures so a wrapped row of pills does not jitter as the
   numbers change, and a hairline in front so "Counter-Strike 2 412h" reads as
   two facts rather than a title with a number stuck to it. */
.pill b {
  font-weight: 650;
  color: var(--ink);
  font-variant-numeric: tabular-nums;
  padding-left: 0.42em;
  border-left: 1px solid var(--line);
}

/* On it in the last fortnight. A dot rather than another coloured pill: this
   is the smallest claim on the card and a fourth pill colour would make it the
   loudest. */
.pill--now { border-color: var(--line-hard); }
.pill--now::before {
  content: "";
  width: 5px;
  height: 5px;
  border-radius: 50%;
  background: var(--p1);
  flex: none;
}

/* ==========================================================================
   Ratings — a member's star average, and the control that gives one
   ========================================================================== */

/* The average, read-only. Two rows of five stars stacked: the lower one is the
   empty track, the upper one is clipped to a percentage of the width. A
   percentage rather than a count of whole stars, so 4.2 draws as 4.2 —
   rounding to the nearest star would quietly make every average wrong. */
.rating {
  display: inline-flex;
  align-items: center;
  gap: 0.34rem;
  font-size: 0.72rem;
  color: var(--ink-3);
  font-variant-numeric: tabular-nums;
}
.rating__stars { position: relative; display: inline-block; flex: none; line-height: 0; }
.rating__row { display: flex; gap: 1px; white-space: nowrap; }
.rating__row svg { width: 12px; height: 12px; flex: none; }
.rating__row--off { color: var(--line-hard); }
.rating__row--on {
  position: absolute;
  inset: 0 auto 0 0;
  overflow: hidden;
  color: var(--gold, var(--p1));
}
/* Filled, so a part-star reads as a part-star rather than as an outline that
   happens to be cut off. */
.rating__row--on svg { fill: currentColor; }
.rating b { color: var(--ink); font-weight: 600; }
.rating__n { color: var(--ink-4); }

/* The control. row-reverse is what lets hover light up "this star and the ones
   before it" with no script: star 5 is first in source order, and `~` only
   reaches forward, which on screen is leftward. */
.rate { display: flex; align-items: center; gap: 0.5rem; flex-wrap: wrap; }
.rate__label {
  font-size: 0.62rem;
  font-weight: 600;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--ink-4);
}
.rate__stars { display: flex; flex-direction: row-reverse; gap: 1px; }
.rate__star {
  background: none;
  border: 0;
  padding: 1px;
  cursor: pointer;
  line-height: 0;
  color: var(--line-hard);
  transition: color 0.12s var(--ease), transform 0.12s var(--ease);
}
.rate__star svg { width: 15px; height: 15px; display: block; }
.rate__star.is-on { color: var(--gold, var(--p1)); }
.rate__star.is-on svg { fill: currentColor; }
.rate__stars:hover .rate__star { color: var(--line-hard); }
.rate__stars:hover .rate__star svg { fill: none; }
.rate__star:hover,
.rate__star:hover ~ .rate__star,
.rate__star:focus-visible,
.rate__star:focus-visible ~ .rate__star {
  color: var(--gold, var(--p1));
}
.rate__star:hover svg,
.rate__star:hover ~ .rate__star svg,
.rate__star:focus-visible svg,
.rate__star:focus-visible ~ .rate__star svg { fill: currentColor; }
.rate__star:hover { transform: translateY(-1px); }

.rate__clear {
  background: none;
  border: 0;
  padding: 0;
  cursor: pointer;
  font-family: var(--font-body);
  font-size: 0.68rem;
  color: var(--ink-4);
  text-decoration: underline;
  text-underline-offset: 2px;
}
.rate__clear:hover { color: var(--ink-2); }

@media (prefers-reduced-motion: reduce) {
  .rate__star { transition: none; }
  .rate__star:hover { transform: none; }
}

/* The host's remove control on a roster row. Inline rather than behind a
   menu: it is one of two things a host does to a person and the other
   (Report or block) is already inline right beneath it. */
.kick { display: flex; gap: 0.4rem; align-items: center; flex-wrap: wrap; margin-top: 0.5rem; }
.kick input {
  flex: 1 1 12rem;
  min-width: 0;
  font-family: var(--font-body);
  font-size: 0.75rem;
  padding: 0.4rem 0.55rem;
  border-radius: var(--radius-sm);
  border: 1px solid var(--line);
  background: var(--panel);
  color: var(--ink);
}
.kick input::placeholder { color: var(--ink-4); }
.kick input:focus-visible { outline: 2px solid var(--room); outline-offset: 1px; }

/* ==========================================================================
   The voice dock — a live call, while you are looking at another room
   ========================================================================== */

/* Bottom-left and clear of the composer on every page that has one. Fixed
   rather than sticky: it belongs to the session, not to the document it
   happens to be floating over. It used to be offset by the rail's width; the
   rail is a strip in the top bar now, so the window's own edge is the mark. */
.vdock {
  position: fixed;
  left: 0.9rem;
  bottom: 0.9rem;
  z-index: 60;
  width: min(320px, calc(100vw - 1.8rem));
  max-height: 60vh;
  overflow-y: auto;
  border-radius: var(--radius);
  border: 1px solid var(--line-hard);
  background: var(--panel);
  box-shadow: 0 18px 40px rgba(0, 0, 0, 0.45);
}
.vdock[hidden] { display: none; }

/* The panel inside is the squad page's own voice block, moved here whole, so
   it arrives wearing .a-act. Strip the outer frame it was drawing against the
   page — the dock is the frame now. */
.vdock .a-act {
  border: 0;
  background: none;
  box-shadow: none;
  margin: 0;
  padding: 0.8rem 0.9rem;
}

/* It is a corner panel now, not a column: the device pickers and the mic
   meter would each be a line of their own at this width. */
.vdock .vc__roster { max-height: 30vh; overflow-y: auto; }
.vdock select { max-width: 100%; }

@media (max-width: 720px) {
  .vdock {
    left: 0.6rem;
    right: 0.6rem;
    bottom: 0.6rem;
    width: auto;
    max-height: 45vh;
  }
}

/* ==========================================================================
   Friends
   ========================================================================== */

.fr {
  border: 1px solid var(--line);
  border-radius: var(--radius);
  background: var(--paper);
  padding: 0.9rem;
  display: grid;
  gap: 0.6rem;
  align-content: start;
  min-width: 0;
}
.fr__who { display: flex; gap: 0.7rem; align-items: center; min-width: 0; }
.fr__av {
  width: 42px;
  height: 42px;
  border-radius: 12px;
  flex: none;
  position: relative;
  overflow: hidden;
  display: grid;
  place-items: center;
  font-family: var(--font-mono);
  font-size: 0.78rem;
  color: #fff;
}
.fr__id { min-width: 0; display: grid; gap: 0.15rem; }
.fr__tag {
  margin: 0;
  font-family: var(--font-display);
  font-size: 0.95rem;
  font-weight: 600;
  letter-spacing: -0.012em;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.fr__sub { font-size: 0.72rem; color: var(--ink-3); }
.fr__foot { display: flex; gap: 0.45rem; align-items: center; flex-wrap: wrap; }

/* The Top 8 as it will be seen, above the control that sets it — the order is
   the feature, so it has to be visible while you are choosing it. */
.tops { display: flex; flex-wrap: wrap; gap: 0.5rem; margin-bottom: 1rem; }
.tops__one {
  display: flex;
  align-items: center;
  gap: 0.45rem;
  padding: 0.3rem 0.6rem 0.3rem 0.35rem;
  border: 1px solid var(--line);
  border-radius: 999px;
  background: var(--panel);
  font-size: 0.78rem;
}
.tops__one b {
  font-family: var(--font-mono);
  font-size: 0.62rem;
  color: var(--ink-4);
  width: 1.1em;
  text-align: center;
}
.tops__av {
  width: 22px;
  height: 22px;
  border-radius: 7px;
  flex: none;
  position: relative;
  overflow: hidden;
  display: grid;
  place-items: center;
  font-family: var(--font-mono);
  font-size: 0.5rem;
  color: #fff;
}

/* Picking the Top 8. A number per row rather than drag handles: dragging needs
   JavaScript and a pointer, and this has to work on a phone with neither. */
.toppick {
  border: 1px solid var(--line);
  border-radius: var(--radius-sm);
  overflow: hidden;
  background: var(--panel);
  margin-bottom: 1.15rem;
}
.toppick__row {
  display: flex;
  align-items: center;
  gap: 0.7rem;
  padding: 0.55rem 0.8rem;
  cursor: pointer;
}
.toppick__row + .toppick__row { border-top: 1px solid var(--line); }
.toppick__row:hover { background: var(--panel-2); }
.toppick__av {
  width: 26px;
  height: 26px;
  border-radius: 8px;
  flex: none;
  position: relative;
  overflow: hidden;
  display: grid;
  place-items: center;
  font-family: var(--font-mono);
  font-size: 0.55rem;
  color: #fff;
}
.toppick__tag { flex: 1 1 auto; min-width: 0; font-size: 0.82rem; color: var(--ink); }
.toppick select { flex: none; width: 4.5rem; }

/* On a card the row is tighter and wraps rather than scrolling. */
.tops--card { margin: 0; gap: 0.35rem; }
.tops--card .tops__one { padding: 0.2rem 0.5rem 0.2rem 0.28rem; font-size: 0.7rem; gap: 0.35rem; }
.tops--card .tops__av { width: 18px; height: 18px; border-radius: 6px; font-size: 0.44rem; }

/* ==========================================================================
   Making room for the tab bar

   Last in the file, and that is the whole point of where it sits: every rule
   below overrides a component declared further up at the SAME specificity, so
   source order is the only thing that carries it. Written beside the tab bar
   in the nav section — which is where it belongs by subject — each of these
   would have been silently outranked by the component it was trying to move.

   Four things live against the bottom of the window, and below the sidebar
   breakpoint the tab bar is under all of them.
   ========================================================================== */
@media (max-width: 940px) {
  /* The last inch of a room's content is behind the tabs. 3.5rem of bottom
     padding was enough while nothing was over it. */
  .main__scroll { padding-bottom: calc(var(--tabbar) + 1.5rem); }

  /* Sticky to the bottom of the scrollport, and the scrollport's bottom edge
     is underneath the tab bar. Save and Discard are what it would have hidden
     — on the one page in the product where nothing is kept until they are
     pressed. */
  .savebar { bottom: var(--tabbar); }

  /* Both fixed to the window's bottom. The banner stacks on top of the tabs
     rather than over them: it is undismissable by design, and covering the
     navigation for the whole of an impersonated session is not what that was
     meant to buy. */
  .imp { bottom: var(--tabbar); }
  .vdock { bottom: calc(var(--tabbar) + 0.9rem); }
}

/* ==========================================================================
   PROFILES — a name you can click, and the card behind it
   ========================================================================== */

/* ---------- the name ----------

   Every gamertag in the app wears this: a forum byline, a chat line, a squad
   roster, a "Run by", a friends list, both halves of a settled duo post.

   It inherits its colour and weight rather than setting them, because it is
   drawn inside a <b>, an <h3>, a .side__item and a dozen other things that
   have already decided how a name looks there. What it adds is one hairline,
   faint at rest and solid on hover — enough to say "this does something"
   without turning a roster into a page of blue links. */
.plink {
  color: inherit;
  cursor: pointer;
  text-decoration: underline;
  text-decoration-thickness: 1px;
  text-decoration-color: var(--line-hard);
  text-underline-offset: 0.18em;
  border-radius: 3px;
  transition: color 0.12s var(--ease), text-decoration-color 0.12s var(--ease);
}
.plink:hover,
.plink:focus-visible {
  color: var(--room-ink, var(--ink));
  text-decoration-color: currentColor;
}

/* Where the whole row is already the control — a sidebar item with an icon and
   a label, a roster seat — the hairline would be drawn straight across it. The
   hook stays; only the underline goes. */
.plink--bare { text-decoration: none; }

/* A name with no account behind it — deleted, or masked on the duo board.
   Deliberately not a disabled link: there is nothing to press, and something
   that looks pressable and is not is worse than plain text. */
.plink--gone { cursor: default; text-decoration: none; color: inherit; }

/* ---------- the dialog ----------

   Built once by app.js and reused for every name on the page. Sized to the
   card rather than to the viewport: a profile is a column, and stretching one
   across a desktop leaves a row of pills with a metre of empty beside it. */
.pdlg {
  width: min(34rem, calc(100vw - 2rem));
  max-height: min(46rem, calc(100vh - 3rem));
  padding: 0;
  border: 1px solid var(--line-hard);
  border-radius: var(--radius);
  background: var(--panel);
  color: var(--ink);
  overflow: hidden;
}
.pdlg::backdrop { background: rgb(0 0 0 / 0.6); }

/* The inner panel is what makes click-outside-to-dismiss testable: <dialog>
   paints its backdrop on the element itself, so a click whose target IS the
   dialog is a click on the backdrop. Same trick as .fsheet. */
.pdlg__in { position: relative; max-height: inherit; display: grid; }

.pdlg__body { overflow-y: auto; padding: 1.15rem 1.15rem 1.25rem; }
.pdlg__body.is-working { opacity: 0.62; transition: opacity 0.12s var(--ease); }

.pdlg__wait {
  font-family: var(--font-body);
  font-size: 0.78rem;
  color: var(--ink-3);
  margin: 0;
  padding: 2.5rem 0;
  text-align: center;
}

/* Floats over the card rather than sitting in a header row of its own: the
   card already opens with a name and an avatar, and a second title bar above
   it would say the same thing twice at the cost of the first screenful. */
.pdlg__x {
  position: absolute;
  top: 0.55rem;
  right: 0.55rem;
  z-index: 2;
  display: grid;
  place-items: center;
  width: 30px;
  height: 30px;
  border: 1px solid var(--line);
  border-radius: 8px;
  background: var(--panel-2);
  color: var(--ink-3);
  cursor: pointer;
}
.pdlg__x:hover { color: var(--ink); border-color: var(--line-hard); }
.pdlg__x svg { width: 15px; height: 15px; }

/* ---------- the card ----------

   One partial, drawn twice: as the body of the dialog above and as the whole
   of /app/people/{id}. It carries no surface of its own — the dialog and the
   page each provide one — so it is a stack of sections and nothing else. */
.pcard { display: grid; gap: 0.85rem; align-content: start; }

.pcard__head {
  display: grid;
  grid-template-columns: auto minmax(0, 1fr) auto;
  gap: 0.75rem;
  align-items: center;
  /* room for the close button, which floats over this row */
  padding-right: 2.2rem;
}
.pcard__av { width: 52px; height: 52px; font-size: 1.15rem; border-radius: 11px; }
.pcard__who { min-width: 0; }
.pcard__tag { font-size: 1.2rem; }
.pcard__mic { align-self: center; }

.pcard__sec { display: grid; gap: 0.4rem; }

/* Dating photographs, and only where ProfileCard::dating() said so. A fixed
   grid rather than a carousel: this is already inside a dialog, and anyone who
   wants the full thing has the deck it came from. */
.pcard__shots {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(6.5rem, 1fr));
  gap: 0.4rem;
}
.pcard__shot {
  width: 100%;
  aspect-ratio: 3 / 4;
  object-fit: cover;
  border-radius: var(--radius-sm);
  border: 1px solid var(--line);
  display: block;
}

/* The three things one member does about another. Wraps rather than scrolls —
   on a phone the star row alone is most of a line. */
.pcard__foot {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.5rem;
  padding-top: 0.8rem;
  border-top: 1px solid var(--line);
}
.pcard__gap { flex: 1 1 auto; }

/* The page version. The same column, centred, so a link somebody pasted opens
   something that looks like the dialog it usually is. */
.pcard-page {
  max-width: 34rem;
  border: 1px solid var(--line);
  border-radius: var(--radius);
  background: var(--panel);
  padding: 1.15rem;
}

@media (prefers-reduced-motion: reduce) {
  .plink { transition: none; }
  .pdlg__body.is-working { transition: none; }
}

/* A full-width card on a phone, anchored to the bottom where a thumb is —
   the same shape the filter sheet takes. */
@media (max-width: 520px) {
  .pdlg {
    width: 100vw;
    max-width: 100vw;
    max-height: 88vh;
    margin: auto auto 0;
    border-radius: var(--radius) var(--radius) 0 0;
    border-bottom: 0;
  }
  .pcard__head { grid-template-columns: auto minmax(0, 1fr); }
  .pcard__mic { grid-column: 2; justify-self: start; }
}
