
.appIcon[data-v-39a24ba9] {
  flex-shrink: 0;
  width: 16px;
  height: 16px;
  fill: currentColor;
}

.consoleAccount[data-v-5b3f1422] {
  display: flex;
  flex-direction: column;
  gap: 0.4rem;
}

/* The account's own actions: a block in the flow, not a bar pinned to the
   window. Two pinned bars in one pane would cover each other. */
.accountActions[data-v-5b3f1422] {
  display: flex;
  flex-direction: column;
  gap: 0.4rem;
  margin-top: 0.5rem;
  padding-top: 0.5rem;
  border-top: 1px solid var(--mainColor-darker);
}
.accountAvatar[data-v-5b3f1422] {
  width: 1.6rem;
  height: 1.6rem;
}

/* The devices: the machines the account signs in from. The id is a hash, so
   the operator reads the head of it and the moment it answered last. */
.accountDevices[data-v-5b3f1422] {
  list-style: none;
  margin: 0.3rem 0 0;
  padding: 0;
}
.accountDeviceRow[data-v-5b3f1422] {
  display: flex;
  flex-flow: row nowrap;
  align-items: center;
  gap: 0.6rem;
  padding: 0.25rem 0;
}
.accountDeviceName[data-v-5b3f1422] {
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  font-size: var(--text-4);
  color: var(--mainColor-lighter);
}
.accountDeviceSeen[data-v-5b3f1422] {
  flex: 1 1 auto;
  font-size: var(--text-3);
  color: var(--mainColor-lighter);
}

.opsGrid[data-v-852cce2b] {
  display: flex;
  flex-wrap: wrap;
  gap: 0.75rem;
}
.opsTile[data-v-852cce2b] {
  display: flex;
  flex-direction: column;
  gap: 0.15rem;
  min-width: 7rem;
  padding: 0.5rem 0.65rem;
  background-color: var(--mainColor-darker-trans-lo);
  border: 1px solid var(--mainColor-darker);
  border-radius: var(--radius-2);
}
.opsLabel[data-v-852cce2b] {
  color: var(--mainColor-lighter);
  font-size: var(--text-3);
  text-transform: uppercase;
  letter-spacing: 0.04em;
}
.opsValue[data-v-852cce2b] {
  font-size: var(--text-7);
  font-weight: 600;
}
.opsChannelRows[data-v-852cce2b] {
  display: flex;
  flex-direction: column;
  gap: 0.2rem;
}
.opsChannelRow[data-v-852cce2b] {
  display: flex;
  align-items: baseline;
  gap: 0.5rem;
  font-size: var(--text-4);
}
.opsChannelName[data-v-852cce2b] {
  color: var(--mainColor-light);
}
.opsChannelMembers[data-v-852cce2b] {
  color: var(--mainColor-lighter);
  font-size: var(--text-4);
}

.consoleBody[data-v-69c350b2] {
  display: flex;
  flex: 1;
  min-height: 0;
}

/* The console's rail is a column of its own, and it says so: the same
   surface the app gives its columns (the roster's column wears it), with the
   rule that separates it from the pane beside it. A rail that shares the
   pane's background is not a rail, it is a column of words. */
.consoleRail[data-v-69c350b2] {
  display: flex;
  flex-direction: column;
  gap: 0.15rem;
  flex: 0 0 12rem;
  padding: 0.75rem 0.5rem;
  background-image: linear-gradient(135deg, var(--mainColor), var(--mainColor-dark));
  border-right: 1px solid var(--mainColor-light);
  box-shadow: 4px 0 6px 0 var(--mainColor-dark-trans-lo);
  overflow-y: auto;
}

/* The rail is a nav, so it says where it is: the open section is marked, and
   the count beside it says how much waits there. */
