/* ============================================================================
   Alliances — crews signing on to one challenge.

   The board is the same hairline-ruled column the inbox and the suggestion
   board keep: an alliance is a line in a shared muster roll, not a floating
   card. Every row leads with the challenge, because nobody joins a name —
   they join the puzzle behind it.

   The room mounts the transcript from `chat.css` — the same object a
   direct-message thread holds, and not a copy of it — with the one change the
   group demands: every run is hung off its author's face and named, since
   "the other person" is no longer a single known face. The roster sits beside
   it so you always know who can read what you write.
   ========================================================================= */

/* ---------------------------------------------------------------------------
   Page
   ------------------------------------------------------------------------ */

.al {
  padding-block: var(--sp-8) var(--sp-16);
}

.al__col {
  max-width: 860px;
  margin-inline: auto;
}

.al__col--narrow {
  max-width: 620px;
}

.al__col--room {
  max-width: 1080px;
}

.al__head {
  margin-block-end: var(--sp-6);
}

/* Title and the one primary action share a line and wrap to two on a phone. */
.al__headline {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--sp-4);
  flex-wrap: wrap;
}

.al__title {
  font-size: var(--fs-3xl);
  font-weight: 700;
  line-height: var(--lh-tight);
  letter-spacing: var(--tr-display);
  color: var(--ink-050);
  margin: 0;
}

.al__subtitle {
  color: var(--ink-300);
  max-width: 58ch;
  margin: var(--sp-2) 0 0;
}

.al__create {
  flex: none;
}

/* The way back, above the title on every inner surface. */
.al__back {
  display: inline-flex;
  align-items: center;
  gap: var(--sp-2);
  margin-block-end: var(--sp-4);
  font-size: var(--fs-sm);
  color: var(--ink-300);
  text-decoration: none;
}

.al__back:hover {
  color: var(--ink-050);
}

.al-login {
  display: flex;
  align-items: center;
  gap: var(--sp-3);
  padding: var(--sp-4) var(--sp-5);
  margin-block-end: var(--sp-6);
  color: var(--ink-300);
}

.al-login__text {
  margin: 0;
}

.al-section + .al-section {
  margin-block-start: var(--sp-10);
}

.al-section__title {
  font-size: var(--fs-lg);
  font-weight: 600;
  color: var(--ink-100);
  margin: 0 0 var(--sp-3);
}

.al-pagination {
  margin-block-start: var(--sp-8);
  display: flex;
  justify-content: center;
}

/* ---------------------------------------------------------------------------
   The board — one panel, hairline-ruled rows.
   ------------------------------------------------------------------------ */

.al-board {
  background: var(--ink-950);
  border: 1px solid var(--line);
  border-radius: var(--r-lg);
  overflow: hidden;
}

.al-list {
  list-style: none;
  margin: 0;
  padding: 0;
}

.al-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--sp-4);
  padding: var(--sp-4) var(--sp-5);
  border-block-start: 1px solid var(--line);
}

.al-list > .al-row:first-child {
  border-block-start: 0;
}

.al-row:hover {
  background: var(--ink-900);
}

/* The crew's picture, cropped square so a wall of rows keeps one rhythm no
   matter what shape was uploaded. Fixed size, never a flex victim. */
/* A crest over the crew's own identicon. The mark is the floor of the box, not
   a substitute for a missing file, so a slow, refused or dead URL still leaves
   a crest here rather than the 64px hole the board used to show. */
.al-row__thumb {
  --identicon-size: 40px;
  position: relative;
  flex: none;
  display: block;
  width: 64px;
  height: 64px;
  border: 1px solid var(--line);
  border-radius: var(--r-md);
  overflow: hidden;
  background: var(--ink-900);
}

/* Takes the slack so the row reads picture-then-crew with the action alone on
   the right. Without it `space-between` spreads three children evenly and the
   text drifts away from the picture it belongs to. */
.al-row__body {
  flex: 1;
  min-width: 0;
}

/* The challenge, printed above the crew's own name — it is what the row is
   about. Set small and in secondary ink so the name still leads visually. */
.al-row__target {
  display: flex;
  align-items: center;
  gap: var(--sp-2);
  margin: 0 0 var(--sp-1);
  font-size: var(--fs-xs);
  color: var(--ink-400);
  min-width: 0;
}

.al-row__target svg {
  flex: none;
}

.al-row__target-link {
  color: var(--ink-300);
  text-decoration: none;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  unicode-bidi: isolate;
}

.al-row__target-link:hover {
  color: var(--accent);
}

/* A challenge whose channel puzzle is gone. Stated, not linked. */
.al-row__target-link.is-gone,
.al-room__target-link.is-gone,
.al-card__target-link.is-gone {
  color: var(--ink-500);
  font-style: italic;
}

.al-row__name {
  display: flex;
  align-items: center;
  gap: var(--sp-2);
  flex-wrap: wrap;
  margin: 0;
  font-size: var(--fs-md);
  font-weight: 600;
  line-height: var(--lh-snug);
}

.al-row__link {
  color: var(--ink-050);
  text-decoration: none;
}

.al-row__link:hover {
  color: var(--accent);
}

.al-row__desc {
  margin: var(--sp-1) 0 0;
  font-size: var(--fs-sm);
  color: var(--ink-300);
  overflow-wrap: anywhere;
}

.al-row__meta {
  display: flex;
  align-items: center;
  gap: var(--sp-2);
  flex-wrap: wrap;
  margin-block-start: var(--sp-2);
  font-size: var(--fs-xs);
  color: var(--ink-400);
}

.al-row__count {
  display: inline-flex;
  align-items: center;
  gap: var(--sp-1);
}

.al-meta-dot {
  color: var(--ink-600);
}

.al-row__action {
  flex: none;
}

/* ---------------------------------------------------------------------------
   The composer for a new alliance.
   ------------------------------------------------------------------------ */

.al-form__body {
  display: flex;
  flex-direction: column;
  gap: var(--sp-5);
}

.al-form__fieldset {
  margin: 0;
  padding: 0;
  border: 0;
}

.al-form__legend {
  padding: 0;
  margin-block-end: var(--sp-3);
  color: var(--ink-300);
}

.al-form__choices {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
  gap: var(--sp-3);
}

/* A radio that takes the whole panel as its hit area, because on a phone the
   12px dot is not a target. */
.al-choice {
  display: flex;
  align-items: flex-start;
  gap: var(--sp-3);
  padding: var(--sp-3) var(--sp-4);
  background: var(--ink-900);
  border: 1px solid var(--line);
  border-radius: var(--r-md);
  cursor: pointer;
}

.al-choice:hover {
  border-color: var(--line-hi);
}

.al-choice:has(input:checked) {
  border-color: var(--accent);
  background: var(--accent-wash);
}

.al-choice:has(input:focus-visible) {
  box-shadow: 0 0 0 2px var(--ring);
}

.al-choice input {
  margin-block-start: 3px;
  accent-color: var(--accent);
  flex: none;
}

.al-choice__body {
  display: flex;
  flex-direction: column;
  gap: 2px;
  min-width: 0;
}

.al-choice__name {
  font-size: var(--fs-sm);
  font-weight: 600;
  color: var(--ink-050);
}

.al-choice__hint {
  font-size: var(--fs-xs);
  color: var(--ink-400);
}

.al-form__slot {
  margin-block-start: var(--sp-4);
}

.al-form__slot[hidden] {
  display: none;
}

.al-form__actions {
  display: flex;
  justify-content: flex-end;
  gap: var(--sp-3);
}

/* ---------------------------------------------------------------------------
   The room.

   For a member this surface is not a page about an alliance, it is the room
   itself, so it is built as one object the height of the screen rather than as
   a cover sheet with a chat box under it. The old shape spent 446px — a banner,
   a title, a description and a button — before the transcript began, then gave
   the transcript a fixed 561px well that could neither grow into a tall monitor
   nor shrink out of a short one, and left 345px of nothing under the composer
   while the roster rail kept going. A member's first screen was 20% of a
   conversation, three quarters of the way down.

   Now: an identity bar across the top, the transcript filling everything under
   it with the composer on its floor, and the rail beside it. The page does not
   scroll; the two columns inside it do.
   ------------------------------------------------------------------------ */

.al--room {
  /* The frame's own gutter, and the height that has to come off the viewport
     to reach it: the header, this padding, and the way-back line above. */
  padding-block: var(--sp-6);
}

.al-room {
  display: grid;
  grid-template-columns: minmax(0, 1fr) 304px;
  grid-template-rows: auto minmax(0, 1fr);
  /* dvh, because a phone's viewport is shorter than 100vh whenever the browser
     chrome is showing, and a composer under the address bar cannot be typed
     into. The vh line is the fallback for clients without dvh. */
  height: calc(100vh - var(--h-header) - var(--sp-6) * 2 - 34px);
  height: calc(100dvh - var(--h-header) - var(--sp-6) * 2 - 34px);
  min-height: 520px;
  background: var(--ink-950);
  border: 1px solid var(--line);
  border-radius: var(--r-lg);
  overflow: hidden;
}

/* Everything a returning member has already read, on one line: which room this
   is, what it is about, and the way out. */
.al-room__bar {
  grid-column: 1 / -1;
  display: flex;
  align-items: center;
  gap: var(--sp-3);
  padding: var(--sp-3) var(--sp-4);
  background: var(--ink-900);
  border-block-end: 1px solid var(--line);
}

/* The crew's picture, cropped square. At banner width it was 216px of the
   first screen and the one thing on it carrying no information; at 44px it
   still says which room this is faster than the title does. */
.al-room__crest {
  --identicon-size: 28px;
  position: relative;
  flex: none;
  display: block;
  width: 44px;
  height: 44px;
  overflow: hidden;
  border: 1px solid var(--line);
  border-radius: var(--r-md);
  background: var(--ink-850);
}

.al-room__ident {
  display: grid;
  gap: 2px;
  flex: 1;
  min-width: 0;
}