.consoleTab[data-v-69c350b2] {
  display: flex;
  align-items: center;
  gap: 6px;
  padding: 6px 0.75rem;
  font: inherit;
  font-size: var(--text-6);
  text-align: left;
  color: var(--mainColor-lighter);
  background: none;
  border: none;
  border-left: 2px solid transparent;
  border-radius: 0 var(--radius-2) var(--radius-2) 0;
  cursor: pointer;
  transition:
    color 0.15s ease-out,
    background-color 0.15s ease-out;
}
.consoleTab[data-v-69c350b2]:hover {
  color: var(--white);
  background-color: var(--mainColor-light);
}
.consoleTab.active[data-v-69c350b2] {
  color: var(--white);
  background-color: var(--mainColor-dark);
  border-left-color: var(--accentColor);
  font-weight: 600;
}
.consoleTabLabel[data-v-69c350b2] {
  flex: 1 1 auto;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.consoleSection[data-v-69c350b2] {
  display: flex;
  flex-direction: column;
  gap: 0.6rem;
  flex: 1;
  min-width: 0;
  padding: 0.75rem 1rem 1rem;
  background-color: var(--mainColor-darker);
  overflow-y: auto;
}
.consoleClose[data-v-69c350b2] {
  background: none;
  border: none;
  color: var(--mainColor-lighter);
  font-size: var(--text-8);
  line-height: var(--leading-none);
  cursor: pointer;
}
.consoleClose[data-v-69c350b2]:hover {
  color: var(--mainColor);
}

.inputBody[data-v-d3aa4b31] {
  display: flex;
  flex-direction: column;
  gap: 0.75rem;
  padding: 1rem 1.25rem;
  overflow-y: auto;
  flex: 1;
  min-height: 0;
}

/* The head line says what the screen is for, so it is said plainly: the
   room's own descriptor is a quiet second voice beside a name that already
   says where you are, and here there is no name to lean on. */
.inputPurpose[data-v-d3aa4b31] {
  padding: 0 0.5rem 0 1rem;
  color: var(--mainColor-lightest);
  font-size: var(--text-5);
  font-weight: 400;
  user-select: none;
}

/* The list and the device card, side by side: the app gives its columns a
   shape already (the room's chat and roster), and the input screen is the
   same shape - what the device is on one side, what it is bound to on the
   other. */
.inputColumns[data-v-d3aa4b31] {
  display: flex;
  gap: 1.5rem;
  align-items: flex-start;
  flex: 1;
  min-height: 0;
}
.inputList[data-v-d3aa4b31] {
  display: flex;
  flex-direction: column;
  gap: 0.75rem;
  min-width: 0;
}

/* The head: what the screen is for, said plainly. The console's own head
   classes are tuned for a page of cases; here the line is the only thing
   that says what the rows below are. */
.inputHead[data-v-d3aa4b31] {
  display: flex;
  align-items: baseline;
  gap: 0.6rem;
  flex-wrap: wrap;
}
.inputHeadTitle[data-v-d3aa4b31] {
  font-size: var(--text-6);
  font-weight: 700;
  color: var(--mainColor-lightest);
}
.inputHeadPurpose[data-v-d3aa4b31] {
  font-size: var(--text-5);
  color: var(--mainColor-lighter);
}

/* The navigation is the sources, and the list below is the selected one's
   slots: one column, one answer per row. A row that repeats the keyboard's
   binding next to the pad's says the same thing twice. */
.inputRows[data-v-d3aa4b31] {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: 0.15rem;
  max-width: 34rem;
}

/* Three columns and room for a conflict line under them: the slot's name,
   the control it holds, the Change the shell allows, and the swap question
   that a refused capture lays under both. */
.inputRow[data-v-d3aa4b31] {
  display: grid;
  grid-template-columns: auto minmax(0, 1fr) auto;
  grid-template-rows: auto auto;
  align-items: baseline;
  gap: 0.2rem 1rem;
  padding: 0.35rem 0.6rem;
}
.inputRow.is-waiting[data-v-d3aa4b31] {
  background: var(--mainColor-darker-trans-lo);
}
.inputRow.is-conflict[data-v-d3aa4b31] {
  background: var(--mainColor-darker-trans-lo);
}
.inputSlotName[data-v-d3aa4b31] {
  font-size: var(--text-5);
  color: var(--mainColor-lighter);
}

/* The service keys are not the player's buttons: one rule above the group
   says so, and their names are as readable as the rest - a dimmer name is
   an invisible one on this background. */
.inputRow.is-groupStart[data-v-d3aa4b31] {
  margin-top: 0.4rem;
  border-top: 1px solid var(--mainColor-lightest-trans-md);
}
.inputControl[data-v-d3aa4b31] {
  font-size: var(--text-6);
  font-weight: 600;
  color: var(--mainColor-lightest);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.inputControl.is-empty[data-v-d3aa4b31] {
  font-weight: 400;
  color: var(--mainColor-lighter);
}

/* The wait is spoken by the row it belongs to, in the settled weight of the
   others' filler text; the hint is the quieter half of the sentence. */
.inputControl.is-waiting[data-v-d3aa4b31] {
  font-weight: 400;
  color: var(--mainColor-lighter);
}
.inputWaitHint[data-v-d3aa4b31] {
  color: var(--mainColor-lightest);
}

/* The quiet family the sources' navigation wears (.segmented): transparent
   ground, a border like the app's buttons, the darker ground on hover. The
   Change on a row is the scale's sm step — a row's size, not the panel's, and
   the step says that in one number instead of a padding measured per callsite. */
.inputChange[data-v-d3aa4b31],
.inputConflictButton[data-v-d3aa4b31] {
  font: inherit;
  font-size: var(--control-font-sm);
  line-height: var(--leading-tight);
  min-height: var(--control-h-sm);
  color: var(--mainColor-lightest);
  background: transparent;
  border: 1px solid var(--mainColor-light);
  border-radius: var(--radius-2);
  padding: 0 var(--control-pad-x-sm);
  cursor: pointer;
}
.inputChange[data-v-d3aa4b31] {
  align-self: center;
}
.inputChange[data-v-d3aa4b31]:hover:not(:disabled) {
  background: var(--mainColor-darker);
  border-color: var(--mainColor-lighter);
}
.inputChange[data-v-d3aa4b31]:disabled {
  opacity: 0.45;
  cursor: default;
}

/* The conflict line: the question and its two answers, one strip under the
   row that raised them. */
.inputConflict[data-v-d3aa4b31] {
  grid-column: 1 / -1;
  display: flex;
  align-items: baseline;
  gap: 0.75rem;
  flex-wrap: wrap;
  padding: 0.35rem 0.4rem 0.45rem;
  border-top: 1px solid var(--mainColor-lightest-trans-md);
}
.inputConflictText[data-v-d3aa4b31] {
  font-size: var(--text-5);
  color: var(--accentColor);
}
.inputConflictActions[data-v-d3aa4b31] {
  display: flex;
  gap: 0.5rem;
}

/* The button declares nothing of its own: the family rule above owns its shape.
   It used to repeat every line of that rule here, which is how one scale ends up
   with five treatments of itself (\#121 U8). */
.inputConflictButton[data-v-d3aa4b31]:hover {
  background: var(--mainColor-darker);
  border-color: var(--mainColor-lighter);
}
.inputProfile[data-v-d3aa4b31] {
  margin: 0;
  font-size: var(--text-3);
  color: var(--mainColor-lighter);
}

/* The footer's two truths, apart from the profile's path: a save the shell
   answered, and the one line the row or the save wants said out loud. */
.inputSaved[data-v-d3aa4b31] {
  color: var(--mainColor-lightest);
}
.inputFault[data-v-d3aa4b31] {
  color: var(--errorColor);
}
.inputNotice[data-v-d3aa4b31] {
  margin: 0;
  font-size: var(--text-5);
  color: var(--mainColor-lighter);
}
.inputNotice.is-reason[data-v-d3aa4b31] {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 0.6rem;
  color: var(--mainColor-lightest);
}

/* The device card: what the player is holding, next to what it is bound to. */
.inputDevice[data-v-d3aa4b31] {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 0.35rem;
  flex: 0 0 15rem;
  padding: 1rem 0.75rem 0.9rem;
  background: var(--mainColor-dark);
  border: 1px solid var(--mainColor-light);
  border-radius: var(--radius-3);
}
.inputDeviceName[data-v-d3aa4b31] {
  font-size: var(--text-6);
  font-weight: 600;
  color: var(--mainColor-lightest);
  text-align: center;
}
.inputDeviceKind[data-v-d3aa4b31] {
  font-size: var(--text-3);
  color: var(--mainColor-lighter);
}
/* left4rcade theme. Class taxonomy mirrors the original client panes
   (mainToolbar, channelsList, usersList, matchesList, chatWrapper) so anyone
   who knows the Fightcade DOM feels at home; the look is our own —
   neutral dark gray with a silver accent, red reserved for alerts.
   Flat only: no grain, no tile, nothing that reads as dirt at panel scale.
   The constants and faces are shared (tokens.css), so the rankings page
   dresses in the same theme without importing this whole sheet. */

/* The theme's constants, faces and shared primitives — the one source every
   entry that dresses itself in this theme reads (the app shell through
   main.css, the rankings page directly). Layout geometry lives here too, so a
   rail resize cannot strand a panel: every rule downstream reads these
   variables. */

:root {
  --white: #fff;
  /* The glossy button edge, one shade under --white. */
  --white-dim: #f2f2f2;
  --black-trans-md: rgba(0, 0, 0, 0.5);
  --mainColor: #202327;
  --mainColor-dark: #16181c;
  --mainColor-darker: #0c0d0f;
  --mainColor-light: #33373d;
  --mainColor-lighter: #8b949d;
  --mainColor-lightest: #e8eaed;
  --mainColor-lightest-trans-hi: rgba(232, 234, 237, 0.7);
  --mainColor-lightest-trans-md: rgba(232, 234, 237, 0.5);
  /* The hover edge the channel toolbar's outlined buttons (Replays,
   * Rankings, Test game, My card) and the game card's actions brighten to.
   * It was used at three sites and defined nowhere; 0.4 is the lowest alpha
   * that clears WCAG 1.4.11's 3:1 on the plates it lands on — 3.29:1 on
   * --mainColor, 3.38:1 on --mainColor-dark (tools/contrast_check.py). */
  --mainColor-lightest-trans-lo: rgba(232, 234, 237, 0.4);
  --mainColor-dark-trans-lo: rgba(22, 24, 28, 0.3);
  --mainColor-darker-trans-hi: rgba(12, 13, 15, 0.7);
  --mainColor-darker-trans-lo: rgba(12, 13, 15, 0.3);
  /* The theme's metal: one value the accent and the online state both wear,
   * written once. The two names stay separate because they answer different
   * questions — `--accentColor` is the theme's highlight ink, `--state-online`
   * is a status — and a status that borrows the highlight's literal would be
   * edited by whoever edits the highlight. \#121 U8 found the pair spelled
   * twice and folded the value, not the names. */
  --silver: #c9d1d9;
  --accentColor: var(--silver);
  /* The mention/turbo warm: one value that is both an ink over the chat's
   * gradient and a plate under a badge's digit, so it has to clear the bar from
   * either side.  The fork's #ef5a36 was 3.53:1 as an ink at the gradient's
   * lightest stop (--mainColor-light) and 2.82:1 as a plate under the inherited
   * --mainColor-lightest digit; this clears both (4.74 as that ink, 7.70 under
   * --mainColor-darker), and it separates from --errorColor, which it nearly
   * duplicated before (#121 U8). */
  --accentColor2: #ff7d55;
  /* The accent's soft plate — the turbo badge's fill, which spelled the accent's
   * own rgb() a second time and stayed behind when this round moved the accent.
   * That is the argument for naming it (#121 U8). */
  --accentColor2-trans-lo: rgba(255, 125, 85, 0.14);
  --errorColor: #e94c3a;
  /* The fork's SOCD plate: the dark red its input display draws the
     clear-opposites badge on, with the word in white. A fact of the fork, not
     a choice of the theme - the lobby badge wears the plate the emulator
     wears. */
  --badgeSocdPlate: #781e14;
  --successColor: #8ce62f;
  --state-online: var(--silver);
  --state-away: #e8b73a;
  --text-dim: #9aa0a6;
  /* Nickname tiers (global roles): admin red, operator green, vip yellow.
   * All clear 4.5:1 on --mainColor-dark (#16181c): 6.40 / 8.82 / 10.41. */
  --roleAdmin: #ff6b6b;
  --roleOperator: #7bc96f;
  --roleVip: #e8c339;
  /* ---- the type ladder ------------------------------------------------
   * Nine steps, one spelling each. The tree drew these in 29 distinct sizes
   * before they existed (tools/ui_scale_census.py), the same step written in
   * `rem` in one rule and `px` in the next, and 0.78/0.8/0.82/0.85/0.88/0.9
   * are six spellings of two sizes a reader cannot tell apart.
   *
   * The steps are in `rem`. The theme sets no root size anywhere — `html`
   * carries height and margin and nothing else — so one `rem` is the browser's
   * default 16, which is the number every comment here is measured against.
   * The ladder stays relative on purpose: a player who raises their minimum
   * text size scales all nine steps together, and pinning the root would
   * spend that to make the comments exact. */
  --text-1: 0.625rem; /* 10px  a circle's own word, a chip's letter */
  --text-2: 0.6875rem; /* 11px  a counter, a timestamp, a ribbon */
  --text-3: 0.75rem; /* 12px  dense meta: a row's second line */
  --text-4: 0.8125rem; /* 13px  secondary prose, a rail label */
  --text-5: 0.875rem; /* 14px  body — the size prose is written at */
  --text-6: 1rem; /* 16px  a row's subject, an input's own text */
  --text-7: 1.25rem; /* 20px  a panel or dialog heading */
  --text-8: 1.5rem; /* 24px  a page heading, a glyph that is the control */
  --text-display: 2.5rem; /* 40px  the landing title */
  /* The roles prose and controls reach for, so a callsite names its job
   * rather than counting pixels. */
  --text-body: var(--text-5);
  --text-control: var(--text-6);
  /* ---- leading --------------------------------------------------------
   * Three steps. A line-height is the other half of every fixed-height box's
   * arithmetic: a box that names its own height owes its text at least
   * `font-size * leading`, and the two clipped boxes this round found were
   * both a height that had not been told what its line measured.
   * `--leading-body` is what every rule that names no line-height already
   * inherits from `body`. `line-height: 0` is not a step and is not folded:
   * fourteen rules use it to suppress a box's own line, which is a layout
   * device, not a size. */
  --leading-none: 1; /* a glyph, a badge, a counter in a plate */
  --leading-tight: 1.1; /* a control's label, a rail column's one-line rows */
  --leading-body: 1.45; /* prose */
  /* ---- the radius ladder ----------------------------------------------
   * Five steps, a pill and a circle. The tree spelled 16 of them (the same
   * census), and three of those were corner shorthands that named a step for
   * two corners and 0 for the other two. */
  --radius-1: 2px; /* a badge plate, a flag */
  --radius-2: 4px; /* the control corner: every button, every input */
  --radius-3: 6px; /* a chip, a tile, a row */
  --radius-4: 8px; /* a popover, a panel's inner card */
  --radius-5: 10px; /* the big panel */
  --radius-pill: 999px;
  --radius-circle: 50%; /* `100%` is the same shape on every box, spelled twice */
  /* ---- the control scale ----------------------------------------------
   * One scale, three steps, two faces. The face says how loud the act is —
   * `gloss` (.button-generic: the one act a screen wants the eye on) and
   * `flat` (.appButton and every bordered control beside an input) — and the
   * step says how big. A variant (is-primary, is-danger, cardClose, a
   * secondary label) changes ink and never geometry, and a callsite that
   * wants a bigger control names a step instead of writing a padding.
   *
   * The heights are min-heights: a wrapped label grows the box, and 44 is the
   * smallest target this theme will hand to a pointer for the act that
   * matters. Each step carries its own font size, so the four things the
   * tree used to set per-callsite (height, pad, font, radius) move together. */
  --control-h-sm: 26px; /* a dense row: the rail's retry, a match's handoff.
                         * WCAG 2.5.8's 24px floor plus the 1px border each
                         * side, so the drawn box is not the target's size. */
  --control-h-md: 32px; /* the default: a control level with a 32px input */
  --control-h-lg: 44px; /* the slab, and WCAG 2.5.5's enlarged-target number */
  --control-font-sm: var(--text-3);
  --control-font-md: var(--text-5);
  --control-font-lg: var(--text-6);
  --control-pad-x-sm: 0.5rem;
  --control-pad-x-md: 0.875rem;
  --control-pad-x-lg: 1rem;
  --control-gap: 0.35rem; /* an icon inside a control */
  /* Layout geometry (single source — the panels that hang on the rail read
   * these, so a rail resize cannot strand one anymore). */
  --rail-w: 120px;
  /* The rail's own account circle.  The state popover is centred in the rail
   * and so is offset from this circle, not from a number measured off it:
   * \#121 U8 found `left: -15px` and a bare `width: 160px` two rules apart,
   * which is a layout that moves only when both are remembered. */
  --rail-avatar: 50px;
  /* A popover opens *from* the rail; it is not sized by it. 160 is the width
   * the state menu's own labels need, and the menu lands over the channel
   * column, which is wider than the rail by design. */
  --popover-w: 160px;
  /* The chat column's own floor. It is a framed flex row with a fixed roster,
   * so a chat that shrinks without a stated minimum lets a long word in one
   * message decide the whole shell's width: measured at \#122 U9, the column's
   * own content asked for 334px and pushed the roster 30px past the window's
   * edge at a 784px page. 20rem holds the composer's inset and a text line
   * and is the number the window floor (shared/window.ts) is held against. */
  --chat-w-min: 20rem;
  --chatbar-h: 58px;
  /* Clearance above the chat bar for the log and popups. Coupled to
   * --chatbar-h by constraint, not equality: must stay >= it. */
  --chatbar-clear: 75px;
}

/* The one hidden-label rule: table captions and aria references across the
   app and the public pages share it, so it lives in the sheet every entry
   reads (F-174). */
.visuallyHidden {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip: rect(0 0 0 0);
  white-space: nowrap;
  border: 0;
}

/* Adobe Source Sans Pro (open license), the theme's face. */
@font-face {
  font-family: 'Source Sans Pro';
  font-style: normal;
  font-weight: 400;
  src: url('./source-sans-pro-regular-DlNrLNKj.woff2') format('woff2');
}

@font-face {
  font-family: 'Source Sans Pro';
  font-style: italic;
  font-weight: 400;
  src: url('./source-sans-pro-italic-DvQ_kjcu.woff2') format('woff2');
}

@font-face {
  font-family: 'Source Sans Pro';
  font-style: normal;
  font-weight: 600;
  src: url('./source-sans-pro-600-CVFS1ax3.woff2') format('woff2');
}

@font-face {
  font-family: 'Source Sans Pro';
  font-style: normal;
  font-weight: 700;
  src: url('./source-sans-pro-700-4xXxtyX1.woff2') format('woff2');
}

@font-face {
  font-family: 'Source Sans Pro';
  font-style: italic;
  font-weight: 700;
  src: url('./source-sans-pro-700italic-CoIzS47E.woff2') format('woff2');
}

/* Nickname tier colors (see UserRole in api/types.ts). Plain users keep
 * the inherited text color. */
.role-admin {
  color: var(--roleAdmin);
}

.role-operator {
  color: var(--roleOperator);
}

.role-vip {
  color: var(--roleVip);
}

* {
  box-sizing: border-box;
}

html,
body,
#app,
#shell {
  height: 100%;
  margin: 0;
}

body {
  background: var(--mainColor-darker);
  color: var(--mainColor-lightest);
  font-family: 'Source Sans Pro', 'Segoe UI', system-ui, sans-serif;
  /* The two properties the ladder owns, never a shorthand: a rule that names
   * no line-height inherits this one, which is why the census measures a
   * 23.2px line for a 16px label and why the clip test can do its arithmetic. */
  font-size: var(--text-body);
  line-height: var(--leading-body);
  overflow: hidden;
}

button {
  font: inherit;
  color: inherit;
  background: none;
  border: none;
  padding: 0;
  cursor: pointer;
}

input {
  font: inherit;
  color: var(--mainColor-lightest);
  background: var(--mainColor-dark);
  border: 1px solid var(--mainColor-light);
  border-radius: var(--radius-3);
  /* The md step, so the flat control that sits beside an input lines up with it
   * by the scale rather than by two numbers that happened to agree. */
  min-height: var(--control-h-md);
  padding: 0.25rem var(--control-pad-x-md);
  outline: none;
}

input:focus {
  border-color: var(--accentColor);
}

/* Visible keyboard focus everywhere (WCAG 2.4.7). Mouse focus keeps the
   border/color cues above; keyboard focus gets an unmissable ring on every
   control, including the menu rows and tabs that are divs and buttons
   without their own focus style. */
:focus-visible {
  outline: 2px solid var(--accentColor);
  outline-offset: 2px;
}

/* ---- boot / login ----
   Layout, tokens and controls mirror the original client's login view
   (reference/fightcade/css): split row, plum gradients, glossy arcade button.
   Only the logo artwork is ours. */

.boot {
  display: grid;
  place-items: center;
  height: 100%;
  color: var(--text-dim);
}

.login {
  display: flex;
  flex-flow: row nowrap;
  width: 100%;
  min-height: 100%;
  font-family: 'Source Sans Pro', arial, sans-serif;
  line-height: var(--leading-tight);
  color: var(--white);
  background-color: var(--mainColor-darker);
}

.login .logoWrapper {
  display: flex;
  align-items: center;
  padding: 1.5rem;
  text-align: center;
  background-image: linear-gradient(135deg, var(--mainColor-darker), var(--mainColor-dark) 20%, var(--mainColor));
  box-shadow: inset -12px 0 30px var(--mainColor-darker-trans-lo);
}

@media only screen and (min-width: 768px) {
  .login .logoWrapper {
    padding: 2rem;
  }
}

@media only screen and (min-width: 1024px) {
  .login .logoWrapper {
    padding: 0 6rem;
  }

  .login .logoWrapper .logo {
    max-width: 500px;
  }
}

@media only screen and (min-width: 1280px) {
  .login .logoWrapper .logo {
    max-width: 540px;
  }
}

@media only screen and (min-width: 1920px) {
  .login .logoWrapper {
    padding: 10rem;
  }

  .login .logoWrapper .logo {
    max-width: 580px;
  }
}

@media only screen and (min-width: 2560px) {
  .login .logoWrapper .logo {
    max-width: 700px;
  }
}

.login .logoWrapper .logo {
  width: 100%;
  max-width: 320px;
  height: auto;
}

.login .formWrapper {
  position: relative;
  flex-grow: 1;
  display: flex;
  flex-flow: column nowrap;
  justify-content: center;
  align-items: center;
  width: 100%;
  padding: 1rem;
  text-align: center;
  background-image: linear-gradient(135deg, var(--mainColor-light), var(--mainColor), var(--mainColor-dark));
}

.login .formWrapper .form {
  margin-bottom: 4rem;
  padding: 2rem 4rem;
  border-radius: var(--radius-2);
  background-color: var(--mainColor-dark);
  box-shadow: inset 0 0 40px var(--mainColor-darker-trans-hi);
}

@media only screen and (min-width: 1280px) {
  .login .formWrapper .form {
    padding: 2rem 5rem;
  }
}

.login .formWrapper .form .title {
  margin-bottom: 0.5rem;
  font-size: var(--text-4);
  font-weight: 700;
  text-transform: uppercase;
}

.login .formWrapper .form .title span {
  display: block;
  color: var(--mainColor-lighter);
  user-select: none;
}

.login .formWrapper .form .title span:last-child {
  color: var(--errorColor);
  font-weight: 400;
  font-style: italic;
  text-transform: none;
}

.login .formWrapper .form .title.error,
.login .formWrapper .form .title.error p {
  margin: 0;
  color: var(--errorColor);
}

.login .formWrapper .form .input-area {
  margin-bottom: 2rem;
}

.login .formWrapper .form .input-area .error ~ input,
.login .formWrapper .form .input-area .error ~ input:focus {
  border-color: var(--errorColor);
}

.login .formWrapper .form .input-area input {
  width: 100%;
  padding: 0.5rem;
  color: var(--white);
  background-color: transparent;
  border: 0;
  border-bottom: 1px solid var(--mainColor-lighter);
  border-radius: 0;
  font-size: var(--text-6);
  text-align: center;
  outline: none;
}

.login .formWrapper .form .input-area input:hover {
  border-color: var(--white);
}

.login .formWrapper .form .input-area input:focus {
  border-color: var(--accentColor);
}

.login .formWrapper .form .button-generic {
  /* The slab's step and ink are the scale's; the form owns only the block
   * spacing under it. The 1.25rem this rule used to carry made the login's CTA
   * a fifth of a rem louder than every other slab in the app (\#121 U8). */
  margin-bottom: 0;
}

.login .coc {
  /* The gap is two line boxes of this column's own text, written as the
   * ladder's arithmetic (2 x 14 x 1.1 = 30.8px). What it replaces —
   * 30.796875px, the same gap measured and written down to five decimals —
   * went stale the moment anyone moved the type scale (\#121 U8). */
  margin-top: calc(2 * var(--text-5) * var(--leading-tight));
  color: var(--mainColor-lightest);
  font-size: var(--text-5);
}

.login .backendLine {
  margin-top: 0.75rem;
  color: var(--mainColor-lighter);
  font-size: var(--text-3);
}

.login .authTabs {
  display: flex;
  gap: 16px;
  margin-bottom: 1rem;
  font-weight: 700;
  text-transform: uppercase;
  font-size: var(--text-4);
}

.login .authTabs button {
  color: var(--mainColor-lighter);
  cursor: pointer;
  background: none;
  border: 0;
  padding: 0;
  font: inherit;
  text-transform: inherit;
}

.login .authTabs button.active {
  color: var(--mainColor-lightest);
  border-bottom: 2px solid var(--accentColor);
}

.button-generic {
  position: relative;
  display: inline-flex;
  flex-flow: row nowrap;
  justify-content: center;
  align-items: center;
  margin: 0 auto;
  margin-bottom: 1rem;
  min-height: var(--control-h-lg);
  padding: 0 var(--control-pad-x-lg);
  background-image: linear-gradient(var(--white-dim), var(--white) 20%, var(--white) 80%, var(--white-dim));
  /* The slab's label is dark ink on a white plate: it used to be
   * --mainColor-lighter, which measures 3.08:1 on the plate and 2.75:1 on its
   * --white-dim edge — under WCAG 1.4.3 for a label this size, and the reason
   * the login's own CTA was the least legible button in the app
   * (tools/contrast_check.py, debug/ui-scale-2026-10-30/). */
  color: var(--mainColor-dark);
  border-radius: var(--radius-2);
  box-shadow:
    0 1px 2px var(--mainColor-lighter),
    0 2px 1px var(--mainColor-light),
    0 3px 2px var(--mainColor),
    0 4px 2px var(--mainColor-dark),
    0 4px 0 var(--mainColor-dark),
    0 1px 20px var(--mainColor-dark),
    0 0 20px var(--mainColor-darker-trans-lo),
    0 8px 12px var(--mainColor-darker-trans-lo);
  font-size: var(--control-font-lg);
  font-weight: 700;
  text-align: center;
  line-height: var(--leading-tight);
  text-transform: uppercase;
  user-select: none;
}

.button-generic::after {
  content: '';
  position: absolute;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  border-radius: var(--radius-2);
  box-shadow:
    inset 0 0 10px var(--accentColor),
    0 1px 2px var(--mainColor-lighter),
    0 2px 1px var(--mainColor-light),
    0 3px 2px var(--mainColor),
    0 4px 2px var(--mainColor-dark),
    0 4px 0 var(--mainColor-dark),
    0 0 20px var(--mainColor-darker-trans-lo),
    0 8px 12px var(--mainColor-darker-trans-lo),
    0 -2px 40px var(--accentColor),
    0 -1px 12px var(--accentColor),
    0 -1px 4px var(--white),
    0 10px 30px var(--mainColor-lighter);
  transform: translateZ(0);
  transition: opacity 0.1s ease-out;
  opacity: 0;
}

/* The hover is the ring and the scale; the label does not change, because at
   rest it is already the darkest ink the plate can carry. */
.button-generic:hover {
  transform: scale(1.05);
  cursor: pointer;
}

.button-generic:hover::after {
  opacity: 1;
}

.button-generic:active {
  box-shadow:
    0 0 30px var(--mainColor-dark-trans-lo),
    inset 0 0 12px var(--mainColor-darker-trans-hi),
    0 0 0 1px var(--mainColor-dark-trans-lo),
    0 1px 20px var(--mainColor-dark);
  transform: none;
}

.button-generic:active::after {
  opacity: 0;
}

.button-generic.working {
  pointer-events: none;
}

.spinner {
  transform: none;
  position: relative;
  text-indent: -9999999999px;
}

.spinner::before {
  content: '';
  box-sizing: border-box;
  position: absolute;
  top: 50%;
  left: 50%;
  width: 24px;
  height: 24px;
  margin-top: -12px;
  margin-left: -12px;
  border-radius: var(--radius-circle);
  border: 2px solid var(--mainColor);
  border-top-color: var(--accentColor);
  animation: spinner 0.6s linear infinite;
}

.spinner::after {
  opacity: 0;
}

@keyframes spinner {
  to {
    transform: rotate(1turn);
  }
}

/* The one forever-motion in the shell, and the only thing that says a login is
 * on its way: the label is indented off the button while it works. A player who
 * asked for less motion still needs that answer, so the ring goes still instead
 * of away — an accent arc on a static circle reads as waiting. */
@media (prefers-reduced-motion: reduce) {
  .spinner::before {
    animation: none;
  }
}

/* ---- shell layout ----
   Icon rail + channel column. Mirrors the original shell
   (reference/fightcade/notes/shell.md); widths come from --rail-w. */

.appShell {
  display: flex;
  height: 100%;
  position: relative;
}

.mainToolbarWrapper {
  display: flex;
}

.mainToolbarWrapper .mainToolbar {
  display: flex;
  flex-flow: column nowrap;
  justify-content: space-between;
  min-width: var(--rail-w);
  width: var(--rail-w);
  text-align: center;
  background-image: linear-gradient(90deg, var(--mainColor-dark), var(--mainColor) 160px, var(--mainColor) 50%, var(--mainColor-light));
  user-select: none;
  box-shadow: 4px 0 6px 0 var(--mainColor-dark-trans-lo);
  z-index: 30;
}

.mainToolbarWrapper .mainToolbar .logo {
  position: relative;
  line-height: 0;
  margin: 18px 14px 0;
  filter: drop-shadow(0 0 6px var(--mainColor-darker));
  overflow: visible;
}

.mainToolbarWrapper .mainToolbar .logo img {
  width: 100%;
  height: auto;
}

.mainToolbarWrapper .mainToolbar .channelsList {
  position: relative;
  padding: 20px 0 24px;
  /* The channel compartment owns the rail's middle: it grows to rest on the
   * bottom cluster, so the divider hugs the cluster and the rail can never
   * produce a dead hole between two compartments on a channel-light server. */
  flex: 1 1 auto;
  min-height: 0;
  overflow-y: scroll;
  border-bottom: 1px solid var(--mainColor-light);
}

.mainToolbarWrapper .mainToolbar .channelsList::-webkit-scrollbar {
  display: none;
}

.channelItemWrapper {
  position: relative;
  width: 100%;
}

.channelItemWrapper:hover .leaveChannelItem,
.channelItemWrapper:hover .awayChannelItem,
.channelItemWrapper:hover .favChannelItem,
.channelItemWrapper:focus-within .leaveChannelItem,
.channelItemWrapper:focus-within .awayChannelItem,
.channelItemWrapper:focus-within .favChannelItem {
  opacity: 1;
}

/* The mini buttons are real buttons with a driver: AABB keyboard users reach
 * the same three actions through the row menu, so they never take a Tab stop,
 * but the circle's focus still reveals them, and they carry the same reset a
 * button needs to look like they always did. */
.channelItemWrapper .favChannelItem {
  position: absolute;
  top: 6px;
  left: 4px;
  width: 18px;
  height: 18px;
  border: 0;
  padding: 1px;
  line-height: 0;
  background: none;
  opacity: 0;
  transition: opacity 0.1s ease-out;
}

.channelItemWrapper .favChannelItem:hover {
  cursor: pointer;
}

.channelItemWrapper .favChannelItem:hover .icon {
  fill: var(--white);
  filter: drop-shadow(0 0 6px var(--white));
}

.channelItemWrapper .favChannelItem.active {
  opacity: 1;
}

.channelItemWrapper .favChannelItem.active .icon {
  fill: var(--accentColor);
  filter: drop-shadow(0 0 6px var(--accentColor));
}

.channelItemWrapper .favChannelItem .icon {
  width: 14px;
  height: 14px;
  fill: var(--mainColor-lighter);
}

/* Rail hover tooltip (behavior mirrors the original: inactive rows only,
 * anchored at the row, viewport-clamped). */
.channelTooltip {
  position: fixed;
  left: calc(var(--rail-w) + 8px);
  z-index: 40;
  max-width: 260px;
  padding: 10px 12px;
  background-color: var(--mainColor-dark);
  border: 1px solid var(--mainColor-light);
  border-radius: var(--radius-4);
  box-shadow: 0 4px 16px var(--black-trans-md);
  pointer-events: none;
}

.channelTooltipName {
  font-weight: 700;
  margin-bottom: 4px;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.channelTooltipMeta {
  color: var(--mainColor-lighter);
  font-size: var(--text-3);
  line-height: var(--leading-body);
}

.channelItemWrapper .awayChannelItem {
  position: absolute;
  bottom: 3px;
  right: 5px;
  width: 18px;
  height: 18px;
  border: 0;
  padding: 1px;
  line-height: 0;
  background: none;
  opacity: 0;
  transition: opacity 0.1s ease-out;
}

.channelItemWrapper .awayChannelItem:hover {
  cursor: pointer;
}

.channelItemWrapper .awayChannelItem:hover .icon {
  fill: var(--white);
  filter: drop-shadow(0 0 6px var(--white));
}

.channelItemWrapper .awayChannelItem:hover .icon .icon-strike {
  fill: var(--white);
}

.channelItemWrapper .awayChannelItem.active {
  opacity: 1;
}

.channelItemWrapper .awayChannelItem.active .icon {
  fill: var(--white);
  filter: drop-shadow(0 0 6px var(--accentColor));
}

.channelItemWrapper .awayChannelItem.active .icon .icon-strike {
  fill: var(--accentColor);
}

.channelItemWrapper .awayChannelItem .icon {
  width: 14px;
  height: 14px;
  fill: var(--mainColor-lighter);
}

.channelItemWrapper .leaveChannelItem {
  position: absolute;
  top: 6px;
  right: 4px;
  width: 18px;
  height: 18px;
  border: 0;
  padding: 1px;
  line-height: 0;
  background: none;
  opacity: 0;
  transition: opacity 0.1s ease-out;
}

.channelItemWrapper .leaveChannelItem:hover {
  cursor: pointer;
}

.channelItemWrapper .leaveChannelItem:hover .icon {
  fill: var(--errorColor);
  filter: drop-shadow(0 0 6px var(--errorColor));
}

.channelItemWrapper .leaveChannelItem .icon {
  width: 14px;
  height: 14px;
  fill: var(--mainColor-lighter);
}

.channelItemWrapper .channelItem {
  display: block;
  position: relative;
  width: 80px;
  height: 80px;
  margin: 0 20px;
  margin-bottom: 1rem;
  border: 3px solid var(--mainColor-light);
  border-radius: var(--radius-circle);
  background-color: var(--mainColor-dark);
}

/* The circle is the game, not a label: the browser card's per-game art fills
 * it the same way (crisp-edges, dimmed while inactive, full colour when the
 * channel is on). The name renders through the art component's own fallback
 * slot, so a channel without art keeps the original's text circle. */
.channelItemWrapper .channelItem img.channelArt {
  position: absolute;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  border-radius: var(--radius-circle);
  object-fit: cover;
  image-rendering: crisp-edges;
  filter: grayscale(35%) brightness(0.72);
  transition: filter 0.15s ease-out;
}

.channelItemWrapper .channelItem:hover img.channelArt {
  filter: grayscale(15%) brightness(0.9);
}

.channelItemWrapper .channelItem.active img.channelArt {
  filter: none;
}

.channelItemWrapper .channelItem::after {
  content: '';
  position: absolute;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  border-radius: var(--radius-circle);
  box-shadow: inset 0 0 12px var(--mainColor-lighter), 0 0 20px var(--mainColor-lighter);
  opacity: 0;
  transition: opacity 0.1s ease-out;
}

.channelItemWrapper .channelItem:hover {
  cursor: pointer;
  border-color: var(--mainColor-lighter);
}

.channelItemWrapper .channelItem:hover .channelItemName {
  color: var(--white);
}

.channelItemWrapper .channelItem:hover::after {
  opacity: 1;
}

.channelItemWrapper .channelItem.active {
  border-color: var(--accentColor);
}

.channelItemWrapper .channelItem.active::after {
  box-shadow: inset 0 0 12px var(--accentColor), 0 0 30px var(--accentColor);
  opacity: 1;
}

.channelItemWrapper .channelItem.active .channelItemName {
  color: var(--white);
  opacity: 1;
}

.channelItemWrapper .channelItem .channelItemName {
  top: 0;
  left: 0;
  padding: 8px;
  border-radius: var(--radius-circle);
  font-size: var(--text-1);
  color: var(--mainColor-lighter);
  line-height: var(--leading-tight);
  text-transform: uppercase;
  font-weight: 700;
  overflow: hidden;
  backface-visibility: hidden;
  transition: opacity 0.1s ease-out, color 0.2s ease-out;
}

.channelItemWrapper .channelItem .channelItemName,
.channelItemWrapper .channelItem .notificationsIndicatorsWrapper {
  position: absolute;
  display: flex;
  justify-content: center;
  align-items: center;
  width: 100%;
  height: 100%;
}

.channelItemWrapper .channelItem .notificationsIndicatorsWrapper {
  flex-flow: column nowrap;
}

.channelItemWrapper .channelItem .notificationsIndicatorsWrapper .notificationsIndicators {
  position: absolute;
  left: -15px;
}

.channelItemWrapper .channelItem .notificationsIndicatorsWrapper .notificationsIndicators .newMessages,
.channelItemWrapper .channelItem .notificationsIndicatorsWrapper .notificationsIndicators .newMentions,
.channelItemWrapper .channelItem .notificationsIndicatorsWrapper .notificationsIndicators .newChallenges {
  display: block;
  width: 6px;
  height: 6px;
  border-radius: var(--radius-circle);
  margin-bottom: 4px;
}

.channelItemWrapper .channelItem .notificationsIndicatorsWrapper .notificationsIndicators .newMessages {
  background-color: var(--white);
  box-shadow: 0 0 12px var(--white);
}

.channelItemWrapper .channelItem .notificationsIndicatorsWrapper .notificationsIndicators .newMentions {
  background-color: var(--accentColor2);
  box-shadow: 0 0 12px var(--accentColor2);
}

.channelItemWrapper .channelItem .notificationsIndicatorsWrapper .notificationsIndicators .newChallenges {
  background-color: var(--accentColor);
  box-shadow: 0 0 12px var(--accentColor);
}

/* The rail's "no set" chip (#136): this machine's engine cannot boot the tile's
   set. The chair stays a chair — a channel is entered for its chat whatever the
   library holds — so the fact is a chip on the tile and not a removed row. It
   sits centered at the circle's lower edge: the unread dots own the left, the
   hover buttons the wrapper's corners, and the tile's own name the middle.
   `aria-hidden` because the row's spoken label and the hover line below carry
   the same fact in words. */
.channelItemWrapper .channelItem .channelNoSet {
  position: absolute;
  bottom: 6px;
  left: 50%;
  transform: translateX(-50%);
  padding: 1px 5px;
  border: 1px solid var(--mainColor-light);
  border-radius: var(--radius-3);
  background-color: var(--mainColor-dark);
  color: var(--mainColor-lighter);
  font-size: var(--text-1);
  font-weight: 700;
  line-height: var(--leading-tight);
  letter-spacing: 0.04em;
  text-transform: uppercase;
  white-space: nowrap;
}

/* The hover line that says what to do about the chip (the path the engine
   actually looked in). Same voice as the meta lines, one step louder because it
   is the actionable one. */
.channelTooltipNoSet {
  margin-top: 4px;
  color: var(--mainColor-lighter);
  font-size: var(--text-3);
  line-height: var(--leading-body);
  border-top: 1px solid var(--mainColor-light);
  padding-top: 4px;
}

.mainToolbarWrapper .mainToolbar .buttonBar {
  position: relative;
  flex-shrink: 0;
  display: flex;
  flex-flow: column nowrap;
  align-items: center;
  margin: 0 10px 25px;
  padding-top: 1.5rem;
  text-align: center;
}

/* The transport's reading, worn by the rail so it is on screen in every
   signed-in view (see components/ConnectionStatus.vue). The state is carried
   by its word and never by the dot's color alone: the dot is a repetition of
   what the text already says, for the eye that reads the rail sideways. */
.mainToolbarWrapper .mainToolbar .buttonBar .connectionState {
  display: flex;
  flex-flow: column nowrap;
  align-items: center;
  gap: 3px;
  width: 100%;
  margin-bottom: 1rem;
  font-size: var(--text-2);
  line-height: var(--leading-tight);
  color: var(--text-dim);
}

.mainToolbarWrapper .mainToolbar .buttonBar .connectionState .connectionDot {
  width: 8px;
  height: 8px;
  border-radius: var(--radius-circle);
  background-color: currentColor;
}

.mainToolbarWrapper .mainToolbar .buttonBar .connectionState.connection-live {
  color: var(--state-online);
}

.mainToolbarWrapper .mainToolbar .buttonBar .connectionState.connection-connecting,
.mainToolbarWrapper .mainToolbar .buttonBar .connectionState.connection-retrying {
  color: var(--state-away);
}

.mainToolbarWrapper .mainToolbar .buttonBar .connectionState.connection-retrying .connectionWord,
.mainToolbarWrapper .mainToolbar .buttonBar .connectionState.connection-given-up .connectionWord {
  color: var(--mainColor-lightest);
}

.mainToolbarWrapper .mainToolbar .buttonBar .connectionState.connection-given-up {
  color: var(--errorColor);
}

/* The chip's second fact, in the quietest voice it has: the reading says how
   far the transport is, this line says *what* it is attached to — because a
   green `Connected` over the simulated lobby is a lie the reading alone cannot
   tell. It stays dim on every reading (the live reading paints the chip green)
   and a host that does not fit the rail wraps instead of clipping: a truncated
   address is the same defect in a new form. */
.mainToolbarWrapper .mainToolbar .buttonBar .connectionState .connectionBackend {
  color: var(--text-dim);
  overflow-wrap: anywhere;
  text-align: center;
}

/* The rail is --rail-w wide, so the shared button's panel padding does not fit
   it: this is the scale's sm step, the one small enough for a chip, and the
   geometry it takes is the step's own rather than a size measured against this
   callsite. */
.mainToolbarWrapper .mainToolbar .buttonBar .connectionState .connectionRetry {
  width: 100%;
  min-height: var(--control-h-sm);
  padding: 0 var(--control-pad-x-sm);
  font-size: var(--control-font-sm);
}

.mainToolbarWrapper .mainToolbar .buttonBar .notificationsButton,
.mainToolbarWrapper .mainToolbar .buttonBar .dmButton,
.mainToolbarWrapper .mainToolbar .buttonBar .settingsButton {
  display: block;
  position: relative;
  margin-bottom: 1rem;
  width: 30px;
  height: 30px;
}

.mainToolbarWrapper .mainToolbar .buttonBar .notificationsButton:hover,
.mainToolbarWrapper .mainToolbar .buttonBar .dmButton:hover,
.mainToolbarWrapper .mainToolbar .buttonBar .settingsButton:hover {
  cursor: pointer;
}

/* One button, one icon (F-173): the state selectors pair each button with
   the icon it actually contains instead of every combination. */
.mainToolbarWrapper .mainToolbar .buttonBar .notificationsButton:hover .notificationsIconSvg,
.mainToolbarWrapper .mainToolbar .buttonBar .dmButton:hover .dmIconSvg,
.mainToolbarWrapper .mainToolbar .buttonBar .settingsButton:hover .settingsIconSvg {
  fill: var(--mainColor-lighter);
  filter: drop-shadow(0 0 8px var(--mainColor-lighter));
  overflow: visible;
}

.mainToolbarWrapper .mainToolbar .buttonBar .notificationsButton.active .notificationsIconSvg,
.mainToolbarWrapper .mainToolbar .buttonBar .dmButton.active .dmIconSvg,
.mainToolbarWrapper .mainToolbar .buttonBar .settingsButton.active .settingsIconSvg {
  fill: var(--accentColor);
  filter: drop-shadow(0 0 8px var(--accentColor));
  overflow: visible;
}

.mainToolbarWrapper .mainToolbar .buttonBar .notificationsButton .notificationsIconSvg,
.mainToolbarWrapper .mainToolbar .buttonBar .dmButton .dmIconSvg,
.mainToolbarWrapper .mainToolbar .buttonBar .settingsButton .settingsIconSvg {
  width: 30px;
  height: 30px;
  fill: var(--mainColor-light);
  transition: fill 0.2s ease-out;
}

.mainToolbarWrapper .mainToolbar .buttonBar .notificationsButton .notificationsIconSvg:active,
.mainToolbarWrapper .mainToolbar .buttonBar .dmButton .dmIconSvg:active,
.mainToolbarWrapper .mainToolbar .buttonBar .settingsButton .settingsIconSvg:active {
  fill: var(--accentColor);
}

.mainToolbarWrapper .mainToolbar .buttonBar .notificationsButton .notificationsIndicator,
.mainToolbarWrapper .mainToolbar .buttonBar .dmButton .notificationsIndicator {
  display: flex;
  justify-content: center;
  align-items: center;
  position: absolute;
  top: 2px;
  right: -2px;
  width: 12px;
  height: 12px;
  font-size: var(--text-2);
  /* The badge is a 12px box holding one digit, so the digit's line box is what
     has to fit it: the inherited 1.45 leading is 15.95px, and a flex-centred
     half-line is a clipped half-line. One unit of leading, no more. */
  line-height: var(--leading-none);
  font-weight: 700;
  /* The digit is the badge's only word, and the plate is the warm accent: the
     inherited lightest ink was 2.82:1 on it, so the badge wore the theme's own
     rule for a loud plate — darkest ink on it (#121 U8). */
  color: var(--mainColor-darker);
  background-color: var(--accentColor2);
  border-radius: var(--radius-circle);
  box-shadow: 0 0 12px var(--accentColor2);
}

.mainToolbarWrapper .mainToolbar .buttonBar .userButton {
  display: block;
  position: relative;
  line-height: 0;
  z-index: 3;
}

.mainToolbarWrapper .mainToolbar .buttonBar .userButton .userAvatarWrapper {
  width: var(--rail-avatar);
  height: var(--rail-avatar);
  transition: transform 0.1s linear;
}

.mainToolbarWrapper .mainToolbar .buttonBar .userButton .userAvatarWrapper:hover {
  cursor: pointer;
  transform: scale(1.1);
}

.mainToolbarWrapper .mainToolbar .buttonBar .userButton .userAvatarWrapper .avatar {
  width: var(--rail-avatar);
  height: var(--rail-avatar);
  font-size: var(--text-7);
}

.mainToolbarWrapper .mainToolbar .buttonBar .userButton .userState {
  position: absolute;
  bottom: -3px;
  right: -3px;
  width: 16px;
  height: 16px;
  border-radius: var(--radius-circle);
  background-color: var(--state-online);
  box-shadow: 0 0 20px var(--state-online);
}

.mainToolbarWrapper .mainToolbar .buttonBar .userButton .userState.away {
  background-color: var(--state-away);
  box-shadow: 0 0 20px var(--state-away);
}

.mainToolbarWrapper .mainToolbar .buttonBar .userButton .userStateMenu {
  position: absolute;
  bottom: 85px;
  /* The panel is wider than the rail it hangs in (--popover-w over --rail-w),
     and it hangs on the avatar, which the bar centres in that rail — so the
     offset is the panel's inset from the rail's left edge minus the half of the
     rail the avatar does not fill. Centring the panel on the avatar instead
     would put 20px of it outside the window. */
  left: calc(20px - (var(--rail-w) - var(--rail-avatar)) / 2);
  width: var(--popover-w);
  padding: 1rem 0.5rem 1.25rem;
  color: var(--mainColor-lighter);
  font-size: var(--text-4);
  font-weight: 700;
  text-transform: uppercase;
  line-height: var(--leading-body);
  cursor: default;
  background-color: var(--mainColor-dark);
  border: 1px solid var(--mainColor-light);
  border-radius: var(--radius-4);
  text-align: left;
  box-shadow: 0 6px 12px var(--mainColor-darker-trans-lo);
}

.mainToolbarWrapper .mainToolbar .buttonBar .userButton .userStateMenu .optionWrapper {
  display: flex;
  width: 100%;
  align-items: center;
  justify-content: flex-start;
  gap: 6px;
  padding: 6px 0.75rem;
  border-radius: var(--radius-2);
  cursor: pointer;
}

/* A separator is a rule between groups of rows. It says nothing about what
   the rows mean: the row that ends the session carries the alert colour on
   its own, so a group can start without a row being tinted. */
.mainToolbarWrapper .mainToolbar .buttonBar .userButton .userStateMenu .optionWrapper.separator {
  margin-top: 0.5rem;
  padding-top: 1rem;
  border-top: 1px solid var(--mainColor-lighter);
}

/* Log out is the one row that ends the session, so it carries the alert
   colour the original gave it. */
.mainToolbarWrapper .mainToolbar .buttonBar .userButton .userStateMenu .optionWrapper.danger {
  color: var(--errorColor);
}

/* The row that ends the session, marked the way the face's own danger variant
   marks the act: at rest the alert ink on the menu's plate, on hover the alert
   plate with the darkest ink the theme owns.  Red ink on this row's old grey
   hover plate is 3.17:1, which is under the bar a 13px word has to clear, and
   the glow that used to stand in for the difference blurred the word it was
   meant to make readable. */
.mainToolbarWrapper .mainToolbar .buttonBar .userButton .userStateMenu .optionWrapper.danger:hover {
  color: var(--mainColor-darker);
  background-color: var(--errorColor);
}

.mainToolbarWrapper .mainToolbar .buttonBar .userButton .userStateMenu .optionWrapper:hover {
  color: var(--white);
}

.mainToolbarWrapper .mainToolbar .buttonBar .userButton .userStateMenu .optionWrapper.active {
  color: var(--white);
  background-color: var(--mainColor-light);
}

.mainToolbarWrapper .mainToolbar .buttonBar .userButton .userStateMenu .optionWrapper .stateShape {
  width: 0.75rem;
  height: 0.75rem;
  border-radius: var(--radius-circle);
}

.mainToolbarWrapper .mainToolbar .buttonBar .userButton .userStateMenu .optionWrapper .stateShape.online {
  background-color: var(--state-online);
  box-shadow: 0 0 20px var(--state-online);
}

.mainToolbarWrapper .mainToolbar .buttonBar .userButton .userStateMenu .optionWrapper .stateShape.away {
  background-color: var(--state-away);
  box-shadow: 0 0 20px var(--state-away);
}

/* ---- channel column ---- */

.channelWrapper {
  flex: 1;
  display: flex;
  flex-flow: column nowrap;
  overflow: hidden;
}

.channelWrapper .channelToolbar {
  flex-shrink: 0;
  display: flex;
  flex-flow: row nowrap;
  justify-content: space-between;
  padding: 0.75rem 0;
  background-image: linear-gradient(0deg, var(--mainColor-dark), var(--mainColor));
  color: var(--mainColor-lighter);
  text-transform: uppercase;
  z-index: 15;
  box-shadow: inset 0 -6px 6px 0 var(--mainColor-dark-trans-lo);
}

.channelWrapper .channelToolbar .channelInfo {
  width: auto;
  display: flex;
  flex-flow: row nowrap;
  align-items: center;
  overflow: hidden;
}

.channelWrapper .channelToolbar .channelInfo .name {
  padding: 1rem 0.5rem;
  font-weight: 700;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  line-height: 0;
}

.channelWrapper .channelToolbar .channelInfo .title {
  padding-left: 1rem;
  color: var(--mainColor-lightest-trans-hi);
}

/* The directory facts the room is actually about — system, year and the seats
 * the cabinet declares, straight from the same row the rail resolves its art
 * and tooltip from — as a quiet second voice beside the name; each part absent
 * when the directory has no such row. */
.channelWrapper .channelToolbar .channelInfo .descriptor {
  padding: 0 0.5rem 0 1rem;
  color: var(--mainColor-lightest-trans-hi);
  font-weight: 400;
  letter-spacing: 0.02em;
  user-select: none;
}

.channelWrapper .channelToolbar .channelInfo .rankedWrapper {
  line-height: 0;
  padding: 0 0.5rem;
}

.channelWrapper .channelToolbar .channelInfo .rankedWrapper .ranked {
  flex-shrink: 0;
  width: 24px;
  height: 24px;
  fill: var(--accentColor);
}

.channelWrapper .channelContent {
  flex-grow: 1;
  display: flex;
  flex-flow: row nowrap;
  justify-content: space-between;
  height: 100%;
  min-height: 0;
}

/* The player list: the column the room's roster lives in. It carries the
   row's own facts - the flag, the avatar, the name, the game's badges, the
   rank, the ping - and a column that squeezes them makes the row read as a
   blur, so the column is wide enough to hold them one by one. */
.usersListToolbar {
  flex-shrink: 0;
  display: flex;
  flex-flow: column nowrap;
  background-image: linear-gradient(135deg, var(--mainColor), var(--mainColor-dark));
  /* A name selects, a label may not (U7). This column used to ban selection for
   * everything inside it, which took the account names with the section
   * headers: `/block ` needs a name, and a player should not have to retype one
   * they can see. The headers keep their own ban; the rows do not. */
  width: 360px;
}

@media only screen and (min-width: 1280px) {
  .usersListToolbar {
    width: 400px;
  }
}

@media only screen and (min-width: 1600px) {
  .usersListToolbar {
    width: 430px;
  }
}

.usersListToolbar .usersListWrapper {
  flex-grow: 1;
  overflow: hidden;
  overflow-y: auto;
  padding-top: 1.5rem;
  min-height: 0;
}

.usersListToolbar .usersListWrapper .usersOnlineTitle,
.usersListToolbar .usersListWrapper .usersAwayTitle,
.usersListToolbar .usersListWrapper .usersBlockedTitle {
  /* A button, because a div with a click handler is not one: this cycles the
     roster sort and the keyboard has to reach it. */
  display: block;
  width: 100%;
  margin-bottom: 0.5rem;
  padding-left: 1rem;
  padding-bottom: 0.5rem;
  color: var(--mainColor-lightest-trans-hi);
  font-family: inherit;
  font-size: var(--text-6);
  font-weight: 700;
  text-align: left;
  text-transform: uppercase;
  background: none;
  border: 0;
  border-bottom: 1px solid var(--mainColor-light);
  cursor: pointer;
  user-select: none;
}

.usersListToolbar .usersListWrapper .usersOnlineTitle span,
.usersListToolbar .usersListWrapper .usersAwayTitle span,
.usersListToolbar .usersListWrapper .usersBlockedTitle span {
  margin-left: 0.5rem;
  color: var(--mainColor-lighter);
}

.usersListToolbar .usersListWrapper .usersOnlineTitle:hover,
.usersListToolbar .usersListWrapper .usersAwayTitle:hover,
.usersListToolbar .usersListWrapper .usersBlockedTitle:hover {
  color: var(--white);
  transition: color 0.2s ease-out;
}

.usersListToolbar .usersListWrapper .usersOnlineList,
.usersListToolbar .usersListWrapper .usersAwayList,
.usersListToolbar .usersListWrapper .usersBlockedList {
  width: 100%;
  margin-bottom: 1rem;
}

.usersListToolbar .usersListWrapper .matchesTitleWrapper {
  display: flex;
  flex-flow: row nowrap;
  align-items: center;
  justify-content: space-between;
  margin-bottom: 0.5rem;
  padding: 0 0.75rem 0.5rem 1rem;
  color: var(--mainColor-lightest-trans-hi);
  font-weight: 700;
  text-transform: uppercase;
  border-bottom: 1px solid var(--mainColor-light);
  user-select: none;
}

.usersListToolbar .usersListWrapper .matchesTitleWrapper .matchesTitle {
  flex-shrink: 0;
  flex-grow: 0;
}

.usersListToolbar .usersListWrapper .matchesTitleWrapper .matchesNumber {
  color: var(--mainColor-lighter);
  padding-left: 0.5rem;
  flex-grow: 1;
  text-align: left;
}

.usersListToolbar .usersListWrapper .matchesTitleWrapper .spectatorsIconWrapper {
  flex-shrink: 0;
  width: 1.5rem;
  height: 1.5rem;
}

.usersListToolbar .usersListWrapper .matchesTitleWrapper .spectatorsIconWrapper .eyeIconSvg {
  width: 100%;
  height: auto;
  fill: var(--mainColor-lighter);
}

.usersListToolbar .usersListWrapper .matchesList {
  margin-bottom: 1rem;
}

.userItem {
  position: relative;
  display: flex;
  flex-flow: row nowrap;
  align-items: center;
  justify-content: space-between;
  padding: 0.25rem 1rem;
  z-index: 1;
}

.userItem::after {
  position: absolute;
  content: '';
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  background-color: var(--mainColor-light);
  opacity: 0;
  z-index: -1;
}

.userItem:hover::after,
.userItem:focus-visible::after {
  opacity: 0.4;
}

.userItem.blocked::after {
  opacity: 0.4;
}

.userItem .flagWrapper,
.userItem .avatarWrapper,
.userItem .pingWrapper,
.userItem .rankWrapper {
  flex-shrink: 0;
}

/* Every flag is an <img> (FlagIcon); the border and the cover-crop live here
   once and each caller supplies the numbers it needs (F-163). The default
   border is the rounded-square frame the roster, DM and chip flags always
   had; the profile row's wide flag opts out. */
.flagImg {
  display: block;
  object-fit: cover;
  width: var(--flag-w);
  height: var(--flag-h);
  border: var(--flag-bw, 1px) solid var(--mainColor);
  border-radius: var(--flag-r, var(--radius-1));
}

/* Every avatar initial is a UserAvatar; the circle is declared once and the
   call sites only size it (F-167). */
.avatar {
  display: grid;
  place-items: center;
  border-radius: var(--radius-circle);
  background-color: var(--mainColor-light);
  font-weight: 700;
  line-height: var(--leading-none);
}

.userItem .flagWrapper {
  --flag-w: 17px;
  --flag-h: 17px;
  --flag-r: var(--radius-4);
  margin-right: 0.4rem;
}

.userItem .avatarWrapper {
  margin-right: 0.5rem;
  width: 1.7rem;
  height: 1.7rem;
  line-height: 0;
}

.userItem .avatarWrapper .avatar {
  width: 100%;
  height: 100%;
  font-size: var(--text-4);
}

.userItem .nameAndGame {
  flex: 1;
  margin-right: 0.5rem;
  line-height: var(--leading-tight);
  min-width: 0;
}

.userItem .nameAndGame .playerName {
  font-size: var(--text-6);
  font-weight: 600;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  cursor: default;
  color: var(--mainColor-lightest-trans-hi);
}

.userItem .nameAndGame .game {
  font-size: var(--text-3);
  font-style: italic;
  color: var(--mainColor-lighter);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  cursor: default;
}

.userItem .rankWrapper {
  width: 1.2rem;
  height: 1.2rem;
  margin-right: 0.5rem;
  line-height: 0;
}

.userItem .rankWrapper .image {
  width: 100%;
  height: auto;
  display: block;
}

/* The letter a missing badge file degrades to (RankBadge's contract): the
   wrapper keeps the badge's shape and the letter fills it. */
.userItem .rankWrapper .letter,
.playerChips .playerRank .letter {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 100%;
  height: 100%;
  font-size: var(--text-1);
  font-weight: 700;
  color: var(--mainColor-lighter);
}

/* The wrapper is 1rem — the size the original gives its own ping glyph. The
   art is 32px, so a 2x screen draws it 1:1 and a 1x screen downsamples it;
   the 12px set this replaces was authored at the display size and therefore
   upscaled (and blurred) everywhere it mattered. */
.userItem .pingWrapper {
  width: 1rem;
  height: 1rem;
  line-height: 0;
}

.userItem .pingWrapper .image {
  width: 100%;
  height: auto;
  display: block;
}

.matchItem {
  position: relative;
  display: flex;
  flex-flow: row wrap;
  justify-content: space-between;
  align-items: center;
  padding: 0.25rem 0.75rem 0.25rem 1rem;
  font-weight: 600;
  font-size: var(--text-6);
  cursor: default;
  z-index: 1;
}

.matchItem::after {
  position: absolute;
  content: '';
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  background-color: var(--mainColor-light);
  opacity: 0;
  z-index: -1;
}

.matchItem:hover::after,
.matchItem:focus-visible::after {
  opacity: 0.4;
}

.matchItem .matchInfo,
.matchItem .matchInfo .playerChips {
  flex-grow: 1;
  display: flex;
  flex-flow: row nowrap;
  align-items: center;
}

.matchItem .matchInfo {
  justify-content: space-between;
  margin-right: 0.5rem;
}

/* ---- the player row (PlayerChips) ----
   One row, one contract: the match lines, the challenge cards and the
   notification panel all render a player the same way, including the image
   fallback the vendored assets promise. `narrow` is the match-line variant,
   where two rows share one line between them. */
.playerChips {
  display: flex;
  flex-flow: row nowrap;
  align-items: center;
  min-width: 0;
}

.playerChips.narrow {
  width: 80px;
  overflow: hidden;
  justify-content: flex-start;
}

.playerChips .playerRank {
  flex-shrink: 0;
  margin-right: 0.25rem;
  border-radius: var(--radius-4);
  border: 1px solid var(--mainColor);
  line-height: 0;
  width: 1.2rem;
  height: 1.2rem;
}

.playerChips .playerRank .image {
  width: 100%;
  height: auto;
  display: block;
}

.playerChips .playerFlag {
  --flag-w: 15px;
  --flag-h: 15px;
  --flag-r: var(--radius-4);
  flex-shrink: 0;
  margin-right: 0.25rem;
}

.playerChips .playerName {
  flex-grow: 0;
  flex-shrink: 1;
  line-height: var(--leading-body);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  color: var(--mainColor-lightest-trans-hi);
}

.playerChips .playerPing {
  flex-shrink: 0;
  margin-left: 0.25rem;
  line-height: 0;
}

.playerChips .playerPing .image {
  width: 1rem;
  height: 1rem;
  display: block;
}

/* Cards and the notification panel give the chips room to breathe. */
.message .challengeContainer .playerChips,
.overlay.notifications .notification-challenge .content .playerChips {
  gap: 0.5rem;
}

.message .challengeContainer .playerChips .playerName,
.overlay.notifications .notification-challenge .content .playerChips .playerName {
  font-weight: 700;
  color: var(--white);
  text-transform: none;
}

.message .challengeContainer .playerChips .playerRank .image,
.overlay.notifications .notification-challenge .content .playerChips .playerRank .image {
  width: 1.2rem;
  height: 1.2rem;
}

.message .challengeContainer .playerChips .playerPing .image,
.overlay.notifications .notification-challenge .content .playerChips .playerPing .image {
  width: 1rem;
  height: 1rem;
}

/* A player we cannot resolve (background channel, left the room) shows the
   name and nothing invented. */
.challengerName {
  font-weight: 700;
  color: var(--white);
  text-transform: none;
}

.matchItem .matchInfo .matchVsText {
  flex-grow: 0;
  flex-shrink: 0;
  margin: 0 0.25rem;
  color: var(--mainColor-lighter);
  font-size: var(--text-4);
  font-weight: 700;
}

.matchItem .matchInfo .matchVsText strong {
  color: var(--white);
}

/* ---- the seat strip (docs/design/seats.md §3, §4) ----
   The open run's seats in seat order. A seated player renders exactly the
   chips every other surface renders them with; the four hollow states carry
   the fact by shape and word, never by color alone: a dashed outline reads
   "claimable", the invitee's own name under the dash reads "reserved", the
   dimmed ellipsis reads a held chair with its tooltip, and the words carry
   what a shape cannot. */
.matchItem .matchInfo .seatStrip {
  display: flex;
  flex-flow: row wrap;
  align-items: center;
  gap: 0.35rem;
  /* The strip is the row's one child, so it takes the line's whole width:
     without the grow its flex items' 50% bases resolve against a
     shrink-to-fit box and every chip sizes to its own words — the reconnecting
     card's strip measured ~169px against a ~290px line, clipping "Catching
     up…" to "Catching …". */
  flex: 1 1 auto;
  min-width: 0;
}

/* Two chairs a line, each half the strip less half the gap. The measure is
   the point: at the users column's own width (360-430px, `main.css`'s
   `.usersListToolbar`) four chairs on one line leave ~79px each and a name
   collapses to "br_…" — so a four-seat strip wraps to two rows and every
   name gets ~150px, which holds the fixture's longest ("br_operator"). A
   longer name still truncates at the chip's own ellipsis, with the full name
   in the chip's title. Seat order stays reading order across the wrap. */
.matchItem .matchInfo .seatStrip > * {
  flex: 1 1 calc(50% - 0.175rem);
  min-width: 0;
}

.matchItem .matchInfo .seatStrip .playerChips.narrow {
  width: auto;
}

.matchItem .matchInfo .seatChip {
  min-width: 0;
  height: 1.4rem;
  display: flex;
  align-items: center;
  justify-content: flex-start;
  padding: 0 0.3rem;
  border: 1px dashed var(--mainColor-lighter);
  border-radius: var(--radius-4);
  font-size: var(--text-4);
  font-weight: 600;
  color: var(--mainColor-lighter);
  overflow: hidden;
}

/* An open chair is Join's second door on an open-access run (§4): the `+` is
   drawn whether or not the pointer is over it — a still picture of the card
   must show the chair is there — and the cursor is a pointer only when there
   is a claim to make (`claimable`, decided in MatchesList: the set is here,
   the shell can hand off, no handoff is in flight). An invite-only run's open
   chair is not a door and must not look like one. */
.matchItem .matchInfo .seatChip.open .seatPlus {
  line-height: var(--leading-none);
  color: var(--mainColor-lighter);
  font-weight: 700;
}

.matchItem .matchInfo .seatChip.open.claimable {
  cursor: pointer;
}

.matchItem .matchInfo .seatChip.open.claimable:hover,
.matchItem .matchInfo .seatChip.open.claimable:active {
  border-color: var(--mainColor-lightest-trans-hi);
}

.matchItem .matchInfo .seatChip.open.claimable:hover .seatPlus,
.matchItem .matchInfo .seatChip.open.claimable:active .seatPlus {
  color: var(--mainColor-lightest);
}

.matchItem .matchInfo .seatChip .seatName,
.matchItem .matchInfo .seatChip .seatWord {
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

/* `held` and `out` words use the chip's own ink on purpose: the state is the
   word and the shape (§4), never a dimmer colour. The old --mainColor-light
   here measured 1.32:1 on the card — a state a player could not read
   (tools/contrast_check.py, 2026-10-29). The chip's ink measures 5.12:1 on
   the card and 4.61:1 over the row's hover plate. */

/* ---- watchers: the people behind the eye count ----
   Eye plus count on the card; the popover (click only) lists every visible
   name with its join count. The count is the room's fact and stays
   authoritative. */
.matchItem .matchWatchers {
  position: relative;
  flex-shrink: 0;
}

.matchItem .matchWatchers .watchersButton {
  display: flex;
  align-items: center;
  gap: 0.3rem;
  min-width: 1.5rem;
  padding: 0.15rem 0.35rem;
  color: var(--mainColor-lighter);
  font-size: var(--text-5);
  background: none;
  border: none;
  border-radius: var(--radius-2);
  cursor: pointer;
}

.matchItem .matchWatchers .watchersButton:hover,
.matchItem .matchWatchers .watchersButton[aria-expanded='true'] {
  color: var(--white);
  background-color: var(--mainColor-dark);
}

.matchWatchers .watcherFace {
  width: 1.1rem;
  height: 1.1rem;
  color: var(--white);
  font-size: var(--text-1);
  flex-shrink: 0;
}

.matchWatchers .watcherRow .watcherFace {
  margin-right: 0;
}

.matchWatchers .watcherFace.self {
  background-color: var(--accentColor);
}

.matchWatchers .watchersButton .watchersEye {
  width: 0.95rem;
  height: 0.95rem;
  fill: currentColor;
}

.matchWatchers .watchersPopover {
  position: absolute;
  right: 0;
  top: calc(100% + 4px);
  z-index: 30;
  min-width: 10rem;
  max-height: 14rem;
  overflow-y: auto;
  padding: 0.35rem;
  background-color: var(--mainColor-dark);
  border: 1px solid var(--mainColor-light);
  border-radius: var(--radius-3);
}

.matchWatchers .watcherRow {
  display: flex;
  align-items: center;
  gap: 0.45rem;
  padding: 0.2rem 0.35rem;
  border-radius: var(--radius-2);
  color: var(--white);
  font-size: var(--text-4);
}

.matchWatchers .watcherRow:focus {
  outline: none;
  background-color: var(--mainColor-light);
}

.matchWatchers .watcherName {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.matchWatchers .watcherJoins {
  margin-left: auto;
  padding-left: 0.5rem;
  color: var(--errorColor);
  font-size: var(--text-3);
  font-weight: 700;
  white-space: nowrap;
}

.matchWatchers .watcherYou {
  padding-left: 0.5rem;
  color: var(--mainColor-lighter);
  font-size: var(--text-2);
  text-transform: uppercase;
}

.matchWatchers .watcherRow .watcherYou {
  margin-left: 0;
}

.matchWatchers .watcherRow .watcherYou:last-child {
  margin-left: auto;
}

.matchWatchers .watcherMore {
  padding: 0.2rem 0.35rem;
  color: var(--mainColor-lighter);
  font-size: var(--text-3);
}

/* ---- the handoff row (docs/design/handoff.md §9) ----
   A full-width line under the players: a run's progress, the card's own
   Play/Join/Rejoin or Spectate action, or the stated reason where the
   action would be. `flex-basis: 100%` is what gives it its own line under
   the wrap above, and the row itself wraps rather than squeezes: a refusal is
   a sentence, and a sentence the player cannot read is not an answer. */
.matchItem .matchHandoffRow {
  flex-basis: 100%;
  display: flex;
  flex-flow: row wrap;
  align-items: center;
  gap: 0.5rem;
  min-width: 0;
  padding-top: 0.15rem;
}

/* The open run's identity line (seats.md §3): the room and the seated count
   where a closed card reads "A vs B, live". */
.matchItem .matchRunLine {
  flex-basis: 100%;
  display: flex;
  flex-flow: row nowrap;
  align-items: baseline;
  gap: 0.5rem;
  min-width: 0;
  padding-top: 0.15rem;
  font-size: var(--text-4);
  font-weight: 600;
}

.matchItem .matchRunLine .runTitle {
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  color: var(--mainColor-lighter);
}

.matchItem .matchRunLine .runStatus {
  flex-shrink: 0;
  color: var(--successColor);
  text-transform: uppercase;
  font-size: var(--text-2);
  letter-spacing: 0.05em;
}

/* The locked run's word reads as copy, not as a colored state: §11 spells it
   "Invite only", and the lock glyph carries the fact (seats.md §7). */
.matchItem .matchRunLine .runStatus.runLocked {
  color: var(--mainColor-lighter);
  text-transform: none;
  font-size: var(--text-3);
  letter-spacing: normal;
  display: inline-flex;
  align-items: center;
  gap: 0.2rem;
}

.matchItem .matchRunLine .runStatus.runLocked .lockIconSvg {
  width: 0.7rem;
  height: 0.7rem;
  fill: currentColor;
  flex-shrink: 0;
}

/* No ellipsis here: this span carries the reason an action is not offered, and
   §9's table is one row per layer precisely so the player can tell which layer
   refused. Truncating it would hide the answer behind the card's width. */
.matchItem .matchHandoffText {
  min-width: 0;
  font-size: var(--text-4);
  font-weight: 600;
  color: var(--mainColor-lighter);
}

.matchItem .matchHandoffText.active {
  color: var(--successColor);
}

.matchItem .matchHandoffButton {
  flex-shrink: 0;
  min-height: var(--control-h-sm);
  padding: 0 var(--control-pad-x-sm);
  font-size: var(--control-font-sm);
  font-weight: 700;
  text-transform: uppercase;
  color: var(--mainColor-lightest);
  background: none;
  border: 1px solid var(--mainColor-light);
  border-radius: var(--radius-2);
  cursor: pointer;
}

.matchItem .matchHandoffButton:hover {
  border-color: var(--accentColor);
  color: var(--white);
}

.matchItem .matchHandoffDismiss {
  flex-shrink: 0;
  padding: 0 0.25rem;
  font-size: var(--text-5);
  line-height: var(--leading-none);
  color: var(--mainColor-lighter);
  background: none;
  border: none;
  cursor: pointer;
}

.matchItem .matchHandoffDismiss:hover {
  color: var(--mainColor-lightest);
}

.emptyNote {
  padding: 16px;
  color: var(--mainColor-lighter);
  font-style: italic;
}

/* ---- chat ---- */

.chatWrapper {
  flex-grow: 1;
  position: relative;
  display: flex;
  flex-flow: column nowrap;
  justify-content: flex-end;
  align-items: flex-start;
  /* The row's one flexible column, and it must not size itself from its own
   * content: a long word in a message used to widen the column and push the
   * fixed roster past the window's edge (measured at \#122 U9 — 334px of
   * content in a 304px space, the roster's last 30px off-screen). Its floor
   * is a token, and the window's own floor is held against the token. */
  min-width: var(--chat-w-min);
  min-height: 0;
  background-image: linear-gradient(210deg, var(--mainColor-light), var(--mainColor) 60%, var(--mainColor-dark));
  box-shadow: 4px 0 6px var(--mainColor-dark-trans-lo);
}

/* The log is the growing member of the wrapper's column; the bottom stack
 * (.chatBottom: the invitation band over the chat bar) takes its own height
 * and the log gives up the rest. It used to be pinned top-and-bottom, which
 * left a band that grew past --chatbar-h painted behind the input. The margin
 * keeps the gap those two tokens always promised between log and bar. */
.chatWrapper .chatContent {
  flex: 1 1 auto;
  min-height: 0;
  width: 100%;
  padding: 1rem 0;
  margin-bottom: calc(var(--chatbar-clear) - var(--chatbar-h));
  color: var(--mainColor-lightest-trans-hi);
  overflow: hidden;
  overflow-y: auto;
}

.chatWrapper .chatContent::-webkit-scrollbar {
  width: 8px;
  background-color: transparent;
}

.chatWrapper .chatContent::-webkit-scrollbar-track {
  background-color: var(--mainColor);
}

.chatWrapper .chatContent .message {
  padding: 4px 16px;
  position: relative;
}

/* The report control belongs to the line it acts on, and only that line
 * reveals it (U7). It keeps its box while uninked so the Tab stop and the
 * pointer target never move, and pointer-events stay off until the row is
 * hovered or holds focus — an invisible control that still caught clicks would
 * eat the right end of every timestamp in the log. */
.chatWrapper .chatContent .message .messageTool {
  appearance: none;
  position: absolute;
  top: 4px;
  right: 16px;
  padding: 1px 6px;
  border: 1px solid var(--mainColor-lighter);
  border-radius: var(--radius-2);
  background: var(--mainColor);
  color: var(--mainColor-lightest);
  font: inherit;
  font-size: var(--text-2);
  opacity: 0;
  pointer-events: none;
}

.chatWrapper .chatContent .message:hover .messageTool,
.chatWrapper .chatContent .message:focus-within .messageTool {
  opacity: 1;
  pointer-events: auto;
}

.chatWrapper .chatContent .message .header {
  display: flex;
  align-items: center;
  gap: 8px;
}

.chatWrapper .chatContent .message .header .avatarWrapper {
  width: 2.5rem;
  line-height: 0;
  flex-shrink: 0;
}

.chatWrapper .chatContent .message .header .avatarWrapper .avatar {
  width: 2rem;
  height: 2rem;
}

.chatWrapper .chatContent .message .header .name {
  font-weight: 700;
}

.chatWrapper .chatContent .message .header .time {
  margin-left: auto;
  font-size: var(--text-2);
  color: var(--mainColor-lighter);
}

.chatWrapper .chatContent .message .line {
  padding-left: calc(2.5rem + 8px);
}

.chatWrapper .chatContent .blocks {
  overflow-wrap: anywhere;
  white-space: pre-line;
}

.chatWrapper .chatContent .blocks .block-link {
  color: var(--accentColor);
  text-decoration: underline;
  cursor: pointer;
}

/* A channel jump is a real <button> now, so it has to shed the UA box and sit
 * in the sentence exactly where the <span> it replaced sat: same line height,
 * no chrome, no inherited button font. */
.chatWrapper .chatContent .blocks .block-channel {
  appearance: none;
  display: inline;
  margin: 0;
  padding: 0;
  border: 0;
  background: none;
  font: inherit;
  text-align: inherit;
  color: var(--accentColor2);
  cursor: pointer;
}

.chatWrapper .chatContent .blocks .block-mention {
  font-weight: bold;
}

.chatWrapper .chatContent .blocks .block-mention-me {
  font-weight: bold;
  color: var(--accentColor);
}

.chatWrapper .chatContent .message.mention {
  box-shadow: inset 2px 0 0 var(--accentColor);
  background-color: var(--mainColor-dark-trans-lo);
}

/* The bottom stack: one column at the wrapper's foot, so every band that
 * joins it (the invitation draft today) shoves the log and the bar apart
 * instead of being painted under either. */
.chatWrapper .chatBottom {
  flex: 0 0 auto;
  width: 100%;
  display: flex;
  flex-flow: column nowrap;
}

.chatWrapper .chatInput {
  width: 100%;
}

.chatWrapper .chatInput input {
  font-family: 'Source Sans Pro', arial, sans-serif;
  width: 100%;
  /* The bar's height is its own (--chatbar-h) and the inset that made it read
     as a recessed tray is horizontal: padding the top and bottom of a 58px box
     with 20px each leaves an 18px content box for a 23.2px line, and the caret
     sits in the clipped half. A single-line input centres its own line in the
     content box, so the full height is what it needs. */
  padding: 0 1.25rem;
  font-size: var(--text-6);
  color: var(--white);
  border: 0;
  outline: 0;
  height: var(--chatbar-h);
  background-color: transparent;
  background-image: linear-gradient(100deg, var(--mainColor-dark), var(--mainColor-dark) 20%, var(--mainColor));
  box-shadow: inset 0 8px 18px var(--mainColor-darker-trans-lo);
}

.chatWrapper .chatInput input::placeholder {
  color: var(--mainColor-lighter);
}

/* ---- finder, settings, browser ---- */

/* The rail is a 120px column and the channel circles sit on its axis
   (80px circle, 20px margins). The find button is a full-width row in the
   same column, so its icon has to be centred on that axis too — as a
   shrink-to-fit button it would otherwise hang on the rail's left edge,
   half a column away from the circles it belongs with. */
.buttonItemWrapper {
  display: flex;
  justify-content: center;
  width: 100%;
  position: relative;
  border-radius: var(--radius-circle);
  cursor: pointer;
}

.buttonItemWrapper:hover .searchIcon {
  fill: var(--mainColor-lighter);
  filter: drop-shadow(0 0 6px var(--mainColor-lighter));
  overflow: visible;
}

.buttonItemWrapper .searchIcon {
  width: 36px;
  height: 36px;
  margin-top: 10px;
  fill: var(--mainColor-light);
  transition: fill 0.2s ease-out;
}

.buttonItemWrapper.active {
  cursor: default;
}

.buttonItemWrapper.active .searchIcon {
  fill: var(--accentColor);
  filter: drop-shadow(0 0 6px var(--accentColor));
  overflow: visible;
}

/* ---- settings (full panel, original taxonomy) ---- */

.mainToolbarWrapper .mainToolbar .buttonBar .settingsButton {
  margin-bottom: 1.5rem;
}

/* ---- the overlay (Overlay.vue) ----
   One slide-over primitive. Settings and notifications used to carry two
   near-identical implementations of the same surface, and neither had the
   keyboard contract a dialog owes. Width and alignment are the only things
   that differ between instances. */
.overlay {
  visibility: hidden;
  position: absolute;
  top: 0;
  right: 0;
  bottom: 0;
  left: var(--rail-w);
  z-index: 28;
  transition: visibility 0.1s ease-out;
}

.overlay.active {
  visibility: visible;
}

/* A modal owns the whole screen: its mask reaches the rail AND paints above
 * it, so the icons there cannot open or receive anything while a dialog is
 * up, and the rail dims under the mask like the rest of the screen. A panel
 * leaves the rail alone on purpose - the icons are its toggles (F-161). The
 * modal panel clears the rail the same way a panel does: a slide-over that
 * starts at the shell's left edge would slide in UNDER the rail and hide
 * its own heading behind the icons. */
.overlay.modal {
  left: 0;
  z-index: 40;
}

.overlay.modal .overlayPanel {
  margin-left: var(--rail-w);
  max-width: calc(90% - var(--rail-w));
}

.overlay .overlayMaskBg {
  position: absolute;
  top: 0;
  right: 0;
  bottom: 0;
  left: 0;
  opacity: 0;
  transition: opacity 0.1s ease-out;
  background-color: var(--mainColor-darker-trans-hi);
}

.overlay.active .overlayMaskBg {
  opacity: 1;
}

.overlay .overlayMask {
  position: absolute;
  top: 0;
  right: 0;
  bottom: 0;
  left: 0;
}

.overlay .overlayPanel {
  position: relative;
  width: var(--panel-w, 400px);
  max-width: 90%;
  height: 100%;
  padding: 2rem 0;
  background-image: linear-gradient(135deg, var(--mainColor-dark), var(--mainColor) 50%, var(--mainColor) 80%, var(--mainColor-light));
  box-shadow: 6px 0 12px var(--mainColor-darker-trans-lo);
  overflow: hidden;
  overflow-y: auto;
  text-align: var(--panel-align, center);
  opacity: 0;
  transform: translateX(-60px);
  transition: transform 0.1s ease-out, opacity 0.1s ease-out;
}

.overlay.active .overlayPanel {
  opacity: 1;
  transform: translateX(0);
}

.overlay.settings {
  --panel-w: 400px;
  --panel-align: center;
}

.overlay.notifications {
  --panel-w: 28.5rem;
  --panel-align: left;
}

.overlay.notifications .overlayPanel {
  font-size: var(--text-5);
  font-weight: 700;
  text-transform: uppercase;
}

.overlay.settings .overlayPanel .userAvatarWrapper {
  position: relative;
  display: inline-block;
  width: 100px;
  height: 100px;
  line-height: 0;
  margin-bottom: 1rem;
}

.overlay.settings .overlayPanel .userAvatarWrapper .avatar {
  width: 100%;
  height: 100%;
  font-size: var(--text-display);
}

.overlay.settings .overlayPanel .userIdWrapper {
  margin-bottom: 1.5rem;
}

.overlay.settings .overlayPanel .label {
  color: var(--mainColor-lighter);
  text-transform: uppercase;
  font-weight: 700;
  margin-bottom: 1.5rem;
  user-select: none;
}

.overlay.settings .overlayPanel .userName {
  margin-bottom: 1.5rem;
  font-size: var(--text-7);
  font-weight: 700;
  padding: 0 2rem;
  user-select: none;
}

.overlay.settings .overlayPanel .gamesOptions,
.overlay.settings .overlayPanel .frontendOptions,
.overlay.settings .overlayPanel .notificationsOptions,
.overlay.settings .overlayPanel .inputOptions,
.overlay.settings .overlayPanel .libraryOptions {
  border-top: 1px solid var(--mainColor-light);
  padding: 2rem 2rem 0;
  margin-bottom: 2rem;
}

.overlayPanel .option {
  display: flex;
  flex-flow: row nowrap;
  justify-content: space-between;
  align-items: center;
  margin-bottom: 1rem;
}

.overlayPanel .option .title {
  display: block;
  font-weight: 600;
  color: var(--mainColor-lightest-trans-hi);
  /* The label names the checkbox it points at, so a click on the title also
   * toggles it: the pointer says so, and the screen reader hears one name. */
  cursor: pointer;
  user-select: none;
  font-size: var(--text-5);
  text-align: left;
  margin: 0;
}

.overlay.settings .overlayPanel select {
  background-color: var(--mainColor);
  border: 2px solid var(--mainColor-light);
  border-radius: var(--radius-2);
  outline: none;
  color: var(--white);
  font-size: var(--text-4);
  padding: 0.25rem;
}

.overlayPanel .option .optionCheckboxWrapper {
  display: flex;
  flex-flow: row nowrap;
  align-items: center;
  position: relative;
}

.overlayPanel .option .optionCheckboxWrapper:hover {
  color: var(--white);
  user-select: none;
  cursor: default;
}

.overlayPanel .option .optionCheckboxWrapper input[type='checkbox'] {
  /* The real input is clipped (`.visuallyHidden`) and the box next to it is
   * drawn and carries the focus ring, so the keyboard's place is visible. It
   * used to be pushed to `left: -99999px`, which is the same thing to a person
   * but not to a geometry audit: a control out there is indistinguishable from
   * one the layout pushed off the window (#122 U9). */
  outline: none;
}

.overlayPanel .option .optionCheckboxWrapper input[type='checkbox']:focus-visible ~ .optionCheckbox {
  outline: 2px solid var(--accentColor);
  outline-offset: 2px;
}

.overlayPanel .option .optionCheckboxWrapper .optionCheckbox {
  position: relative;
  width: 20px;
  height: 20px;
  border-radius: var(--radius-2);
  border: 2px solid var(--mainColor-light);
}

.overlayPanel .option .optionCheckboxWrapper .optionCheckbox::before {
  content: '';
  position: absolute;
  top: 3px;
  left: 3px;
  right: 3px;
  bottom: 3px;
  background-color: var(--white);
  opacity: 0;
  transition: opacity 0.1s ease-out;
}

.overlayPanel .option .optionCheckboxWrapper input[type='checkbox']:checked ~ .optionCheckbox::before {
  opacity: 1;
}

.overlayPanel .option .optionCheckboxWrapper input[type='checkbox']:checked ~ .optionCheckbox {
  border-color: var(--accentColor);
  box-shadow: 0 0 6px var(--accentColor);
}

.overlayPanel .option .optionCheckboxWrapper label {
  position: absolute;
  top: 0;
  left: 0;
  right: 0;
  bottom: 0;
  cursor: pointer;
}

/* The input section's line under its button says what the button opens, so
   the player does not have to press it to find out. */
.overlay.settings .overlayPanel .inputHint {
  margin-top: 0.35rem;
  font-size: var(--text-4);
  color: var(--mainColor-lighter);
}

/* The game-library section is the input section's sibling: the same divider
   (the grouped rule above), the same quiet note family, and the same rule that a
   button says what it does before it is pressed. */

/* The path is the one string in this panel a player has to be able to take with
   them — it goes into a folder door, a share path or a bug report — so it is
   selectable text that wraps rather than a truncated label, and it is a size
   above the notes it sits under. */
.overlay.settings .overlayPanel .libraryPath {
  margin: 0 0 0.35rem;
  font-size: var(--text-5);
  font-weight: 600;
  color: var(--mainColor-lightest);
  user-select: text;
  overflow-wrap: anywhere;
}

.overlay.settings .overlayPanel .libraryNote {
  margin: 0 0 0.35rem;
  font-size: var(--text-4);
  color: var(--mainColor-lighter);
}

/* A sentence about the machine rather than about the setting: the folder that is
   not there, or the shell that cannot be asked. Same weight, a brighter voice —
   the .inputNotice.is-reason line the input screen already uses. */
.overlay.settings .overlayPanel .libraryNote.is-reason {
  color: var(--mainColor-lightest);
}

/* The folder door's own refusal, which is the only red this section is allowed:
   it is about the path the player just handed back, not about a set they lack. */
.overlay.settings .overlayPanel .libraryNote.is-fault {
  color: var(--errorColor);
}

.overlay.settings .overlayPanel .libraryDirs {
  list-style: none;
  margin: 0.35rem 0 0.75rem;
  padding: 0;
  max-width: 28rem;
}

/* One folder per row, name and count at the two ends: the layout a player keeps
   their sets in is the thing being read back to them, so it is lined up rather
   than run into one comma list. */
.overlay.settings .overlayPanel .libraryDir {
  display: flex;
  justify-content: space-between;
  gap: 1rem;
  font-size: var(--text-4);
  padding: 0.1rem 0;
}

.overlay.settings .overlayPanel .libraryDirName {
  color: var(--mainColor-lightest);
  overflow-wrap: anywhere;
}

.overlay.settings .overlayPanel .libraryDirCount {
  color: var(--mainColor-lighter);
  white-space: nowrap;
}

.overlay.settings .overlayPanel .libraryActions {
  display: flex;
  align-items: center;
  gap: 0.75rem;
  flex-wrap: wrap;
  margin-top: 0.5rem;
}

/* "Use this app's folder" is the second voice of the pair: the .appButton is the
   action that opens the door, this one only undoes a pick. The quiet button shape
   the input screen's own actions wear. */
.overlay.settings .overlayPanel .libraryUndo {
  font: inherit;
  font-size: var(--text-4);
  color: var(--mainColor-lightest);
  background: transparent;
  border: 1px solid var(--mainColor-light);
  border-radius: var(--radius-2);
  padding: 0.3rem 0.8rem;
  cursor: pointer;
}

.overlay.settings .overlayPanel .libraryUndo:hover:not(:disabled) {
  background: var(--mainColor-darker);
  border-color: var(--mainColor-lighter);
}

.overlay.settings .overlayPanel .libraryUndo:disabled {
  opacity: 0.45;
  cursor: default;
}

/* ---- notifications (original taxonomy) ---- */

/* Every panel speaks with the same title row: DMs, notifications and
   settings all carry one, so the three open the same way. */
.overlay .overlayPanel .panelHead {
  display: flex;
  flex-flow: row nowrap;
  align-items: center;
  justify-content: space-between;
  padding: 0 2rem 1rem;
  font-size: var(--text-4);
  font-weight: 700;
  text-transform: uppercase;
  color: var(--mainColor-lighter);
}

.overlay .overlayPanel .panelHead .clear:hover {
  cursor: pointer;
  color: var(--white);
}

.overlay.notifications .notification-mention,
.overlay.notifications .notification-challenge {
  position: relative;
  padding: 0.5rem 2rem 1rem;
  margin-bottom: 0.5rem;
  border-bottom: 1px solid var(--mainColor-light);
  cursor: pointer;
}

/* The rows are reachable by keyboard now, so the pointer cursor and the focus
   ring have to agree that the whole row is the target. */
.overlay.notifications .notification-mention:focus-visible,
.overlay.notifications .notification-challenge:focus-visible {
  background-color: var(--mainColor-light);
  outline-offset: -2px;
}

.overlay.notifications .notification-mention:last-child,
.overlay.notifications .notification-challenge:last-child {
  border-bottom: 0;
}

.overlay.notifications .notification-mention .header {
  color: var(--accentColor2);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  transition: color 0.2s ease-out;
}

.overlay.notifications .notification-mention .header:hover {
  cursor: pointer;
  color: var(--white);
}

.overlay.notifications .notification-challenge .header {
  color: var(--accentColor);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  margin-bottom: 0.5rem;
  transition: color 0.2s ease-out;
}

.overlay.notifications .notification-challenge .header:hover {
  cursor: pointer;
  color: var(--white);
}

.overlay.notifications .notification-mention .content {
  text-transform: none;
  font-weight: 400;
  color: var(--mainColor-lightest-trans-hi);
}

.overlay.notifications .notification-challenge .content {
  display: flex;
  flex-flow: row nowrap;
  align-items: center;
}

.overlay.notifications .notification-challenge .content .playerChips {
  margin-right: 1rem;
}

.overlay.notifications .notification-challenge .content .buttons {
  display: flex;
  align-items: center;
  gap: 0.75rem;
  margin-left: auto;
}

/* The two acts of a challenge notice wear the control scale's flat face: the
   accent marks the act the notice wants taken, the plain one is the act that
   says no. They used to be a slab shrunk by a per-callsite font size beside a
   bare word with no geometry at all — an 18.9px target next to a 25px one, in
   the same row, for the same kind of decision (\#121 U8). */
.overlay.notifications .notification-challenge .content .buttons .accept .icon {
  /* An icon inside a control is one label box of that control. */
  width: 1em;
  height: 1em;
  fill: currentColor;
}

.overlay.notifications .notification-challenge .content .buttons .decline {
  color: var(--mainColor-lighter);
}

.overlay.notifications .notification-challenge .content .buttons .decline:hover {
  cursor: pointer;
  color: var(--errorColor);
}

/* ---- direct messages (PLAN 6.3): the inbox over the server's rows ---- */

.overlay.notifications .overlayPanel .dmList {
  outline: none;
}

.overlay.notifications .overlayPanel .dmRow {
  padding: 0.75rem 1rem;
  border-bottom: 1px solid var(--mainColor-light);
  cursor: pointer;
}

.overlay.notifications .overlayPanel .dmRow:focus-visible {
  outline: 2px solid var(--accentColor);
  outline-offset: -2px;
}

.overlay.notifications .overlayPanel .dmRowTop {
  display: flex;
  align-items: center;
  gap: 0.5rem;
}

/* The peer's flag, drawn the way the roster draws one. */
.overlay.notifications .overlayPanel .dmFlag {
  --flag-w: 15px;
  --flag-h: 15px;
  --flag-r: var(--radius-4);
  flex-shrink: 0;
}

.overlay.notifications .overlayPanel .dmRowTime {
  margin-left: auto;
  font-size: var(--text-2);
  color: var(--mainColor-lighter);
  text-transform: none;
}

.overlay.notifications .overlayPanel .dmPeer {
  font-size: var(--text-5);
}

.overlay.notifications .overlayPanel .dmUnread {
  display: inline-flex;
  justify-content: center;
  align-items: center;
  min-width: 1.25rem;
  height: 1.25rem;
  padding: 0 0.3rem;
  font-size: var(--text-3);
  font-weight: 700;
  color: var(--mainColor-darker);
  background-color: var(--accentColor2);
  border-radius: var(--radius-circle);
}

.overlay.notifications .overlayPanel .dmPreview {
  font-size: var(--text-4);
  font-weight: 400;
  text-transform: none;
  color: var(--mainColor-lighter);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.overlay.notifications .overlayPanel .dmOlderWrap {
  margin: 0 1rem 0.5rem;
}

.overlay.notifications .overlayPanel .dmThread {
  max-height: 50vh;
  overflow-y: auto;
  margin: 0 1rem 1rem;
  outline: none;
}

.overlay.notifications .overlayPanel .dmMine,
.overlay.notifications .overlayPanel .dmTheirs {
  margin-bottom: 0.75rem;
  font-weight: 400;
  text-transform: none;
}

.overlay.notifications .overlayPanel .dmMine .dmAuthor,
.overlay.notifications .overlayPanel .dmTheirs .dmAuthor {
  font-size: var(--text-3);
  font-weight: 700;
  color: var(--mainColor-lighter);
}

.overlay.notifications .overlayPanel .dmMine .dmText,
.overlay.notifications .overlayPanel .dmTheirs .dmText {
  font-size: var(--text-5);
  word-break: break-word;
}

.overlay.notifications .overlayPanel .dmMine .dmTime,
.overlay.notifications .overlayPanel .dmTheirs .dmTime {
  font-size: var(--text-2);
  color: var(--mainColor-lighter);
}

.overlay.notifications .overlayPanel .dmComposer {
  display: flex;
  gap: 0.5rem;
  margin: 0 1rem 1rem;
}

.overlay.notifications .overlayPanel .dmComposer input {
  flex: 1;
  min-width: 0;
  padding: 0.5rem 0.75rem;
  font-size: var(--text-5);
  color: inherit;
  background-color: var(--mainColor-dark);
  border: 1px solid var(--mainColor-light);
  border-radius: var(--radius-2);
}

.overlay.notifications .overlayPanel .dmComposer .button-generic {
  /* Only the row's own spacing and the label's refusal to wrap: the slab's
   * step is the scale's, and this rule used to quiet it to --text-4. */
  margin-bottom: 0;
  white-space: nowrap;
}

/* ---- user report dialog (PLAN 6.4): moderation food, never room state ---- */

/* The report dialog has left the settings panel behind: its own variant,
 * wider than the settings panel and left-aligned like the gamecard form.
 * The panel and the dialog are flex columns, so the dialog fills the panel
 * and the reason field takes every spare pixel of vertical space. */
.overlay.report {
  --panel-w: 30rem;
  --panel-align: left;
}

.overlay.report .overlayPanel {
  display: flex;
  flex-direction: column;
}

.overlay.report .overlayPanel .reportDialog {
  padding: 0 2rem;
  flex: 1 1 auto;
  min-height: 0;
  display: flex;
  flex-direction: column;
}

.overlay.report .overlayPanel .reportDialog .label {
  color: var(--mainColor-lighter);
  text-transform: uppercase;
  font-weight: 700;
  margin-bottom: 0.5rem;
  user-select: none;
}

.overlay.report .overlayPanel .reportDialog .reportTarget {
  font-size: var(--text-7);
  font-weight: 700;
  margin-bottom: 0.25rem;
}

.overlay.report .overlayPanel .reportDialog .reportContext {
  color: var(--mainColor-lighter);
  font-size: var(--text-5);
  font-weight: 700;
  text-transform: uppercase;
  margin-bottom: 1.5rem;
  user-select: none;
}

/* The evidence the report travels with (D-014): the flagged message or the
 * named set, the thing the moderator acts on. Boxed like the reason field,
 * reading like the console's evidence line: label small and faded, the text
 * itself in sentence case and wrapping, because a message a player wrote can
 * be long. */
.overlay.report .overlayPanel .reportDialog .reportEvidence {
  background-color: var(--mainColor-dark);
  border: 1px solid var(--mainColor-light);
  border-radius: var(--radius-2);
  padding: 0.5rem 0.75rem;
  margin-bottom: 1rem;
  text-align: left;
}

.overlay.report .overlayPanel .reportDialog .reportEvidence .reportEvidenceLabel {
  display: block;
  font-size: var(--text-1);
  font-weight: 700;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--mainColor-lighter);
}

.overlay.report .overlayPanel .reportDialog .reportEvidence .reportEvidenceText {
  color: var(--mainColor-lightest);
  overflow-wrap: anywhere;
}

.overlay.report .overlayPanel .reportDialog textarea {
  width: 100%;
  min-height: 6rem;
  padding: 0.5rem 0.75rem;
  font-size: var(--text-5);
  font-family: inherit;
  color: inherit;
  background-color: var(--mainColor-dark);
  border: 1px solid var(--mainColor-light);
  border-radius: var(--radius-2);
  flex: 1 1 auto;
}

.overlay.report .overlayPanel .reportDialog .reportCount {
  margin-top: 0.25rem;
  font-size: var(--text-3);
  color: var(--mainColor-lighter);
}

.overlay.report .overlayPanel .reportDialog .reportError {
  margin-top: 0.5rem;
  font-size: var(--text-4);
  color: var(--errorColor);
  text-transform: none;
}

.overlay.report .overlayPanel .reportDialog .reportButtons {
  display: flex;
  justify-content: center;
  align-items: center;
  gap: 1rem;
  margin-top: 1rem;
}

.overlay.report .overlayPanel .reportDialog .reportButtons .button-generic {
  /* The row owns the pair's spacing (gap, centering) the way the challenge
   * card's and the match report's rows do: button-generic centers itself in
   * a column with `margin: 0 auto`, and inside this full-width flex row
   * those auto margins would split the two buttons to the panel's edges. */
  margin: 0;
}

/* The row's quiet half.  It used to wear no face at all: the `button` reset at
 * the top of this file paints nothing, so this rule's ink — one step *down* the
 * ramp from the plate behind it — sat on the panel's own gradient and the panel's
 * brightest stop made the word disappear (1.00:1, measured by
 * `tools/ui_scale_census.py` on `#33373d` over `#33373d`).  On the flat face the
 * control brings its own plate and the same quiet voice the challenge card's
 * Decline uses clears the bar on it: `--mainColor-lighter` on `--mainColor-dark`
 * is 5.77:1, and `--errorColor` on it is 4.71:1. */
.overlay.report .overlayPanel .reportDialog .reportButtons .reportCancel {
  color: var(--mainColor-lighter);
}

.overlay.report .overlayPanel .reportDialog .reportButtons .reportCancel:hover {
  color: var(--errorColor);
}

/* ---- account view (PLAN 6.5): only what the service owns ---- */

.overlay.settings .overlayPanel .profileView {
  padding: 0 2rem;
}

.overlay.settings .overlayPanel .profileView .userName {
  margin-bottom: 0.5rem;
}

.overlay.settings .overlayPanel .profileView .profileTier {
  margin-bottom: 1.5rem;
  font-size: var(--text-5);
  font-weight: 700;
  text-transform: uppercase;
  color: var(--mainColor-lighter);
  user-select: none;
}

.overlay.settings .overlayPanel .profileView .profileRow {
  display: flex;
  justify-content: center;
  align-items: center;
  gap: 0.5rem;
  margin-bottom: 1rem;
  font-size: var(--text-5);
  font-weight: 700;
  text-transform: uppercase;
}

.overlay.settings .overlayPanel .profileView .profileRow .flagWrapper {
  --flag-w: 21px;
  --flag-h: 15px;
  --flag-bw: 0;
  flex-shrink: 0;
}

.overlay.settings .overlayPanel .profileView .profileRow .label {
  margin-bottom: 0;
  color: var(--mainColor-lighter);
}

/* ---- account card + standings (PLAN 7.3): the shared body ---- */

.overlay.settings .overlayPanel .profileView .profileSectionTitle {
  margin: 1.5rem 0 0.5rem;
  font-size: var(--text-4);
  font-weight: 700;
  text-transform: uppercase;
  color: var(--mainColor-lighter);
  text-align: center;
  user-select: none;
}

.overlay.settings .overlayPanel .profileView .profileStandings {
  width: 100%;
  border-collapse: collapse;
  font-size: var(--text-4);
}

.overlay.settings .overlayPanel .profileView .profileStandings th {
  padding: 0.3rem 0.5rem;
  font-size: var(--text-2);
  text-transform: uppercase;
  color: var(--mainColor-lighter);
  text-align: left;
  user-select: none;
}

.overlay.settings .overlayPanel .profileView .profileStandings td {
  padding: 0.35rem 0.5rem;
  border-top: 1px solid var(--mainColor-dark);
  white-space: nowrap;
}

.overlay.settings .overlayPanel .profileView .profileHistory {
  list-style: none;
  margin: 0;
  padding: 0;
  font-size: var(--text-4);
}

.overlay.settings .overlayPanel .profileView .profileHistory .historyRow {
  display: flex;
  flex-flow: row nowrap;
  gap: 0.5rem;
  padding: 0.35rem 0;
  border-top: 1px solid var(--mainColor-dark);
}

.overlay.settings .overlayPanel .profileView .profileHistory .historyWhen {
  min-width: 6.5rem;
  color: var(--mainColor-lighter);
}

.overlay.settings .overlayPanel .profileView .profileHistory .historyChannel {
  min-width: 4rem;
  color: var(--mainColor-lighter);
}

.overlay.settings .overlayPanel .profileView .profileHistory .historyResult {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.overlay.settings .overlayPanel .profileView .profileHistory .historyCorrected {
  margin-left: auto;
  padding: 0 0.35rem;
  border: 1px solid var(--mainColor-darker);
  border-radius: var(--radius-2);
  color: var(--mainColor-lighter);
  font-size: var(--text-3);
  white-space: nowrap;
}

/* ---- channel web links (REPLAYS / RANKINGS) ----
   Both destinations live on the web beside the app, addressed the way the
   original addresses them (`/game/{gameid}/…`): real anchors opened by the
   system browser. Each page owns its own stylesheet (src/ranks, src/replays).
   The toolbar's own buttons join the list rather than declaring their own
   shape: one family rule, so one scale cannot end up with four treatments of
   itself (\#121 U8). */

.channelWrapper .channelToolbar .channelInfo .replaysButton,
.channelWrapper .channelToolbar .channelInfo .rankingsButton,
.channelWrapper .channelToolbar .channelInfo .localPlayButton {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  margin-left: 0.75rem;
  min-height: var(--control-h-sm);
  padding: 0 var(--control-pad-x-sm);
  font-size: var(--control-font-sm);
  line-height: var(--leading-tight);
  font-weight: 700;
  text-transform: uppercase;
  color: var(--mainColor-lighter);
  background: transparent;
  border: 1px solid var(--mainColor-light);
  border-radius: var(--radius-2);
  text-decoration: none;
  cursor: pointer;
  white-space: nowrap;
  transition:
    color 0.2s ease-out,
    border-color 0.2s ease-out;
}

.channelWrapper .channelToolbar .channelInfo .replaysButton:hover,
.channelWrapper .channelToolbar .channelInfo .rankingsButton:hover,
.channelWrapper .channelToolbar .channelInfo .localPlayButton:hover {
  color: var(--white);
  border-color: var(--mainColor-lightest-trans-lo);
}

/* What the solo boot says about itself (views/ShellView.vue). The sentence is
 * the shell's own fact — the process exists, or it left and said this — and it
 * sits beside the button that made it, not in a toast that drifts away while the
 * cabinet is still running. The × clears an answer the player has read. */
.channelWrapper .channelToolbar .channelInfo .localPlayText {
  margin-left: 0.6rem;
  padding: 0 0.25rem;
  font-size: var(--text-3);
  font-weight: 600;
  color: var(--mainColor-lighter);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  min-width: 0;
}

.channelWrapper .channelToolbar .channelInfo .localPlayDismiss {
  flex-shrink: 0;
  padding: 0 0.25rem;
  font-size: var(--text-5);
  line-height: var(--leading-none);
  color: var(--mainColor-lighter);
  background: none;
  border: none;
  cursor: pointer;
}

.channelWrapper .channelToolbar .channelInfo .localPlayDismiss:hover {
  color: var(--mainColor-lightest);
}

/* ---- context menu (original taxonomy) ---- */

.mask {
  position: fixed;
  top: 0;
  left: 0;
  bottom: 0;
  right: 0;
  z-index: 100;
}

.menu {
  position: absolute;
  /* The menu's one width declaration (F-175); the placement code measures it. */
  width: 220px;
  color: var(--mainColor-lighter);
  text-transform: uppercase;
  font-size: var(--text-4);
  font-weight: 700;
  background-color: var(--mainColor-dark);
  border-top: 1px solid var(--mainColor-light);
  border-left: 1px solid var(--mainColor-light);
  border-bottom: 1px solid var(--mainColor-dark);
  border-right: 1px solid var(--mainColor-dark);
  border-radius: var(--radius-2);
  text-align: left;
  box-shadow: 0 0 12px 4px var(--mainColor-dark-trans-lo);
}

.menu .contextMenuItemsWrapper {
  padding: 0.75rem 0.5rem;
}

.contextMenuItem {
  display: flex;
  flex-flow: row nowrap;
  justify-content: flex-start;
  align-items: center;
  gap: 0.5rem;
  margin: 0;
  padding: 0.35rem 0.6rem;
  transition: color 0.2s ease-out;
  user-select: none;
}
.contextMenuItem:not(:last-child) {
  margin-bottom: 0.5rem;
}

.contextMenuItem:hover:not(.disabled) {
  cursor: pointer;
  color: var(--white);
  background-color: var(--mainColor-light);
  border-radius: var(--radius-2);
}

.contextMenuItem.disabled {
  color: var(--mainColor-light);
  cursor: default;
}

.contextMenuItem.warning:hover:not(.disabled) {
  color: var(--errorColor);
}

.contextMenuItem .label {
  flex-grow: 1;
}

.contextMenuItem .state {
  width: 1rem;
  text-align: right;
  color: var(--accentColor);
  font-weight: 700;
}

/* ---- challenge + event + motd messages ---- */

.chatWrapper .chatContent .messageWrapper {
  position: relative;
  padding-bottom: 0.5rem;
  margin-bottom: 0.5rem;
  font-size: var(--text-5);
  line-height: var(--leading-body);
}

.chatWrapper .chatContent .messageWrapper.event {
  text-align: center;
  color: var(--mainColor-lighter);
  font-style: italic;
  padding: 0.25rem 1rem;
}

.chatWrapper .chatContent .messageWrapper.motd {
  margin: 0 1rem 1rem;
  padding: 0.4rem 0.75rem;
  border-radius: var(--radius-2);
  box-shadow: inset 0 0 60px var(--mainColor-dark);
  background-color: var(--mainColor);
  color: var(--mainColor-lightest);
  text-align: center;
}

.chatWrapper .chatContent .message.flash {
  animation: flashRow 0.8s ease-out 3;
}

@keyframes flashRow {
  0%,
  100% {
    background-color: transparent;
  }
  50% {
    background-color: var(--mainColor-dark-trans-lo);
  }
}

/* The jumped-to line: three pulses is how the eye catches it, and the mark the
 * pulse carries is the message, not the movement. Steady ink for as long as the
 * class stands (ChatPane's FLASH_MS) says "here" without moving. */
@media (prefers-reduced-motion: reduce) {
  .chatWrapper .chatContent .message.flash {
    animation: none;
    background-color: var(--mainColor-dark-trans-lo);
  }
}

.message .challengeContainer {
  display: flex;
  flex-flow: row wrap;
  align-items: center;
  justify-content: center;
  gap: 0.5rem 1rem;
  padding: 0.75rem 1rem;
  background-color: var(--mainColor-dark);
  border: 1px solid var(--mainColor-light);
  border-radius: var(--radius-2);
  text-align: center;
}

/* The chips in a card are the shared row with room to breathe: gap, name weight
   and icon sizes are declared once, for the card and the notification panel
   together. */
.message .challengeContainer .playerChips .playerAvatar {
  width: 1.7rem;
  height: 1.7rem;
  flex-shrink: 0;
  margin-right: 0.5rem;
}

.message .challengeContainer .wrapUpWrapper.accepted {
  color: var(--successColor);
}

.message .challengeContainer .wrapUpWrapper.declined {
  color: var(--errorColor);
}

.message .challengeContainer .wrapUpWrapper.cancelled {
  color: var(--mainColor-lighter);
}

.message .challengeContainer .wrapUpWrapper.challenging {
  color: var(--accentColor);
}

/* The card's acts are the scale's own flat face: they were a fourth treatment of
   a button — `background: none; border: 0; padding: 0`, uppercase, 700, a red
   `drop-shadow` on hover — whose box was the label's line box, 20.3px tall
   against WCAG 2.5.8's 24px floor (#121 U8, read off the captured row at
   1280x800: a target the census could not see because nothing in its name said
   "button").  Decline wears the alert variant because it refuses another
   player; Cancel stays the plain face because withdrawing your own reservation
   is an undo, not a danger.  The row wraps like the card it sits in: the accept
   label carries the rank's word ("Accept (Unranked)"), and a nowrap row is the
   same overflow in a smaller box. */
.message .challengeContainer .rank-options {
  display: flex;
  flex-flow: row wrap;
  align-items: center;
  justify-content: center;
  gap: 0.5rem 1rem;
}

/* ---- floating mention + emoji picker ---- */

.chatWrapper .floatingNotificationsWrapper {
  position: absolute;
  top: 1rem;
  right: 10px;
  left: 10px;
  padding: 0 1rem;
  display: flex;
  flex-flow: column nowrap;
  align-items: center;
  font-size: var(--text-4);
  font-weight: 700;
  text-transform: uppercase;
  z-index: 20;
  user-select: none;
}

.chatWrapper .floatingNotificationsWrapper .floatingNotificationsItem {
  margin-bottom: 0.5rem;
  padding: 0.5rem 1rem;
  border-radius: var(--radius-2);
  box-shadow: 0 0 12px var(--mainColor-dark);
  border-top: 1px solid var(--mainColor-light);
  border-left: 1px solid var(--mainColor-light);
  border-right: 1px solid var(--mainColor-dark);
  border-bottom: 1px solid var(--mainColor-dark);
  background-color: var(--mainColor-dark);
  color: var(--accentColor2);
}

.chatWrapper .floatingNotificationsWrapper .floatingNotificationsItem:hover {
  cursor: pointer;
  color: var(--white);
}

.chatWrapper .emojisView {
  position: absolute;
  bottom: var(--chatbar-clear);
  left: 1rem;
  right: 1rem;
  padding: 0.75rem;
  display: flex;
  flex-flow: row wrap;
  gap: 0.35rem;
  background-color: var(--mainColor-dark);
  border: 1px solid var(--mainColor-light);
  border-radius: var(--radius-4);
  z-index: 20;
}

.chatWrapper .emojiMask {
  position: fixed;
  top: 0;
  left: 0;
  bottom: 0;
  right: 0;
  z-index: 19;
}

.chatWrapper .emojisView .emoji {
  border: 0;
  background: none;
  font-size: var(--text-8);
  font-family: inherit;
  line-height: var(--leading-none);
  padding: 0.2rem;
  border-radius: var(--radius-2);
}

.chatWrapper .emojisView .emoji:hover,
.chatWrapper .emojisView .emoji:focus-visible {
  cursor: pointer;
  background-color: var(--mainColor-dark-trans-lo);
}

/* The invitation being assembled, one strip above the chat bar. Not a popup:
   it outlives the click that made it, and the person may leave the keyboard
   with names still sitting on it. */
.chatWrapper .inviteDraft {
  display: flex;
  flex-flow: row wrap;
  align-items: center;
  justify-content: center;
  gap: 0.4rem 0.6rem;
  padding: 0.4rem 0.75rem;
  background-color: var(--mainColor-dark);
  border-top: 1px solid var(--mainColor-light);
  box-shadow: inset 0 6px 6px var(--mainColor-darker-trans-lo);
  font-size: var(--text-4);
}

.chatWrapper .inviteDraft .inviteDraftText {
  color: var(--mainColor-lightest);
}

.chatWrapper .inviteDraft .inviteSeat,
.chatWrapper .inviteDraft .inviteSend,
.chatWrapper .inviteDraft .inviteClear {
  background: none;
  border: 0;
  padding: 0.1rem 0.4rem;
  font: inherit;
  border-radius: var(--radius-2);
  color: var(--mainColor-lighter);
}

/* A named chair is the run's, so it wears the same colour the challenge card
   uses for a live ask; the two verbs that end the draft stay neutral. */
.chatWrapper .inviteDraft .inviteSeat {
  color: var(--accentColor);
}

.chatWrapper .inviteDraft .inviteSend,
.chatWrapper .inviteDraft .inviteClear {
  font-weight: 700;
  text-transform: uppercase;
}

.chatWrapper .inviteDraft .inviteSeat:hover,
.chatWrapper .inviteDraft .inviteSeat:focus-visible,
.chatWrapper .inviteDraft .inviteSend:hover,
.chatWrapper .inviteDraft .inviteSend:focus-visible,
.chatWrapper .inviteDraft .inviteClear:hover,
.chatWrapper .inviteDraft .inviteClear:focus-visible {
  cursor: pointer;
  background-color: var(--mainColor-dark-trans-lo);
}

.chatWrapper .chatInput {
  display: flex;
  flex-flow: row nowrap;
  align-items: stretch;
}

.chatWrapper .chatInput .emojiButton {
  flex-shrink: 0;
  width: var(--chatbar-h);
  font-size: var(--text-7);
  background-image: linear-gradient(100deg, var(--mainColor-dark), var(--mainColor-dark) 20%, var(--mainColor));
  box-shadow: inset 0 8px 18px var(--mainColor-darker-trans-lo);
}

.chatWrapper .chatInput .emojiButton:hover {
  cursor: pointer;
  filter: brightness(1.3);
}

.chatWrapper .chatInput .sendButton {
  flex-shrink: 0;
  width: var(--chatbar-h);
  display: grid;
  place-items: center;
  color: var(--mainColor-lighter);
  background-image: linear-gradient(100deg, var(--mainColor), var(--mainColor-dark) 80%, var(--mainColor-dark));
  box-shadow: inset 0 8px 18px var(--mainColor-darker-trans-lo);
}

.chatWrapper .chatInput .sendButton:hover {
  cursor: pointer;
  color: var(--accentColor);
  filter: brightness(1.3);
}

.chatWrapper .chatInput .sendButton .icon {
  display: block;
}

.chatWrapper .matchesWrapper {
  position: absolute;
  left: 0;
  right: 0;
  bottom: calc(var(--chatbar-h) + 12px);
  z-index: 21;
  background-color: var(--mainColor-dark);
  border-top: 1px solid var(--mainColor-light);
  box-shadow: inset 0 6px 6px var(--mainColor-darker-trans-lo);
  font-size: var(--text-4);
  overflow: hidden;
}

.chatWrapper .matchesWrapper header {
  padding: 1rem 1.25rem 0;
  margin-bottom: 1rem;
  text-transform: uppercase;
  font-weight: 700;
  font-size: var(--text-5);
  color: var(--white);
}

.chatWrapper .matchesWrapper header span {
  color: var(--mainColor-lighter);
  text-transform: none;
}

.chatWrapper .matchesWrapper .matchesList {
  max-height: 20rem;
  overflow: auto;
}

.chatWrapper .matchesWrapper .matchesList .match {
  display: flex;
  flex-flow: row nowrap;
  align-items: center;
  padding: 0.5rem 1.25rem;
  color: var(--mainColor-lightest-trans-md);
}

.chatWrapper .matchesWrapper .matchesList .match.active {
  cursor: pointer;
  color: var(--white);
  background-color: var(--mainColor);
}

.chatWrapper .matchesWrapper .matchesList .match .command {
  font-weight: 700;
}

.chatWrapper .matchesWrapper .matchesList .match .description {
  margin-left: 0.5rem;
  font-weight: 400;
}

.chatWrapper .chatInput input {
  flex-grow: 1;
  min-width: 0;
  width: auto;
}

.usersListToolbar.compact .userItem {
  padding-top: 0.1rem;
  padding-bottom: 0.1rem;
}

.usersListToolbar.compact .usersListWrapper {
  padding-top: 0.75rem;
}

.usersEmptyNote {
  padding: 0.5rem 1rem;
  font-size: var(--text-4);
  color: var(--mainColor-lighter);
  text-align: center;
}

/* The close button of a full-pane view (find channels, the console). One
   drawing for every view that has one: a view that draws its own close
   button ends up with it a few pixels off, and the eye notices. */
.viewClose {
  margin-right: 1rem;
  font-size: var(--text-8);
  line-height: var(--leading-none);
  color: var(--mainColor-lighter);
}

.viewClose:hover {
  color: var(--white);
}

.browserBody {
  flex-grow: 1;
  display: flex;
  flex-direction: column;
  padding: 1.5rem 2rem;
  gap: 1rem;
  overflow: hidden;
  min-height: 0;
}

.browserBody .browserFilter {
  max-width: 420px;
  background-color: var(--mainColor-dark);
}

.browserBody .browserTopRow {
  display: flex;
  gap: 8px;
  align-items: center;
}

.browserBody .browserTopRow .browserFilter {
  flex-grow: 1;
  max-width: none;
}

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

/* The select: the browser's drawing, shared with every surface that needs
   one. A surface that draws its own select is a select that does not match
   the one beside it. */
.appSelect {
  background-color: var(--mainColor-dark);
  color: var(--mainColor-lighter);
  border: 1px solid var(--mainColor-light);
  border-radius: var(--radius-3);
  padding: 6px 8px;
  font-size: var(--text-4);
  color-scheme: dark;
}

.browserBody .browserSelect {
  text-transform: capitalize;
}

.browserBody .browserPager {
  display: flex;
  align-items: center;
  gap: 12px;
}

.browserBody .browserRange {
  color: var(--mainColor-lighter);
  font-size: var(--text-4);
}

.browserBody .browserPageBtn {
  padding: 6px 12px;
  border: 1px solid var(--mainColor-light);
  border-radius: var(--radius-3);
  font-size: var(--text-4);
  font-weight: 700;
}

.browserBody .browserPageBtn:disabled {
  opacity: 0.4;
  cursor: default;
}

.browserBody .browserPageBtn:not(:disabled):hover {
  border-color: var(--accentColor);
  color: var(--white);
}

.browserBody .browserGrid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(170px, 1fr));
  gap: 12px;
  overflow-y: auto;
  min-height: 0;
  padding: 4px 2px;
}

/* The grid's status is one panel-wide message, not a card-sized cell. */
.browserBody .browserGrid .panelStatus {
  grid-column: 1 / -1;
}

/* Channel preview cards (anatomy mirrors the original: thumbnail with
 * overlaid presence, name/system line, hover action overlay, hack ribbon).
 * Look is ours — flat dark gray, silver accent. */
.channelPreviewWrapper {
  position: relative;
  border-radius: var(--radius-3);
  text-align: center;
  transition: transform 0.1s ease-out;
  background-image: linear-gradient(var(--mainColor-dark) 40%, var(--mainColor-dark-trans-lo));
  z-index: 1;
}

.channelPreviewWrapper:hover {
  box-shadow: 0 0 10px var(--mainColor-darker-trans-hi);
}

.channelPreviewWrapper .thumbnail {
  position: relative;
  display: flex;
  flex-flow: column nowrap;
  justify-content: flex-start;
  align-items: center;
  width: 100%;
  height: 8rem;
  box-shadow: 0 6px 6px var(--mainColor-darker-trans-lo);
  border-radius: var(--radius-3) var(--radius-3) 0 0;
  background-size: cover;
  background-position: 50%;
  background-color: var(--mainColor);
  overflow: hidden;
}

/* Our captures are the cabinets' own resolutions (384×224 and up), so an
 * intrinsic-size image would sit clipped in the fixed-height frame. Fill it
 * and keep the pixels crisp, the same contract the rail's circles use. */
.channelPreviewWrapper .thumbnail img.channelArt {
  width: 100%;
  height: 100%;
  object-fit: cover;
  image-rendering: crisp-edges;
}

.channelPreviewWrapper .noPreview {
  position: absolute;
  inset: 0;
  display: flex;
  justify-content: center;
  align-items: center;
  padding: 0.5rem;
  background-color: var(--mainColor);
  box-shadow: inset 0 0 80px var(--mainColor-dark);
  color: var(--mainColor-lighter);
  font-weight: 700;
  font-size: var(--text-4);
  text-transform: uppercase;
}

.channelPreviewWrapper .channelExtraInfoWrapper {
  position: absolute;
  top: 0;
  left: 0;
  right: 0;
  display: flex;
  justify-content: center;
  z-index: 2;
}

.channelPreviewWrapper .channelExtraInfo {
  display: flex;
  flex-flow: row nowrap;
  align-items: center;
  gap: 8px;
  margin-top: 6px;
  padding: 2px 10px;
  border-radius: var(--radius-5);
  background-color: var(--mainColor-darker-trans-hi);
  font-size: var(--text-3);
  font-weight: 700;
}

.channelPreviewWrapper .favStatus {
  color: var(--accentColor);
  line-height: var(--leading-none);
}

.channelPreviewWrapper .num-users {
  display: flex;
  align-items: center;
  gap: 4px;
}

.channelPreviewWrapper .num-users .usersIconSvg {
  fill: var(--mainColor-lightest-trans-hi);
}

.channelPreviewWrapper .rankedStatus {
  display: flex;
  fill: var(--accentColor);
}

.channelPreviewWrapper .channelInfoWrapper {
  padding: 8px 8px 10px;
}

.channelPreviewWrapper .channelInfoWrapper .name {
  font-weight: 700;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.channelPreviewWrapper .channelInfoWrapper .system {
  color: var(--mainColor-lighter);
  font-size: var(--text-3);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.channelPreviewWrapper .channelActions {
  display: flex;
  flex-flow: column nowrap;
  justify-content: center;
  align-items: center;
  gap: 8px;
  position: absolute;
  border-radius: var(--radius-3);
  top: 0;
  width: 100%;
  height: 100%;
  background-image: radial-gradient(var(--mainColor-dark-trans-lo) 30%, var(--mainColor-darker));
  opacity: 0;
  transition: opacity 0.1s ease-out;
  z-index: 10;
}

.channelPreviewWrapper:hover .channelActions,
.channelPreviewWrapper:focus-within .channelActions {
  opacity: 1;
}

.channelPreviewWrapper .channelSubActions {
  display: flex;
  flex-flow: row nowrap;
  gap: 8px;
  justify-content: center;
  align-items: center;
}

.channelPreviewWrapper .channelActions .joinButton {
  min-width: 100px;
}

/* The card overlay is already dark, so the big white buttons the global
   .button-generic paints disappear into nothing on it: two loud rectangles
   that carry none of the theme. Here they are dark chips that light up with
   the accent instead — same shape, same press, the overlay's own palette. */
.channelPreviewWrapper .channelActions .button-generic {
  background-image: none;
  background-color: var(--mainColor-darker-trans-hi);
  color: var(--mainColor-lightest);
  box-shadow:
    inset 0 0 0 1px var(--mainColor-lighter),
    0 0 12px var(--mainColor-darker-trans-hi);
}

.channelPreviewWrapper .channelActions .button-generic:hover {
  color: var(--white);
  box-shadow:
    inset 0 0 0 1px var(--accentColor),
    0 0 12px var(--mainColor-darker-trans-hi);
}

.channelPreviewWrapper .channelActions .button-generic:active {
  box-shadow:
    inset 0 0 0 1px var(--accentColor),
    inset 0 0 12px var(--mainColor-darker-trans-hi);
}

.channelPreviewWrapper .ribbon {
  position: absolute;
  top: 8px;
  right: -4px;
  z-index: 11;
  padding: 2px 10px;
  background-color: var(--errorColor);
  /* Dark ink on the red chip: reds this dark cannot carry small white text at
     AA (3.8), but the same red under dark text clears it (5.2). */
  color: var(--mainColor-darker);
  font-size: var(--text-2);
  font-weight: 700;
  text-transform: uppercase;
  border-radius: var(--radius-2) 0 0 var(--radius-2);
  pointer-events: none;
}

/* ---- landing (welcome/about when no channel is active) ---- */

.landingBody {
  flex-grow: 1;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 16px;
  padding: 2rem;
  text-align: center;
  overflow-y: auto;
}

.landingLogo {
  width: 120px;
  opacity: 0.9;
}

.landingTitle {
  font-size: var(--text-8);
  font-weight: 700;
  margin: 0;
}

.landingText {
  max-width: 460px;
  color: var(--mainColor-lighter);
  line-height: var(--leading-body);
  margin: 0;
}

.landingAction {
  padding: 10px 28px;
  border: 2px solid var(--mainColor-light);
  border-radius: var(--radius-2);
  color: var(--mainColor-lightest-trans-md);
  font-weight: 700;
  text-transform: uppercase;
}

.landingAction:hover {
  border-color: var(--accentColor);
  color: var(--white);
}

/* ---- endgame result card + match report form ---- */

.result-message {
  display: flex;
  justify-content: center;
}

.resultContainer {
  display: inline-block;
  padding: 10px 18px;
  background-color: var(--mainColor-dark);
  border: 1px solid var(--mainColor-light);
  border-radius: var(--radius-4);
  text-align: center;
}

.resultScore {
  font-size: var(--text-6);
}

.resultMeta {
  margin-top: 4px;
  color: var(--mainColor-lighter);
  font-size: var(--text-3);
}

.resultWatchers {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 5px;
  margin-top: 4px;
  color: var(--mainColor-lighter);
  font-size: var(--text-3);
}

.resultWatchers .eyeIconSvg {
  width: 14px;
  height: 14px;
  flex-shrink: 0;
  fill: var(--mainColor-lighter);
}

.resultReplay {
  margin-top: 6px;
  font-size: var(--text-3);
}

.resultReplay a {
  color: var(--accentColor);
  text-decoration: none;
  word-break: break-all;
}

.resultReplay a:hover {
  text-decoration: underline;
}

.matchReport {
  padding: 8px 12px 10px;
  border-top: 1px solid var(--mainColor-light);
  display: flex;
  flex-direction: column;
  gap: 8px;
}

.matchReportRow {
  display: flex;
  align-items: center;
  gap: 8px;
}

.matchReportWinner {
  padding: 4px 12px;
  border: 1px solid var(--mainColor-light);
  border-radius: var(--radius-3);
  font-size: var(--text-4);
}

.matchReportWinner.active {
  border-color: var(--accentColor);
  color: var(--white);
}

.matchReportScore {
  width: 64px;
  background-color: var(--mainColor-dark);
  color: var(--mainColor-lighter);
  border: 1px solid var(--mainColor-light);
  border-radius: var(--radius-3);
  padding: 4px 6px;
  font-size: var(--text-4);
}

.matchReportDash {
  color: var(--mainColor-lighter);
}

.matchReportSubmit,
.matchReportCancel {
  padding: 4px 12px;
  border: 1px solid var(--mainColor-light);
  border-radius: var(--radius-3);
  font-size: var(--text-3);
  font-weight: 700;
  text-transform: uppercase;
}

.matchReportSubmit:hover {
  border-color: var(--accentColor);
  color: var(--white);
}

.matchReportError {
  color: var(--errorColor);
  font-size: var(--text-3);
}

/* ---- toasts ---- */

/* The stack is one corner holding two live regions. The wrapper owns the
   position, the width and the console clearance; each region owns one severity,
   because a screen reader is asked to wait for news and to interrupt for a
   failure and one element cannot do both. Both are bottom-anchored, so the
   newest row stands nearest the corner whichever kind it is. An empty region
   has no box at all, so a region nobody is reading captures no click. */
.toastStack {
  position: fixed;
  right: 16px;
  bottom: 16px;
  display: flex;
  flex-direction: column;
  align-items: flex-end;
  gap: 8px;
  /* The stack is narrow on purpose: it sits over the work, so it takes as
     little of it as it can. */
  max-width: 20rem;
  z-index: 50;
}

.toastRun {
  display: flex;
  flex-direction: column;
  align-items: flex-end;
  gap: 8px;
}

.toast {
  display: flex;
  align-items: flex-start;
  gap: 10px;
  padding: 10px 14px;
  background: var(--mainColor-dark);
  border: 1px solid var(--mainColor-light);
  border-radius: var(--radius-4);
  cursor: pointer;
}

.toastText {
  /* The sentence takes the row's width; the close control never pushes it onto
     more lines than the text needs. */
  flex: 1 1 auto;
}

/* The keyboard's way out of a notice. It is a real button, so it is Tab-reachable
   and Enter/Space dismiss it; the row keeps its own click for a mouse. */
.toastClose {
  flex: 0 0 auto;
  padding: 0 2px;
  font: inherit;
  font-size: var(--text-6);
  line-height: var(--leading-none);
  color: var(--accentColor);
  background: none;
  border: 0;
  cursor: pointer;
}

/* The two severities are not told apart by a hue alone, because a colour a
   reader cannot see carries no meaning for them: an error wears a mark that says
   what it is, an edge that clears the 3:1 a non-text object owes, and it stays
   until dismissed while news times out. The numbers are measurements, not
   impressions (python3 tools/contrast_check.py): --errorColor #e94c3a on this
   plate #16181c is 4.71:1, and the ring --accentColor #c9d1d9 draws globally on
   keyboard focus is 11.52:1 on the plate and 12.60:1 on the window behind it.
   The plate's own 1px edge (--mainColor-light #33373d, 1.49:1) stays decorative:
   it frames nothing a reader has to find. */
.toast.is-error {
  border-left: 3px solid var(--errorColor);
}

.toastMark {
  flex: 0 0 auto;
  font-weight: 700;
  color: var(--errorColor);
}

/* The console's decision bar sits at the foot of its pane, and the toast
   stack stands at the foot of the window: in the console the stack rises
   above the bar, so a message never covers the buttons a verdict is given
   with. */
.toastStack.is-console {
  bottom: 10rem;
}

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

/* The badge itself: a plate with a word, the way the emulator's input display
   draws its own two badges. */
.cardBadge {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  height: 0.95rem;
  padding: 0 0.2rem;
  font-size: var(--text-1);
  font-weight: 700;
  line-height: var(--leading-none);
  letter-spacing: 0.06em;
  text-transform: uppercase;
  border-radius: var(--radius-1);
  cursor: default;
  user-select: none;
}

.cardBadge.is-turbo {
  color: var(--accentColor2);
  background-color: var(--accentColor2-trans-lo);
  border: 1px solid var(--accentColor2);
}

/* The clear-opposites badge wears the fork's own plate: white on the dark
   red the emulator's input display draws it on. */
.cardBadge.is-socd {
  color: var(--white);
  background-color: var(--badgeSocdPlate);
  border: 1px solid var(--badgeSocdPlate);
}

/* The profile's card rows: the game, the badges the emulator measured there,
   and the characters the player used the most. */
.overlay.settings .overlayPanel .profileView .profileCards {
  list-style: none;
  margin: 0;
  padding: 0;
  font-size: var(--text-4);
}

.overlay.settings .overlayPanel .profileView .profileCardRow {
  display: flex;
  flex-flow: row nowrap;
  align-items: center;
  gap: 0.5rem;
  padding: 0.35rem 0;
  border-top: 1px solid var(--mainColor-dark);
}

.overlay.settings .overlayPanel .profileView .profileCardChannel {
  flex: 0 0 auto;
  min-width: 5rem;
  font-weight: 700;
  color: var(--mainColor-lighter);
}

.overlay.settings .overlayPanel .profileView .profileCardBadges {
  flex: 0 0 auto;
  display: inline-flex;
  gap: 0.25rem;
  min-width: 5.5rem;
}

.overlay.settings .overlayPanel .profileView .profileCardCharacters {
  flex: 1 1 auto;
  min-width: 0;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  color: var(--mainColor-lighter);
}

/* The card button in the room's toolbar: the card belongs to the game the
   room is about, so it lives beside the game's name. */
.channelWrapper .channelToolbar .channelInfo .gameCardButton {
  display: inline-flex;
  align-items: center;
  gap: var(--control-gap);
  justify-content: center;
  margin-left: 0.75rem;
  min-height: var(--control-h-sm);
  padding: 0 var(--control-pad-x-sm);
  font-size: var(--control-font-sm);
  line-height: var(--leading-tight);
  font-weight: 700;
  text-transform: uppercase;
  color: var(--mainColor-lighter);
  background: transparent;
  border: 1px solid var(--mainColor-light);
  border-radius: var(--radius-2);
  cursor: pointer;
  white-space: nowrap;
  transition:
    color 0.2s ease-out,
    border-color 0.2s ease-out;
}

.channelWrapper .channelToolbar .channelInfo .gameCardButton:hover {
  color: var(--white);
  border-color: var(--mainColor-lightest-trans-lo);
}

.channelWrapper .channelToolbar .channelInfo .gameCardButton.active {
  color: var(--white);
  border-color: var(--accentColor);
}

/* Turbo on: the button says so before the panel is even opened. */
.channelWrapper .channelToolbar .channelInfo .gameCardButton.turbo {
  color: var(--accentColor2);
  border-color: var(--accentColor2);
}

.channelWrapper .channelToolbar .channelInfo .gameCardButton .gameCardIcon {
  width: 1em;
  height: 1em;
  fill: currentColor;
}

/* The card panel: a form, so it reads left-aligned and narrow. */
.overlay.gamecard {
  --panel-w: 21rem;
  --panel-align: left;
}

.overlay.gamecard .overlayPanel {
  font-size: var(--text-5);
}

.overlay.gamecard .overlayPanel .panelHead {
  padding-bottom: 0.5rem;
  border-bottom: 1px solid var(--mainColor-light);
}

.overlay.gamecard .overlayPanel .panelHead .panelGame {
  color: var(--mainColor-lightest-trans-hi);
  text-transform: none;
  font-weight: 400;
}

.overlay.gamecard .overlayPanel .cardSection {
  padding: 1rem 2rem 0;
  margin-bottom: 1.5rem;
}

.overlay.gamecard .overlayPanel .cardLabel {
  color: var(--mainColor-lighter);
  text-transform: uppercase;
  font-weight: 700;
  font-size: var(--text-3);
  letter-spacing: 0.04em;
  margin-bottom: 0.75rem;
  user-select: none;
}

.overlay.gamecard .overlayPanel .cardHint {
  margin: 0.6rem 0 0;
  font-size: var(--text-3);
  line-height: var(--leading-body);
  color: var(--mainColor-lighter);
}

/* The badges the emulator measured, and the characters it counted: the card
   as it stands, not a form. */
.overlay.gamecard .overlayPanel .cardBadgesRow {
  display: flex;
  flex-flow: row nowrap;
  gap: 0.4rem;
  min-height: 0.95rem;
}

.overlay.gamecard .overlayPanel .cardCharactersRow {
  display: flex;
  flex-flow: row wrap;
  gap: 0.4rem;
}

.overlay.gamecard .overlayPanel .cardCharacter {
  padding: 0.2rem 0.5rem;
  font-size: var(--text-4);
  font-weight: 700;
  color: var(--mainColor-lighter);
  background-color: var(--mainColor-dark);
  border: 1px solid var(--mainColor-light);
  border-radius: var(--radius-2);
}

.overlay.gamecard .overlayPanel .cardEmpty {
  font-size: var(--text-4);
  color: var(--mainColor-lighter);
}

.overlay.gamecard .overlayPanel .cardActions {
  /* The panel scrolls, and the actions are the way out of it: they stay at
     the bottom of the window, so a short window never buries them under the
     roster the player is picking from. */
  position: sticky;
  bottom: 0;
  display: flex;
  flex-flow: row nowrap;
  justify-content: space-between;
  gap: 0.75rem;
  padding: 0.75rem 2rem;
  margin-top: 0.5rem;
  border-top: 1px solid var(--mainColor-light);
  background-color: var(--mainColor-dark);
}

/* The content scrolls under the bar, so the bar carries a fade: a line of
   text that sinks into it reads as scrolled under, and one that stops at a
   hard edge reads as broken. */
.overlay.gamecard .overlayPanel .cardActions::before {
  content: '';
  position: absolute;
  left: 0;
  right: 0;
  bottom: 100%;
  height: 20px;
  background: linear-gradient(to top, var(--mainColor-dark), rgba(22, 24, 28, 0));
  pointer-events: none;
}

.overlay.gamecard .overlayPanel .cardActions button {
  min-height: var(--control-h-md);
  padding: 0 var(--control-pad-x-md);
  font-size: var(--control-font-md);
  font-weight: 700;
  text-transform: uppercase;
  border: 1px solid var(--mainColor-light);
  border-radius: var(--radius-2);
  color: var(--mainColor-lighter);
}

.overlay.gamecard .overlayPanel .cardActions button:hover {
  color: var(--white);
  border-color: var(--mainColor-lightest-trans-lo);
}

.overlay.gamecard .overlayPanel .cardActions .cardClose {
  color: var(--mainColor-dark);
  background-color: var(--accentColor);
  border-color: var(--accentColor);
}

.overlay.gamecard .overlayPanel .cardActions .cardClose:hover {
  color: var(--mainColor-dark);
  background-color: var(--white);
  border-color: var(--white);
}

/* The flat face of the control scale: a bordered control in the app's own
   tokens, the shape the app's input already has, at the md step.
   The app's buttons are two faces across three steps, and this is one of the
   faces: the slab (.button-generic) is the loud act a screen wants the eye on,
   this is a control that sits beside an input and lines up with it — without
   the slab's shadow and its margins (the slab carries a bottom margin for a
   block of its own, which throws a row out of line). The third thing this
   comment used to claim — that the app had "two button weights" — was not true
   of the tree it described: .matchHandoffButton, .cardActions button,
   .connectionRetry and the slab's four per-callsite font sizes were four more
   treatments of the same concept, and \#121 U8 put them on this scale.
   What a row of plain-text buttons still cannot do is hold "Filter Clear
   Search": a row of links reads as links, so the bar of acts wears controls. */
.appButton {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--control-gap);
  min-height: var(--control-h-md);
  padding: 0 var(--control-pad-x-md);
  font: inherit;
  font-size: var(--control-font-md);
  font-weight: 600;
  line-height: var(--leading-tight);
  color: var(--mainColor-lightest);
  background-color: var(--mainColor-dark);
  border: 1px solid var(--mainColor-light);
  border-radius: var(--radius-2);
  cursor: pointer;
  transition:
    color 0.15s ease-out,
    border-color 0.15s ease-out,
    background-color 0.15s ease-out;
}

.appButton:hover:not(:disabled) {
  color: var(--white);
  border-color: var(--mainColor-lighter);
  background-color: var(--mainColor);
}

.appButton:disabled {
  opacity: 0.45;
  cursor: not-allowed;
}

/* The one act a surface wants the eye on: the app's silver accent, flat. */
.appButton.is-primary {
  color: var(--mainColor-dark);
  background-color: var(--accentColor);
  border-color: var(--accentColor);
}

.appButton.is-primary:hover:not(:disabled) {
  color: var(--mainColor-dark);
  background-color: var(--white);
  border-color: var(--white);
}

/* The act that is not undone by a click: the app's alert colour on the flat
   control, so the button that closes an account is marked the way the row
   that ends the session is marked. */
.appButton.is-danger {
  color: var(--errorColor);
  border-color: var(--errorColor);
}

.appButton.is-danger:hover:not(:disabled) {
  /* The plate is the mid-tone alert colour, and white on it is 3.78:1 — under
     the bar the face's own 14px label has to clear.  The darkest ink the theme
     owns is what reads on it (5.15:1), the same way the silver plate takes the
     dark ink above. */
  color: var(--mainColor-darker);
  background-color: var(--errorColor);
  border-color: var(--errorColor);
}

/* ---- the console ------------------------------------------------------- */

/* One frame for the six sections: a head that says what the screen is, a
   toolbar with that section's own filters, and a body that is a list beside a
   detail. The sections differ in what they show, not in how they are put
   together - a worklist and a log are read the same way, and a console whose
   sections each draw their own layout is a console with six designs. */
.consolePage {
  display: flex;
  flex-direction: column;
  gap: 0.6rem;
  min-height: 0;
  flex: 1;
}

.consolePageHead {
  display: flex;
  align-items: baseline;
  gap: 0.6rem;
  flex-wrap: wrap;
}

.consolePageTitle {
  font-size: var(--text-6);
  font-weight: 700;
  color: var(--mainColor-lightest);
}

.consolePagePurpose {
  font-size: var(--text-4);
  color: var(--mainColor-lighter);
}

/* The count beside a title: how much there is. */
.consoleCount {
  padding: 0.05rem 0.4rem;
  font-size: var(--text-2);
  font-weight: 700;
  color: var(--mainColor-lighter);
  background-color: var(--mainColor-darker);
  border-radius: var(--radius-pill);
}

.consoleToolbar {
  display: flex;
  align-items: center;
  gap: 0.4rem;
  flex-wrap: wrap;
}

.consoleBody {
  display: flex;
  gap: 0.75rem;
  min-height: 0;
  flex: 1;
}

.consoleList {
  flex: 1 1 55%;
  min-width: 0;
  overflow-y: auto;
}

.consoleDetail {
  flex: 1 1 45%;
  min-width: 0;
  overflow-y: auto;
  padding-left: 0.75rem;
  border-left: 1px solid var(--mainColor-darker);
}

.consoleRows {
  display: flex;
  flex-direction: column;
  gap: 0.4rem;
}

/* A row of the list: the whole row is the target, so it is a button. */
.consoleRow {
  display: flex;
  flex-direction: column;
  gap: 0.25rem;
  width: 100%;
  padding: 0.5rem 0.6rem;
  font: inherit;
  text-align: left;
  color: var(--mainColor-lighter);
  background-color: var(--mainColor-dark);
  border: 1px solid var(--mainColor-light);
  border-radius: var(--radius-2);
  cursor: pointer;
  transition:
    border-color 0.15s ease-out,
    background-color 0.15s ease-out;
}

.consoleRow:hover {
  border-color: var(--mainColor-lighter);
  background-color: var(--mainColor);
}

/* The row that is open in the detail pane. */
.consoleRow.is-open {
  border-color: var(--accentColor);
  box-shadow: inset 2px 0 0 var(--accentColor);
}

.consoleRowTop {
  display: flex;
  align-items: baseline;
  gap: 0.5rem;
}

.consoleRowSubject {
  font-size: var(--text-6);
  font-weight: 600;
  color: var(--mainColor-lightest);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.consoleRowMeta {
  margin-left: auto;
  flex-shrink: 0;
  font-size: var(--text-3);
  color: var(--mainColor-lighter);
}

/* The line under the subject: what the case is about, in one line. */
.consoleRowEvidence {
  font-size: var(--text-4);
  color: var(--mainColor-lighter);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* The chips: the kind of case, the claim, the state. */
.consoleChip {
  flex-shrink: 0;
  padding: 0.05rem 0.35rem;
  font-size: var(--text-1);
  font-weight: 700;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--mainColor-lighter);
  background-color: var(--mainColor-dark);
  border: 1px solid var(--mainColor-light);
  border-radius: var(--radius-2);
}

.consoleChip.is-kind {
  color: var(--mainColor-light);
  border-color: var(--mainColor-light);
}

/* The claim is a work state, so it is said plainly: whose work it is. */
.consoleChip.is-mine {
  color: var(--accentColor);
  border-color: var(--accentColor);
}

.consoleChip.is-taken {
  color: var(--mainColor-lighter);
}

/* The verdict a case stands in. */
.consoleState {
  flex-shrink: 0;
  padding: 0.05rem 0.35rem;
  font-size: var(--text-1);
  font-weight: 700;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  border-radius: var(--radius-2);
  border: 1px solid currentColor;
}

.consoleState.is-open {
  color: var(--mainColor-light);
}

.consoleState.is-dismissed {
  color: var(--mainColor-lighter);
}

.consoleState.is-warned {
  color: var(--state-away);
}

.consoleState.is-deactivated {
  color: var(--errorColor);
}

.consoleState.is-corrected {
  color: var(--accentColor);
}

/* The detail pane: the head, the blocks, and the action bar at the foot. */
.consoleDetailHead {
  display: flex;
  align-items: baseline;
  gap: 0.5rem;
  flex-wrap: wrap;
  padding-bottom: 0.4rem;
  border-bottom: 1px solid var(--mainColor-darker);
}

.consoleDetailName {
  font-size: var(--text-6);
  font-weight: 700;
  color: var(--mainColor-lightest);
}

.consoleBlock {
  display: flex;
  flex-direction: column;
  gap: 0.3rem;
  margin-top: 0.75rem;
}

.consoleBlockLabel {
  font-size: var(--text-2);
  font-weight: 700;
  letter-spacing: 0.05em;
  text-transform: uppercase;
  color: var(--mainColor-lighter);
}

.consoleQuote {
  margin: 0;
  padding: 0.4rem 0.55rem;
  font-size: var(--text-4);
  color: var(--mainColor-lightest);
  background-color: var(--mainColor-dark);
  border-left: 2px solid var(--mainColor-lighter);
  border-radius: 0 var(--radius-2) var(--radius-2) 0;
  overflow-wrap: anywhere;
}

/* The action bar: the reason, the closure and the verdicts, together, at
   the foot of the case they belong to. A reason written above the list
   belongs to whichever case is clicked next, and a reason that lands on the
   wrong case is a wrong record. */
.consoleActionBar {
  position: sticky;
  bottom: 0;
  display: flex;
  flex-direction: column;
  gap: 0.5rem;
  margin-top: 0.9rem;
  padding: 0.6rem 0 0.2rem;
  border-top: 1px solid var(--mainColor-darker);
  background-color: var(--mainColor-dark);
}

.consoleActionBar::before {
  content: '';
  position: absolute;
  left: 0;
  right: 0;
  bottom: 100%;
  height: 18px;
  background: linear-gradient(to top, var(--mainColor-dark), rgba(22, 24, 28, 0));
  pointer-events: none;
}

.consoleActionRow {
  display: flex;
  align-items: center;
  gap: 0.4rem;
  flex-wrap: wrap;
}

.consoleActionNote {
  font-size: var(--text-3);
  color: var(--mainColor-lighter);
}

/* The console's fields and buttons are the app's: the global input and the
   shared select, the plain-text button for a quiet act and the loud slab for
   the one act a surface wants the eye on. A surface that draws its own
   controls is a surface that does not match the one beside it.
   `.fieldGrow` is the only thing the console adds: a field that takes the
   room the row has left. */
.fieldGrow {
  flex: 1 1 10rem;
  min-width: 0;
}

/* The filter row's fields: one rule for all of them, so the row reads as a
   row. The fields share what the row has left, between a measure that fits
   a name and a measure that does not waste the window; the segmented answer
   beside them has a fixed width of its own, so the fields do not dance when
   the answer changes. */
.consoleFilter {
  flex: 1 1 9rem;
  min-width: 9rem;
  max-width: 16rem;
}

/* The segmented control: one of several answers, in one place. The console's
   claim filter and the input screen's device choice are the same control —
   the chosen one is drawn in bold, and a bold word is a wider word, so the
   answers share one width: without it the row under the cursor moves every
   time the answer changes, and a bar that moves is a bar that makes you
   click the wrong thing. */
.segmented {
  display: inline-flex;
  flex: 0 0 auto;
  border: 1px solid var(--mainColor-darker);
  border-radius: var(--radius-2);
  overflow: hidden;
}

.segmented button {
  flex: 0 0 auto;
  width: 6rem;
  min-height: var(--control-h-sm);
  padding: 0 var(--control-pad-x-sm);
  font: inherit;
  font-size: var(--control-font-sm);
  line-height: var(--leading-tight);
  color: var(--mainColor-lighter);
  background: transparent;
  border: none;
  border-right: 1px solid var(--mainColor-darker);
  white-space: nowrap;
  cursor: pointer;
}

.segmented button:last-child {
  border-right: none;
}

.segmented button:hover {
  background-color: var(--mainColor-darker);
}

.segmented button.is-on {
  color: var(--mainColor-dark);
  background-color: var(--accentColor);
  font-weight: 700;
}

/* The timeline: the log is a story, so it reads as one. */
.consoleTimeline {
  display: flex;
  flex-direction: column;
  gap: 0.35rem;
  /* A story is read at a readable measure; a line that runs the width of the
     window puts the moment a whole screen away from the action. */
  max-width: 46rem;
}

.consoleTimelineRow {
  display: flex;
  flex-direction: column;
  gap: 0.1rem;
  padding: 0.4rem 0.55rem;
  background-color: var(--mainColor-dark);
  border-left: 2px solid var(--mainColor-lighter);
  border-radius: 0 var(--radius-2) var(--radius-2) 0;
}

.consoleTimelineHead {
  display: flex;
  align-items: baseline;
  gap: 0.5rem;
  font-size: var(--text-4);
}

.consoleTimelineAction {
  font-weight: 700;
  color: var(--mainColor-lightest);
}

.consoleTimelineBy {
  color: var(--mainColor-lighter);
  font-size: var(--text-4);
}

.consoleTimelineTime {
  margin-left: auto;
  color: var(--mainColor-lighter);
  font-size: var(--text-3);
}

.consoleTimelineReason {
  font-size: var(--text-4);
  color: var(--mainColor-lighter);
}