.al-room__titleline {
  display: flex;
  align-items: center;
  gap: var(--sp-2);
  min-width: 0;
}

/* One line, and truncated rather than wrapped: the bar is chrome, and a crew
   with an 80-character name must not push the transcript down the screen. The
   whole of it stays on the element's title. */
.al-room__title {
  font-size: var(--fs-lg);
  font-weight: 700;
  line-height: var(--lh-tight);
  letter-spacing: var(--tr-tight);
  color: var(--ink-050);
  margin: 0;
  min-width: 0;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  unicode-bidi: isolate;
}

.al-room__titleline .chip {
  flex: none;
}

.al-room__target {
  display: flex;
  align-items: center;
  gap: var(--sp-2);
  margin: 0;
  min-width: 0;
  font-size: var(--fs-sm);
  color: var(--ink-300);
}

.al-room__target svg {
  flex: none;
  color: var(--ink-400);
}

.al-room__target-key {
  flex: none;
  color: var(--ink-400);
}

.al-room__target-link {
  color: var(--ink-100);
  text-decoration: none;
  border-block-end: 1px solid var(--line-hi);
  unicode-bidi: isolate;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.al-room__target-link:hover {
  color: var(--accent);
  border-color: var(--accent);
}

.al-room__actions {
  flex: none;
  display: flex;
  gap: var(--sp-2);
}

/* The transcript column: everything under the bar, with the composer on its
   floor. `min-height: 0` is what lets the transcript inside it scroll instead
   of stretching the grid row past the frame. */
.al-room__main {
  grid-column: 1;
  grid-row: 2;
  display: flex;
  flex-direction: column;
  min-width: 0;
  min-height: 0;
  border-inline-end: 1px solid var(--line);
}

/* The rail: what you consult rather than read. It scrolls on its own, and the
   invite is pinned to its floor the way the composer is pinned to the
   transcript's — both are the thing you came to the column to do, and neither
   should be reached by scrolling past a roster. */
.al-room__rail {
  grid-column: 2;
  grid-row: 2;
  display: flex;
  flex-direction: column;
  min-height: 0;
  background: var(--ink-900);
}

.al-room__rail-scroll {
  flex: 1;
  min-height: 0;
  overflow-y: auto;
  overscroll-behavior: contain;
}

/* A section of the rail. Inside a frame that is already bordered these carry a
   hairline and nothing else — a panel here would be a card inside a card. */
.al-rail + .al-rail {
  border-block-start: 1px solid var(--line);
}

.al-rail__head,
.al-rail__title {
  margin: 0;
}

.al-rail__head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--sp-3);
  padding: var(--sp-4) var(--sp-4) var(--sp-2);
}

.al-rail__title {
  color: var(--ink-400);
}

.al-rail--about {
  padding: var(--sp-4);
}

.al-about {
  font-size: var(--fs-sm);
  line-height: var(--lh-body);
  color: var(--ink-200);
  overflow-wrap: anywhere;
}

.al-about p {
  margin: 0 0 var(--sp-2);
}

.al-about p:last-child {
  margin-block-end: 0;
}

/* ---------------------------------------------------------------------------
   The transcript.

   The transcript, its day rules, its lines and the composer under them are
   `chat.css` — the same object a direct-message thread holds, so the two
   surfaces read as one system and a fix to either is a fix to both. The
   difference is what encloses it: a thread is a file on a page, a room is the
   page.

   Naming someone is `mentions.css` for the same reason — a puzzle's comments
   name people too, and the chip and its picker had to stop being a thing only
   a room could do.

   What is left here is what only a room has: a picture on its way out.
   ------------------------------------------------------------------------ */

/* The composer's own additions — the shared one is `.chat-composer`, which
   this element carries too and which supplies the containing block the name
   picker is positioned against. */

/* Dragging a picture over the composer says so before the drop. */
.al-composer.is-dropping {
  background: var(--ink-850);
  box-shadow: inset 0 0 0 2px var(--accent);
}

/* The label is the control: the file input beside it is off-screen but still
   the thing that takes focus and opens the picker, so the ring is drawn here
   from the input's state. */
.al-composer__attach {
  flex: none;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 44px;
  height: 44px;
  border: 1px solid var(--line);
  border-radius: var(--r-md);
  background: var(--ink-850);
  color: var(--ink-300);
  cursor: pointer;
  transition: color var(--dur-1) var(--ease), background-color var(--dur-1) var(--ease),
    border-color var(--dur-1) var(--ease);
}

.al-composer__attach:hover {
  background: var(--ink-800);
  border-color: var(--line-hi);
  color: var(--ink-050);
}

.al-composer__file:focus-visible + .al-composer__attach {
  box-shadow: var(--ring);
  border-color: var(--accent);
  color: var(--ink-050);
}

/* The picture waiting to be sent, above the row it will go out with. */
.al-attach {
  display: flex;
  align-items: center;
  gap: var(--sp-3);
  margin-block-end: var(--sp-3);
  padding: var(--sp-2);
  background: var(--ink-850);
  border: 1px solid var(--line);
  border-radius: var(--r-md);
}

.al-attach[hidden] {
  display: none;
}

.al-attach__img {
  flex: none;
  width: 44px;
  height: 44px;
  object-fit: cover;
  border-radius: var(--r-sm);
  background: var(--ink-950);
}

.al-attach__name {
  flex: 1;
  min-width: 0;
  font-size: var(--fs-xs);
  color: var(--ink-300);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  unicode-bidi: isolate;
}

.al-attach__remove {
  flex: none;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 32px;
  height: 32px;
  padding: 0;
  border: 1px solid transparent;
  border-radius: var(--r-sm);
  background: transparent;
  color: var(--ink-400);
  cursor: pointer;
}

.al-attach__remove:hover {
  background: var(--ink-800);
  border-color: var(--line);
  color: var(--ink-050);
}

/* ---------------------------------------------------------------------------
   The words board.

   A crew working a seed phrase spends its time on the same question over and
   over: is this word one of ours. So the board is a ranking, not a list — the
   words the crew believes sit at the top, the ones it has ruled against sink,
   and every row carries the same control for saying which.

   It shares the column with the transcript and swaps with it, which is why the
   two are built the same way round: one thing pinned, one thing scrolling. The
   pinned part is at the top here, because a ranking is read downwards from its
   best answer while a conversation ends at the bottom.
   ------------------------------------------------------------------------ */

/* The tab bar belongs to the column it switches, not to the frame: the rail
   beside it serves both tabs. */
.al-room__tabs {
  flex: none;
  padding-inline: var(--sp-4);
  background: var(--ink-900);
  border-block-end: 1px solid var(--line);
}

.al-room__tabs .tabs-nav__btn {
  display: inline-flex;
  align-items: center;
  gap: var(--sp-2);
  background: transparent;
  border-block-start: 0;
  border-inline: 0;
  font-family: var(--font-ui);
  cursor: pointer;
}

.al-room__tabcount {
  padding: 0.0625rem 0.375rem;
  border-radius: var(--r-sm);
  background: var(--ink-800);
  color: var(--ink-300);
  font-size: var(--fs-2xs);
  font-weight: 700;
  line-height: 1.4;
  transition: background-color var(--dur-1) var(--ease),
    color var(--dur-1) var(--ease);
}

.al-room__tabs .tabs-nav__btn.active .al-room__tabcount {
  background: var(--accent-wash);
  color: var(--accent);
}

/* Each pane is the whole column under the bar. The `hidden` attribute has to
   win over the flex display, or a swapped-out tab would keep its space. */
.al-room__pane {
  display: flex;
  flex-direction: column;
  flex: 1;
  min-height: 0;
}

.al-room__pane[hidden] {
  display: none;
}

.al-room__pane:focus-visible {
  outline: none;
  box-shadow: inset 0 0 0 2px var(--ink-950), inset 0 0 0 4px var(--accent);
}

.al-words {
  display: flex;
  flex-direction: column;
  flex: 1;
  min-height: 0;
}

.al-words__scroll {
  flex: 1;
  /* A floor, because the settings panel below unfolds into this column and
     would otherwise squeeze the board down to a hairline — the leader would
     lose sight of the thing they are configuring. */
  min-height: 120px;
  overflow-y: auto;
  overscroll-behavior: contain;
  padding: var(--sp-3) var(--sp-4) var(--sp-5);
}

/* --- Adding a word ------------------------------------------------------- */

.al-wordform {
  flex: none;
  padding: var(--sp-3) var(--sp-4);
  background: var(--ink-900);
  border-block-end: 1px solid var(--line);
}

.al-wordform__row {
  display: flex;
  align-items: center;
  gap: var(--sp-3);
}

/* The field is set in the machine face and forced LTR: what goes in it is a
   token out of a wordlist, and under an RTL interface a Latin word typed into
   a right-aligned box reads as if it were being written backwards. */
.al-wordform__input {
  flex: 1;
  min-width: 0;
  min-height: 44px;
  font-family: var(--font-mono);
  direction: ltr;
}

.al-wordform__add {
  flex: none;
  min-height: 44px;
}

.al-wordform__note {
  margin: var(--sp-2) 0 0;
  font-size: var(--fs-xs);
}

/* --- The ranking --------------------------------------------------------- */

.al-wordlist {
  display: flex;
  flex-direction: column;
  gap: var(--sp-2);
  margin: 0;
  padding: 0;
  list-style: none;
}

.al-word {
  display: flex;
  align-items: center;
  gap: var(--sp-3);
  padding: var(--sp-2) var(--sp-3);
  background: var(--ink-900);
  border: 1px solid var(--line);
  border-radius: var(--r-md);
}

.al-word__body {
  flex: 1;
  min-width: 0;
}

.al-word__text {
  margin: 0;
  font-size: var(--fs-md);
  font-weight: 600;
  color: var(--ink-050);
  overflow-wrap: anywhere;
}

.al-word__meta {
  display: flex;
  align-items: center;
  gap: var(--sp-2);
  margin: 2px 0 0;
  font-size: var(--fs-xs);
  color: var(--ink-400);
  min-width: 0;
}

.al-word__author {
  color: var(--ink-300);
  text-decoration: none;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.al-word__author:hover {
  color: var(--accent);
}

.al-word__dot {
  color: var(--ink-600);
}

.al-word__time {
  flex: none;
  white-space: nowrap;
}

.al-word__remove-form {
  margin: 0;
  line-height: 0;
}

/* Quiet until the row is reached: taking a word off the board is not the point
   of the row, it is the correction of a mistake on it. */
.al-word__remove {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 32px;
  height: 32px;
  padding: 0;
  background: transparent;
  border: none;
  border-radius: var(--r-sm);
  color: var(--ink-500);
  cursor: pointer;
  transition: background-color var(--dur-1) var(--ease),
    color var(--dur-1) var(--ease);
}

.al-word__remove:hover {
  background: var(--danger-wash);
  color: var(--danger);
}

/* --- The vote ------------------------------------------------------------ */

/* Same control as the suggestions board, at the size this row can carry: two
   arrows around a signed figure. Agreement is the platform's green and
   disagreement its red, which is what those two colours mean everywhere else
   on the site. */
.al-wvote {
  flex: none;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 1px;
}

.al-wvote__form {
  margin: 0;
  line-height: 0;
}

.al-wvote__btn {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 34px;
  height: 22px;
  padding: 0;
  background: transparent;
  border: none;
  border-radius: var(--r-sm);
  color: var(--ink-400);
  cursor: pointer;
  transition: background-color var(--dur-1) var(--ease),
    color var(--dur-1) var(--ease);
}

.al-wvote__btn:hover {
  background: var(--ink-800);
  color: var(--ink-100);
}

.al-wvote__btn--up.is-active {
  background: var(--solved-wash);
  color: var(--solved);
}

.al-wvote__btn--down.is-active {
  background: var(--danger-wash);
  color: var(--danger);
}

.al-wvote__score {
  min-width: 3ch;
  font-size: var(--fs-sm);
  font-weight: 700;
  color: var(--ink-200);
  text-align: center;
  /* A signed figure: without isolation the bidi algorithm moves the sign to
     the other side of the digits under dir="rtl" ("+12" renders as "12+"). */
  direction: ltr;
  unicode-bidi: isolate;
}

.al-wvote__score.is-up {
  color: var(--solved);
}

.al-wvote__score.is-down {
  color: var(--danger);
}

/* --- The leader's switch, pinned to the board's floor -------------------- */

/* Shrinkable and scrollable rather than fixed: unfolded it is the tallest
   thing in the column, and on a short window it gives way to the board instead
   of pushing it off the bottom. */
.al-wordset {
  flex: 0 1 auto;
  min-height: 0;
  overflow-y: auto;
  overscroll-behavior: contain;
  background: var(--ink-900);
  border-block-start: 1px solid var(--line);
}

/* Unfolded over a running board it takes at most half the column and scrolls
   inside that: a form of four controls is not worth pushing the board it
   configures out of sight, and the leader is choosing a list for words they
   need to be able to see. Over a board that is off there is nothing above it
   to protect — the panel is the only thing on the tab worth doing — so it
   takes the room it needs. */
.al-words--live .al-wordset[open] {
  max-block-size: 52%;
}

.al-wordset__toggle {
  display: flex;
  align-items: center;
  gap: var(--sp-2);
  padding: var(--sp-3) var(--sp-4);
  font-size: var(--fs-sm);
  font-weight: 600;
  color: var(--ink-300);
  cursor: pointer;
  list-style: none;
}

.al-wordset__toggle::-webkit-details-marker {
  display: none;
}

.al-wordset__toggle:hover {
  color: var(--ink-100);
}

.al-wordset[open] .al-wordset__toggle {
  color: var(--ink-100);
  border-block-end: 1px solid var(--line);
}

.al-wordset__form {
  display: flex;
  flex-direction: column;
  gap: var(--sp-4);
  padding: var(--sp-4);
}

.al-wordset__actions {
  display: flex;
  justify-content: flex-end;
}

/* ---------------------------------------------------------------------------
   Roster.

   The rail is 300px and the row used to be a flex line that wrapped when it
   ran out of it, which meant a display name and a level badge decided the
   row's height between them: nine seats came out 74, 75, 47, 47, 75, 75, 47,
   47, 75, a 28px swing on no rule a reader could perceive, and the badges
   landed on nine different inline offsets. Every row is now the same grid and
   the identity stack is exactly two lines whatever it holds, so the column has
   one geometry, one badge column, and a shorter panel than the ragged one it
   replaces.
   ------------------------------------------------------------------------ */

.al-roster__head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--sp-3);
}

.al-roster__count {
  font-size: var(--fs-sm);
  color: var(--ink-400);
}

/* In the rail the roster is the section that grows, so its own list runs to the
   bottom of the scroll and the rows narrow their gutter to the rail's. */
.al-roster--rail .al-roster__row {
  padding-inline: var(--sp-4);
}

.al-roster__list {
  list-style: none;
  margin: 0;
  padding: 0;
}

/* Face, identity, control. `auto` on the outer tracks lets the control column
   collapse to nothing on the rows that have none, so a roster nobody can edit
   does not carry an empty gutter down its end edge. */
.al-roster__row {
  position: relative;
  display: grid;
  grid-template-columns: auto minmax(0, 1fr) auto;
  align-items: center;
  gap: var(--sp-3);
  /* A two-line stack is taller than the one-line rows it replaces, so the
     block padding comes down a step to pay for it: the panel ends up shorter
     than the ragged version rather than merely more regular, which matters
     because the roster is ordered before the transcript on a phone. */
  padding: var(--sp-2) var(--sp-5);
  border-block-start: 1px solid var(--line);
  transition: background-color var(--dur-1) var(--ease);
}

.al-roster__list > .al-roster__row:first-child {
  border-block-start: 0;
}

/* The whole row is one target, so it takes the hover fill rather than leaving
   the visitor to find the few pixels the name happens to occupy. */
.al-roster__row:hover {
  background: var(--ink-800);
}

.al-roster__face {
  /* 28px, not the 22px `--sm`: at 22px a face in a list is a coloured dot, and
     the avatar is the only thing distinguishing two people who share a level
     and truncate to the same eleven characters. */
  --avatar-size: 28px;
}

/* Two rows, always — never a wrap. A grid rather than a flex column so the
   name and the standing line hold their tracks whatever their content. */
.al-roster__ident {
  display: grid;
  gap: 2px;
  min-width: 0;
}

.al-roster__name {
  font-size: var(--fs-base);
  font-weight: 600;
  line-height: 1.25;
  color: var(--ink-100);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  unicode-bidi: isolate;
  transition: color var(--dur-1) var(--ease);
}

.al-roster__row:hover .al-roster__name {
  color: var(--ink-050);
}

.al-roster__link {
  color: inherit;
  text-decoration: none;
}

/* One stretched target per row. */
.al-roster__link::after {
  content: "";
  position: absolute;
  inset: 0;
  z-index: 1;
}

/* The ring belongs on the row, not on the few characters of the name: the
   link's hit area is the whole row, so reshaping the one focus treatment onto
   the stretched pseudo-element is what makes the ring describe what Enter will
   actually open. */
.al-roster__link:focus-visible {
  outline: none;
}

.al-roster__link:focus-visible::after {
  border-radius: var(--r-sm);
  box-shadow: var(--ring);
}

.al-roster__standing {
  display: flex;
  align-items: center;
  gap: var(--sp-2);
  min-width: 0;
}

/* The seat in this room outranks the rank on the platform — one is about this
   alliance, the other about a profile — so Líder takes heading white and the
   level keeps its hue. Neither takes the accent: this system spends orange on
   money and the primary action, and a roster holds neither. */
.al-roster__role {
  flex: none;
  font-size: var(--fs-2xs);
  font-weight: 700;
  line-height: 1.4;
  letter-spacing: var(--tr-label);
  text-transform: uppercase;
  color: var(--ink-050);
  white-space: nowrap;
}

/* Drawn, not a middot: a 2px rule-coloured dot separates the two standings
   without putting a glyph in the text for a screen reader to announce. */
.al-roster__role::after {
  content: "";
  display: inline-block;
  width: 2px;
  height: 2px;
  margin-inline-start: var(--sp-2);
  vertical-align: middle;
  border-radius: var(--r-full);
  background: var(--ink-600);
}

/* The level badge is a plate everywhere else in the app, and a plate is right
   where it appears once — beside a message, under a card. Nine of them stacked
   in a 300px rail turn a list of people into a column of buttons and push the
   names, which are the actual content of a participant list, into second
   place. Here the badge keeps its hue and its label scale and gives up its
   fill, its border and its padding. */
.al-roster .user-level-badge {
  min-width: 0;
  padding: 0;
  background: none;
  border-color: transparent;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* Two levels state their colour as a fill rather than as ink, so stripping the
   fill would leave them unreadable: Leigo's `--ink-400` is 4.31:1 against the
   row's hover fill, under the floor, and Mestre Caçatal's near-black
   `--accent-ink` disappears outright. Level 5 lifts to `--accent-hi` rather
   than borrowing level 4's `--accent`, which would have flattened the top two
   steps of the ramp into one colour. */
.al-roster .user-level-badge.level-0 {
  color: var(--ink-300);
}

.al-roster .user-level-badge.level-5 {
  color: var(--accent-hi);
}

/* Removing someone is the owner's control and it sits on every row but their
   own, so it has to stay reachable without being loud. Raised over the row's
   stretched link, which would otherwise swallow the click. */
.al-roster__remove-form {
  position: relative;
  z-index: 2;
  display: flex;
}

.al-roster__remove {
  display: flex;
  align-items: center;
  justify-content: center;
  /* The mark is 14px; the target is 34px square and the row's own padding
     carries the rest, which clears the touch minimum without cutting a 44px
     hole into a 300px rail. */
  width: 34px;
  height: 34px;
  margin-inline-end: calc(var(--sp-2) * -1);
  padding: 0;
  border: 0;
  border-radius: var(--r-sm);
  background: none;
  color: var(--ink-400);
  cursor: pointer;
  transition: color var(--dur-1) var(--ease), background-color var(--dur-1) var(--ease);
}

.al-roster__remove:hover {
  background: var(--ink-750);
  color: var(--danger);
}

/* ---------------------------------------------------------------------------
   Invite panel — pinned to the rail's floor.
   ------------------------------------------------------------------------ */

.al-invite {
  flex: none;
  padding: var(--sp-3) var(--sp-4) var(--sp-4);
  border-block-start: 1px solid var(--line);
  background: var(--ink-850);
}

.al-invite__head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--sp-2);
  margin-block-end: var(--sp-2);
}

.al-invite__title {
  margin: 0;
  color: var(--ink-400);
}

.al-invite__link {
  display: flex;
  align-items: stretch;
  gap: var(--sp-2);
}

/* The link is a machine value: mono and LTR whatever the page's direction. One
   line, truncated — nobody reads this string, they copy it, and wrapping it
   across three lines of monospace spent a third of the rail on it. The whole
   of it stays on the title and on the clipboard. */
.al-invite__url {
  flex: 1;
  min-width: 0;
  display: block;
  padding: var(--sp-2) var(--sp-3);
  background: var(--ink-900);
  border: 1px solid var(--line);
  border-radius: var(--r-sm);
  font-size: var(--fs-2xs);
  line-height: 1.5;
  color: var(--ink-300);
  direction: ltr;
  unicode-bidi: isolate;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.al-invite__copy {
  flex: none;
}

/* Rotating the token is the owner's destructive control, so it is an icon at
   the head of the section rather than a third button under it. */
.al-invite__rotate-form {
  display: flex;
}

.al-invite__rotate {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 28px;
  height: 28px;
  padding: 0;
  border: 0;
  border-radius: var(--r-sm);
  background: transparent;
  color: var(--ink-400);
  cursor: pointer;
  transition: background-color var(--dur-1) var(--ease),
    color var(--dur-1) var(--ease);
}

.al-invite__rotate:hover {
  background: var(--ink-800);
  color: var(--ink-050);
}

/* The copy button ships both labels so the confirmation stays translated; the
   controller only swaps the class. */
.al-invite__copy .copy-text-copied {
  display: none;
}

.al-invite__copy.copied .copy-text {
  display: none;
}

.al-invite__copy.copied .copy-text-copied {
  display: inline;
}

/* ---------------------------------------------------------------------------
   Invite landing card.
   ------------------------------------------------------------------------ */

/* The cover runs to the card's edges, so the panel has to clip its own
   radius — `.panel` rounds but does not hide overflow. */
.al-card {
  overflow: hidden;
}

/* Shallower than the card silhouette's 16:9: this card is the whole page, and
   a band that tall pushes the one button it exists for below the fold. */
.al-card__cover {
  --identicon-size: 96px;
  position: relative;
  aspect-ratio: 3 / 1;
  min-height: 140px;
  overflow: hidden;
  border-block-end: 1px solid var(--line);
  background: var(--ink-900);
}

.al-card__body {
  display: flex;
  flex-direction: column;
  gap: var(--sp-3);
  text-align: center;
  align-items: center;
  padding-block: var(--sp-8);
}

.al-card__eyebrow {
  color: var(--accent);
  margin: 0;
}

.al-card__titleline {
  display: flex;
  align-items: center;
  justify-content: center;
  flex-wrap: wrap;
  gap: var(--sp-3);
  min-width: 0;
}

.al-card__title {
  margin: 0;
  font-size: var(--fs-2xl);
  font-weight: 700;
  line-height: var(--lh-tight);
  letter-spacing: var(--tr-display);
  color: var(--ink-050);
  overflow-wrap: anywhere;
}

.al-card__target {
  display: inline-flex;
  align-items: center;
  gap: var(--sp-2);
  margin: 0;
  font-size: var(--fs-sm);
  color: var(--ink-300);
}

.al-card__target-link {
  color: var(--ink-100);
  text-decoration: none;
  border-block-end: 1px solid var(--line-hi);
  overflow-wrap: anywhere;
}

.al-card__desc {
  color: var(--ink-300);
  max-width: 52ch;
}

.al-card__desc p {
  margin: 0 0 var(--sp-2);
}

.al-card__facts {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: var(--sp-4) var(--sp-8);
  padding-block: var(--sp-3);
}

.al-card__fact {
  display: flex;
  flex-direction: column;
  gap: 2px;
}

.al-card__fact-key {
  color: var(--ink-400);
}

.al-card__fact-value {
  font-size: var(--fs-lg);
  font-weight: 600;
  color: var(--ink-050);
  overflow-wrap: anywhere;
}

.al-card__faces {
  display: flex;
  list-style: none;
  margin: 0;
  padding: 0;
}

/* Overlapped faces: the crew reads as a group rather than a list. */
.al-card__faces li + li {
  margin-inline-start: -8px;
}

.al-card__faces .avatar {
  border: 2px solid var(--ink-950);
}

.al-card__actions {
  width: 100%;
  margin-block-start: var(--sp-3);
}

.al-card__note {
  margin: 0 0 var(--sp-3);
  font-size: var(--fs-sm);
  color: var(--ink-400);
}

/* That the chat is members-only, said under the button rather than on a panel
   of its own further down: it is a term of the decision the button makes, and
   an outsider reads it in the second they are deciding or not at all. */
.al-card__gate {
  display: flex;
  align-items: flex-start;
  justify-content: center;
  gap: var(--sp-2);
  margin: var(--sp-4) 0 0;
  font-size: var(--fs-sm);
  line-height: var(--lh-snug);
  text-align: start;
  color: var(--ink-400);
}

.al-card__gate svg {
  flex: none;
  margin-block-start: 2px;
}

.al-card__gate strong {
  color: var(--ink-200);
  font-weight: 600;
}

/* The outsider's page — the same card, then the manifest under it. */
.al--pitch .al-roster {
  margin-block-start: var(--sp-6);
}

/* Three facts stop fitting on one line somewhere under 640px, and two-then-one
   reads as a row that ran out of room rather than as a decision. Stacked, they
   are three deliberate lines. */
@media (max-width: 639px) {
  .al-card__facts {
    flex-direction: column;
    align-items: center;
    gap: var(--sp-3);
  }
}

/* ---------------------------------------------------------------------------
   On a challenge page — the same board, seen from the one puzzle it is about.

   The board keeps its own markup; only the frame around it changes. What the
   rows lose is the line naming the challenge, which the page they sit on has
   already said twice.
   ------------------------------------------------------------------------ */

.pz-al__head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--sp-4);
  flex-wrap: wrap;
  margin-block-end: var(--sp-4);
}

.pz-al__lede {
  margin: 0;
  max-width: 52ch;
  font-size: var(--fs-sm);
  color: var(--ink-300);
}

.pz-al__create {
  flex: none;
}

/* ---------------------------------------------------------------------------
   Below 900px — the rail stops being a column and goes under the transcript.

   It used to go *above* it, on the reasoning that who is in a room is what you
   check on a phone. That is true of the alliance you are considering and false
   of the one you are in: a member arriving at their own room on a phone had to
   scroll past a cover sheet and eleven roster rows — around a thousand pixels —
   to reach the last thing anybody said. The outsider's page is now a separate
   surface and keeps that reading; here the conversation comes first.
   ------------------------------------------------------------------------ */

@media (max-width: 899px) {
  .al-room {
    grid-template-columns: minmax(0, 1fr);
    grid-template-rows: auto minmax(0, 1fr) auto;
    /* Auto height, because the rail is now stacked underneath and the frame is
       no longer the whole screen — but the transcript above it still gets a
       screenful, so the composer lands where the thumb already is. */
    height: auto;
    max-height: none;
  }

  .al-room__main {
    grid-column: 1;
    grid-row: 2;
    height: calc(100vh - var(--h-header) - 180px);
    height: calc(100dvh - var(--h-header) - 180px);
    min-height: 420px;
    border-inline-end: 0;
  }

  .al-room__rail {
    grid-column: 1;
    grid-row: 3;
    border-block-start: 1px solid var(--line);
  }

  /* Nothing to scroll against once it is stacked: the rail runs to its full
     height and the page carries it. */
  .al-room__rail-scroll {
    overflow-y: visible;
    min-height: 0;
    flex: none;
  }
}

@media (max-width: 767px) {
  .al {
    padding-block: var(--sp-6) var(--sp-12);
  }

  .al__title {
    font-size: var(--fs-2xl);
  }

  /* The row's action drops under its body rather than squeezing the name into
     a two-word column. */
  .al-row {
    flex-direction: column;
    align-items: stretch;
  }

  .al-row__action {
    display: flex;
  }

  .al-row__action .btn,
  .al-row__action form {
    width: 100%;
  }

  .al--room {
    padding-block: var(--sp-4) var(--sp-12);
  }

  /* The bar loses the crest and the label on the challenge line — at 360px the
     room's name, its standing and what it is about are what has to fit, and
     the picture is the one of the four that says nothing the title does not. */
  .al-room__crest,
  .al-room__target-key {
    display: none;
  }

  .al-room__bar {
    padding: var(--sp-3);
    gap: var(--sp-2);
  }

  .al-room__title {
    font-size: var(--fs-md);
  }

  /* The way out stays in the bar and stays labelled — it is the only one on the
     surface, and an icon is not worth the ambiguity on the control that ends a
     membership. The identity beside it truncates instead. */
  .al-room__actions .btn {
    white-space: nowrap;
  }

  .al-room__main {
    height: calc(100vh - var(--h-header) - 148px);
    height: calc(100dvh - var(--h-header) - 148px);
  }

  /* The room's composer carries an attach control the thread's does not, so at
     this width it is the one that has to give up the word beside its arrow. */
  .al-composer .chat-composer__send span {
    display: none;
  }

  .al-card__facts {
    gap: var(--sp-4) var(--sp-6);
  }
}
