/* ==========================================================================
   style.css — design tokens, app shell, screens, controls, modals, toasts
   ========================================================================== */

:root {
  /* Surfaces */
  --bg: #0f1115;
  --bg-glow: #1c2130;
  --bg-elevated: #161922;
  --panel: #1b1f2a;
  --panel-soft: #212636;
  --border: #2a3040;
  --border-strong: #3a4257;

  /* Text */
  --text: #eef1f7;
  --text-muted: #98a2b6;
  --text-faint: #6b7488;

  /* Accents */
  --accent: #e8b44c;
  --accent-soft: rgba(232, 180, 76, 0.14);
  --accent-text: #1a1405;
  --danger: #e5594d;
  --danger-soft: rgba(229, 89, 77, 0.15);
  --success: #4fc98a;
  --info: #6ba7f0;

  /* Shape */
  --radius-sm: 8px;
  --radius: 14px;
  --radius-lg: 20px;

  /* Depth */
  --shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.4);
  --shadow: 0 6px 20px rgba(0, 0, 0, 0.35);
  --shadow-lg: 0 20px 50px rgba(0, 0, 0, 0.55);

  --font: "Inter", "Segoe UI", system-ui, -apple-system, "Helvetica Neue", Arial, sans-serif;

  /* Minimum comfortable touch target */
  --tap: 44px;
}

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

/*
   The `hidden` attribute is only `display: none` in the UA stylesheet, so any
   author `display` rule silently beats it. Several components here set
   `display` (.modal is grid, .btn is inline-flex), which would leave hidden
   modals as invisible full-screen click traps. Enforce it globally instead.
*/
[hidden] {
  display: none !important;
}

html,
body {
  margin: 0;
  padding: 0;
}

/* --------------------------------------------------------------------------
   Backgrounds.

   One block per id in BACKGROUNDS (js/config.js), set on the root element as
   `data-bg` — before first paint by the inline script in index.html, and again
   by the app once settings load.

   Each block redefines the SURFACE ladder only, and keeps the lightness steps
   the default sets: base, glow, elevated, panel, panel-soft, then the two
   borders. Text and accent tokens are deliberately not listed. They are what
   carry contrast against every one of these, and a background is no reason to
   renegotiate that — which is also what keeps adding a background to a block
   of seven values rather than an audit of the whole stylesheet.

   Midnight has a block like the others even though `:root` already says the
   same thing, and the duplication is deliberate. `:root` is the fallback for a
   page with no `data-bg` at all — script blocked, storage unreadable — while
   this block is what the attribute selects, and what a SWATCH needs: a swatch
   cannot inherit Midnight's values from `:root` while some other background is
   the one in force, which is exactly when it would be showing the wrong
   colours. The two must be changed together.
   -------------------------------------------------------------------------- */

:root[data-bg="midnight"],
.bg-swatch__preview[data-bg="midnight"] {
  --bg: #0f1115;
  --bg-glow: #1c2130;
  --bg-elevated: #161922;
  --panel: #1b1f2a;
  --panel-soft: #212636;
  --border: #2a3040;
  --border-strong: #3a4257;
}

:root[data-bg="charcoal"],
.bg-swatch__preview[data-bg="charcoal"] {
  --bg: #101012;
  --bg-glow: #202024;
  --bg-elevated: #17181b;
  --panel: #1d1e22;
  --panel-soft: #24262b;
  --border: #2e3036;
  --border-strong: #40434b;
}

:root[data-bg="forest"],
.bg-swatch__preview[data-bg="forest"] {
  --bg: #0b1410;
  --bg-glow: #16281f;
  --bg-elevated: #111d18;
  --panel: #16241e;
  --panel-soft: #1c2c25;
  --border: #26382f;
  --border-strong: #364d41;
}

:root[data-bg="mahogany"],
.bg-swatch__preview[data-bg="mahogany"] {
  --bg: #14100d;
  --bg-glow: #2a1e16;
  --bg-elevated: #1d1813;
  --panel: #241d17;
  --panel-soft: #2c241c;
  --border: #382c22;
  --border-strong: #4d3c2e;
}

body {
  min-height: 100vh;
  min-height: 100dvh;
  background:
    radial-gradient(1100px 600px at 50% -10%, var(--bg-glow) 0%, transparent 60%),
    var(--bg);
  color: var(--text);
  font-family: var(--font);
  font-size: 16px;
  line-height: 1.5;
  -webkit-font-smoothing: antialiased;
  -webkit-tap-highlight-color: transparent;
  overflow-x: hidden;
}

body.is-modal-open {
  overflow: hidden;
}

/* ==========================================================================
   Screens
   ========================================================================== */

.screen {
  display: none;
  min-height: 100vh;
  min-height: 100dvh;
  padding: env(safe-area-inset-top) env(safe-area-inset-right)
           env(safe-area-inset-bottom) env(safe-area-inset-left);
}

.screen.is-active {
  display: block;
}

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

.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 0.5rem;
  min-height: var(--tap);
  padding: 0.7rem 1.15rem;
  border: 1px solid var(--border-strong);
  border-radius: var(--radius);
  background: var(--panel-soft);
  color: var(--text);
  font-family: inherit;
  font-size: 0.95rem;
  font-weight: 600;
  letter-spacing: 0.01em;
  cursor: pointer;
  transition: background 0.15s ease, border-color 0.15s ease, transform 0.08s ease;
}

.btn:hover:not(:disabled) {
  background: #29304291;
  border-color: var(--border-strong);
}

.btn:active:not(:disabled) {
  transform: translateY(1px);
}

.btn:disabled {
  opacity: 0.4;
  cursor: not-allowed;
}

.btn--primary {
  background: linear-gradient(180deg, #f0c163, var(--accent));
  border-color: #f3cd83;
  color: var(--accent-text);
}

.btn--primary:hover:not(:disabled) {
  background: linear-gradient(180deg, #f5cd7d, #edbc5b);
}

.btn--ghost {
  background: transparent;
  border-color: var(--border);
  color: var(--text);
}

.btn--ghost:hover:not(:disabled) {
  background: var(--panel-soft);
}

.btn--block {
  width: 100%;
}

.btn[data-tone="danger"] {
  background: linear-gradient(180deg, #ec6d61, var(--danger));
  border-color: #ef7e73;
  color: #fff;
}

.btn__meta {
  display: block;
  width: 100%;
  margin-top: 0.15rem;
  color: var(--text-faint);
  font-size: 0.76rem;
  font-weight: 500;
}

.btn--ghost:has(.btn__meta) {
  flex-direction: column;
  gap: 0;
}

.icon-btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: var(--tap);
  height: var(--tap);
  padding: 0;
  border: 1px solid transparent;
  border-radius: var(--radius-sm);
  background: transparent;
  color: var(--text-muted);
  font-size: 1.25rem;
  cursor: pointer;
  transition: background 0.15s ease, color 0.15s ease;
}

.icon-btn:hover {
  background: var(--panel-soft);
  color: var(--text);
}

/*
  Drawn icons rather than text glyphs.

  `currentColor` is the whole point: the icon inherits the button's colour, so
  the hover and focus states it already had keep working with nothing added,
  which a colour-emoji glyph could never do.
*/
.icon {
  display: block;
  width: 22px;
  height: 22px;
  fill: currentColor;
  /* A drawn shape has no font metrics to sit on, so it is centred by the
     button's flexbox and needs no baseline nudging. */
  flex: none;
}

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

/* ==========================================================================
   Start menu
   ========================================================================== */

.menu {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  min-height: 100vh;
  min-height: 100dvh;
  max-width: 420px;
  margin: 0 auto;
  padding: 2rem 1.25rem;
  text-align: center;
}

.menu__logo {
  font-family:
    "Segoe UI Symbol", "Apple Symbols", "Noto Sans Symbols 2",
    "DejaVu Sans", "FreeSerif", sans-serif;
  font-variant-emoji: text;
  display: grid;
  place-items: center;
  width: 96px;
  height: 96px;
  margin-bottom: 1.5rem;
  border: 1px solid var(--border-strong);
  border-radius: 28px;
  background: linear-gradient(160deg, var(--panel-soft), var(--panel));
  box-shadow: var(--shadow);
  color: var(--accent);
  font-size: 3.2rem;
  line-height: 1;
}

.menu__title {
  margin: 0;
  font-size: clamp(2rem, 9vw, 2.75rem);
  font-weight: 800;
  letter-spacing: 0.14em;
}

.menu__actions {
  display: flex;
  flex-direction: column;
  gap: 0.75rem;
  width: 100%;
  /* Holds the gap the subtitle used to provide below the title. */
  margin-top: 2.5rem;
}

/* ==========================================================================
   New game setup
   ========================================================================== */

.setup {
  max-width: 480px;
  margin: 0 auto;
  padding: 1.25rem 1.25rem 3rem;
}

.setup__header {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  margin-bottom: 1.75rem;
}

.setup__title {
  margin: 0;
  font-size: 1.35rem;
  font-weight: 700;
}

.setup__form {
  display: flex;
  flex-direction: column;
  gap: 1.25rem;
}

.field {
  display: flex;
  flex-direction: column;
  gap: 0.5rem;
  margin: 0;
  padding: 0;
  border: 0;
}

.field__label {
  color: var(--text-muted);
  font-size: 0.8rem;
  font-weight: 600;
  letter-spacing: 0.08em;
  text-transform: uppercase;
}

.field__hint {
  margin-left: 0.4rem;
  color: var(--text-faint);
  font-weight: 500;
  text-transform: none;
  letter-spacing: 0;
}

.field__input {
  width: 100%;
  min-height: var(--tap);
  padding: 0.7rem 0.9rem;
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: var(--panel);
  color: var(--text);
  font-family: inherit;
  font-size: 1rem;
  transition: border-color 0.15s ease;
}

.field__input::placeholder {
  color: var(--text-faint);
}

.field__input:focus {
  border-color: var(--accent);
  outline: none;
}

/* Name box and profile picture, side by side. */
.name-row {
  display: flex;
  align-items: center;
  gap: 0.6rem;
}

.name-row .field__input {
  flex: 1;
  min-width: 0;
}

/* ==========================================================================
   Profile picture picker

   A round button showing either the chosen picture or a king, with a `+`
   badge until there is one and a `×` to take it off once there is. The badge
   and the clear button are siblings rather than children of the button: the
   button clips its contents to the circle so the photo fills it, and anything
   inside would be clipped with it.
   ========================================================================== */

.avatar-picker {
  position: relative;
  display: block;
  flex: none;
  width: var(--tap);
  height: var(--tap);
}

.avatar-picker__btn {
  display: grid;
  place-items: center;
  width: 100%;
  height: 100%;
  padding: 0;
  overflow: hidden;
  border: 1px dashed var(--border-strong);
  border-radius: 50%;
  background: var(--panel);
  color: var(--text-faint);
  font-family:
    "Segoe UI Symbol", "Apple Symbols", "Noto Sans Symbols 2",
    "DejaVu Sans", "FreeSerif", sans-serif;
  font-variant-emoji: text;
  font-size: 1.2rem;
  line-height: 1;
  cursor: pointer;
  transition: border-color 0.15s ease, color 0.15s ease;
}

.avatar-picker__btn:hover {
  border-color: var(--accent);
  color: var(--text-muted);
}

.avatar-picker__btn:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}

/* A picture fills the circle, so the dashes have done their job and go. */
.avatar-picker.has-photo .avatar-picker__btn {
  border-style: solid;
  border-color: var(--border-strong);
}

.avatar-picker__img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
}

.avatar-picker__badge {
  position: absolute;
  right: -2px;
  bottom: -2px;
  display: grid;
  place-items: center;
  width: 18px;
  height: 18px;
  border-radius: 50%;
  background: var(--accent);
  color: var(--accent-text);
  font-size: 0.8rem;
  font-weight: 700;
  line-height: 1;
  /* Decoration in front of the button — never the thing a tap lands on. */
  pointer-events: none;
}

.avatar-picker.has-photo .avatar-picker__badge {
  display: none;
}

/*
  The remove button is small on purpose but keeps a full-size tap target: the
  visible circle is 20px and the ::before below extends what a finger can hit
  to 44px, so it is easy to press and hard to press by accident.
*/
.avatar-picker__clear {
  position: absolute;
  right: -6px;
  top: -6px;
  display: grid;
  place-items: center;
  width: 20px;
  height: 20px;
  padding: 0;
  border: 1px solid var(--border-strong);
  border-radius: 50%;
  background: var(--panel-soft);
  color: var(--text-muted);
  font-size: 0.9rem;
  line-height: 1;
  cursor: pointer;
}

.avatar-picker__clear::before {
  content: "";
  position: absolute;
  inset: -12px;
}

.avatar-picker__clear:hover {
  border-color: var(--danger);
  color: var(--text);
}

.avatar-picker__clear:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}

/*
  Visually hidden rather than `hidden`: a display:none file input cannot
  reliably be opened with .click() in every browser, and this one is opened
  entirely from the button in front of it.
*/
.avatar-picker__file {
  position: absolute;
  width: 1px;
  height: 1px;
  opacity: 0;
  pointer-events: none;
}

/* Decoding and re-encoding a large photo is not instant. Say that it is working. */
.avatar-picker.is-busy .avatar-picker__btn {
  opacity: 0.55;
  cursor: progress;
}

.field--modes {
  gap: 0.6rem;
}

.mode {
  display: flex;
  align-items: center;
  gap: 0.85rem;
  min-height: var(--tap);
  padding: 0.85rem 1rem;
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: var(--panel);
  cursor: pointer;
  transition: border-color 0.15s ease, background 0.15s ease;
}

.mode input {
  width: 18px;
  height: 18px;
  accent-color: var(--accent);
  flex: none;
}

.mode__body {
  display: flex;
  flex-direction: column;
}

.mode__name {
  font-weight: 600;
}

.mode__desc {
  color: var(--text-faint);
  font-size: 0.82rem;
}

/*
  The chosen mode. Keyed on "not disabled" rather than on a class each
  label has to carry, because the label that forgot to carry it was the
  online one — the only mode that can be unavailable, and so the only one
  whose row state was worth anything. Derived from the real condition, a
  new mode is highlighted correctly without anybody remembering anything.
*/
.mode:not(.mode--disabled):has(input:checked) {
  border-color: var(--accent);
  background: var(--accent-soft);
}

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

/* ==========================================================================
   Online multiplayer (Phase 2)
   ========================================================================== */

.online-fields {
  display: flex;
  flex-direction: column;
  gap: 1.25rem;
}

.online-choice {
  display: flex;
  flex-direction: column;
  gap: 0.9rem;
}

.online-choice__divider {
  display: flex;
  align-items: center;
  gap: 0.75rem;
  color: var(--text-faint);
  font-size: 0.78rem;
  letter-spacing: 0.06em;
  text-transform: uppercase;
}

.online-choice__divider::before,
.online-choice__divider::after {
  content: "";
  flex: 1;
  height: 1px;
  background: var(--border);
}

.join-row {
  display: flex;
  gap: 0.6rem;
}

.join-row .btn {
  flex: none;
  padding-inline: 1.4rem;
}

/* Room codes read as codes: wide tracking, tabular, unmistakable */
.field__input--code {
  flex: 1;
  min-width: 0;
  font-family: ui-monospace, "SF Mono", "Cascadia Mono", Menlo, Consolas, monospace;
  font-size: 1.15rem;
  font-weight: 700;
  letter-spacing: 0.34em;
  text-align: center;
  text-transform: uppercase;
}

.field__input--code::placeholder {
  letter-spacing: 0.34em;
}

/* --- Waiting-for-opponent screen --- */

.waiting {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  min-height: 100vh;
  min-height: 100dvh;
  max-width: 420px;
  margin: 0 auto;
  padding: 2rem 1.25rem;
  text-align: center;
}

.waiting__spinner {
  width: 46px;
  height: 46px;
  margin-bottom: 1.5rem;
  border: 3px solid var(--border-strong);
  border-top-color: var(--accent);
  border-radius: 50%;
  animation: spin 0.9s linear infinite;
}

@keyframes spin {
  to { transform: rotate(360deg); }
}

.waiting__title {
  margin: 0 0 0.4rem;
  font-size: 1.4rem;
  font-weight: 750;
}

.waiting__hint {
  margin: 0 0 1.5rem;
  color: var(--text-muted);
  font-size: 0.92rem;
}

.room-code {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  /*
    Never wider than the screen. Without this the box just keeps growing with
    the text size, and `body { overflow-x: hidden }` quietly clips whatever
    sticks out — see .room-code__value for why that is the worst possible
    failure here.
  */
  max-width: 100%;
  /* Centred, and the copy button drops to its own row if the code needs the
     whole width. */
  flex-wrap: wrap;
  justify-content: center;
  margin-bottom: 1.5rem;
  /* Padding gives way before the code does: at a large text size the fixed
     1.1rem was costing 40px of the very space the code needed. */
  padding: 0.6rem clamp(0.45rem, 3vw, 1.1rem);
  border: 1px solid var(--accent);
  border-radius: var(--radius);
  background: var(--accent-soft);
}

.room-code__value {
  font-family: ui-monospace, "SF Mono", "Cascadia Mono", Menlo, Consolas, monospace;
  /*
    Shrinks with the viewport, so a narrow phone gets a code that fits rather
    than one that runs off the edge. The rem floor keeps it legible and keeps
    it scaling with the reader's own text size, which a bare `vw` would not.
  */
  font-size: clamp(1.35rem, 9vw, 1.85rem);
  font-weight: 800;
  /* Tracking is the first thing to give: it is what makes the code readable in
     chunks, not what makes it legible, so trading it for a code that stays on
     one line is a good trade. */
  letter-spacing: clamp(0.12em, 2.5vw, 0.3em);
  /* Matches the tracking, because the trailing space on the last character
     would otherwise push the code off-centre. */
  text-indent: clamp(0.12em, 2.5vw, 0.3em);
  color: var(--accent);

  /*
    Wrap rather than vanish.

    Horizontal overflow is hidden on the body, so a code wider than the screen
    is not something you can scroll to — it is simply gone. And .waiting
    centres its children, so it goes from BOTH ends and leaves the middle:
    at a 2x accessibility text size on a 320px phone, ABC234 renders as BC23,
    which reads as a complete four-character code rather than as a broken one.
    That is the worst failure available to the one string this screen exists to
    hand to another person.

    min-width lets the flex item shrink below its content — without it the
    `auto` minimum makes it refuse — and the break lets the code use two lines
    when one will not do. A wrapped code is slightly ugly. An invisible one is
    a dead end.
  */
  min-width: 0;
  overflow-wrap: anywhere;
}

.waiting__note {
  margin: 0 0 2rem;
  color: var(--text-faint);
  font-size: 0.86rem;
  line-height: 1.6;
}

.waiting__note strong {
  color: var(--text-muted);
}

/* --- In-game room bar --- */

.room-bar {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 0.75rem;
  max-width: 560px;
  margin: 0 auto;
  padding: 0.45rem 0.9rem;
  border-bottom: 1px solid var(--border);
  font-size: 0.8rem;
}

.room-bar__code {
  color: var(--text-faint);
  letter-spacing: 0.04em;
}

.room-bar__code strong {
  font-family: ui-monospace, "SF Mono", "Cascadia Mono", Menlo, Consolas, monospace;
  letter-spacing: 0.16em;
  color: var(--text-muted);
}

.room-bar__status {
  display: inline-flex;
  align-items: center;
  gap: 0.45rem;
  color: var(--text-muted);
  font-weight: 600;
}

.room-bar__dot {
  width: 8px;
  height: 8px;
  flex: none;
  border-radius: 50%;
  background: var(--text-faint);
}

.room-bar__status[data-state="live"] .room-bar__dot {
  background: var(--success);
  box-shadow: 0 0 0 3px rgba(79, 201, 138, 0.16);
}

.room-bar__status[data-state="connecting"] .room-bar__dot,
.room-bar__status[data-state="waiting"] .room-bar__dot {
  background: var(--accent);
  animation: pulse 1.3s ease-in-out infinite;
}

.room-bar__status[data-state="away"] .room-bar__dot,
.room-bar__status[data-state="offline"] .room-bar__dot {
  background: var(--danger);
}

.room-bar__status[data-state="away"],
.room-bar__status[data-state="offline"] {
  color: #ff9d94;
}

/* ==========================================================================
   Game screen shell
   ========================================================================== */

.app-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 0.5rem;
  padding: 0.4rem 0.6rem;
  border-bottom: 1px solid var(--border);
  background: rgba(15, 17, 21, 0.85);
  backdrop-filter: blur(8px);
  position: sticky;
  top: 0;
  z-index: 20;
}

.app-header__title {
  font-size: 0.85rem;
  font-weight: 700;
  letter-spacing: 0.16em;
  color: var(--text-muted);
}

.game {
  display: flex;
  flex-direction: column;
  gap: 0.7rem;
  max-width: 560px;
  margin: 0 auto;
  padding: 0.8rem 0.75rem 2.5rem;
}

/*
   On phones the panel is transparent to layout — its children participate in
   .game's flex flow directly. Wider breakpoints turn it into a real column.
*/
.game-panel {
  display: contents;
}

/* ==========================================================================
   Player cards
   ========================================================================== */

.player-card {
  display: flex;
  align-items: center;
  gap: 0.75rem;
  padding: 0.6rem 0.85rem;
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: var(--panel);
  transition: border-color 0.2s ease, background 0.2s ease;
}

.player-card.is-active {
  border-color: var(--accent);
  background: linear-gradient(90deg, var(--accent-soft), var(--panel) 60%);
}

.player-card__avatar {
  font-family:
    "Segoe UI Symbol", "Apple Symbols", "Noto Sans Symbols 2",
    "DejaVu Sans", "FreeSerif", sans-serif;
  font-variant-emoji: text;
  display: grid;
  place-items: center;
  width: 38px;
  height: 38px;
  flex: none;
  overflow: hidden;
  border: 1px solid var(--border-strong);
  border-radius: 50%;
  font-size: 1.3rem;
  line-height: 1;
}

.player-card__avatar[data-color="w"] {
  background: #f2f2ee;
  color: #22252d;
}

.player-card__avatar[data-color="b"] {
  background: #23262f;
  color: #e9ecf2;
}

/*
  A profile picture fills the circle completely, so the light/dark fill above
  never shows through and the two cards stop differing by background. What
  keeps them telling apart is the ring: the colour the player is playing.
*/
.player-card__photo {
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
}

.player-card__avatar:has(.player-card__photo:not([hidden])) {
  border-width: 2px;
}

.player-card__avatar[data-color="w"]:has(.player-card__photo:not([hidden])) {
  border-color: #f2f2ee;
}

.player-card__avatar[data-color="b"]:has(.player-card__photo:not([hidden])) {
  border-color: #565d72;
}

.player-card__info {
  display: flex;
  flex-direction: column;
  min-width: 0;
  flex: 1;
}

.player-card__name {
  font-weight: 650;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

/*
  Colour, captured pile and lead share one line, so the pile costs the card no
  height. It is allowed to shrink and clip rather than push the row wider,
  because the name above it must never be squeezed out by a long pile.
*/
.player-card__meta {
  display: flex;
  align-items: center;
  gap: 0.4rem;
  min-width: 0;
}

.player-card__color {
  flex: none;
  color: var(--text-faint);
  font-size: 0.78rem;
  letter-spacing: 0.06em;
  text-transform: uppercase;
}

/*
  The glyphs overlap slightly. A full set of captures is fifteen pieces and a
  phone card has room for about eight at their natural width; tucking each one
  a third behind the last fits the lot while leaving every piece identifiable,
  which is the same trick a stack of poker chips uses.
*/
/*
   The taken pieces, in a column either side of the board.

   Overlaid rather than laid out beside it, and that is the whole trick. The
   board is square and limited by WIDTH, so a column in the flow costs a pixel
   off each side of every square — measured at 14% of the board, which is more
   than the full-bleed change had just won back.

   It does not have to cost anything, because the canvas is already not full of
   board. The camera reserves headroom for a king standing on the far rank, so
   at the low angle the board is drawn across about 84% of the canvas and the
   rest is empty sky either side: 32px of it on a 390px phone, against a 26px
   column. The tray goes there, for free.

   That margin shrinks as the camera rises — 23px at Large, 12px at Max — so
   the board-area takes real padding at Max, where there is no longer room to
   borrow. See [data-zoom] below.
*/
.capture-tray {
  position: absolute;
  display: flex;
  flex-direction: column;
  align-items: center;
  /*
    20px, which is the margin the camera leaves at the MIDDLE zoom — the
    tightest level the tray still has to fit inside. Sizing to the widest
    level instead put 3px of captured pieces on top of the a-file.

    The default level has half as much again to spare, and a piece portrait is
    a picture rather than a letterform: it needs the pixels far more than a
    glyph does. So that level gets the wider tray — see board.css.
  */
  width: 20px;
  padding: 6px 0;
  border-radius: 999px;
  /*
    A faint column behind the pile, so a handful of small glyphs against a
    board reads as a deliberate tray rather than as pieces that have come
    loose. It hugs the pile rather than running the board's full height —
    an empty full-height panel is just a stripe.
  */
  background: rgba(255, 255, 255, 0.06);
  /* Decoration over a board that is tapped: never intercept a move. */
  pointer-events: none;
}

/* Nothing taken yet means nothing to show, not an empty pill. */
.capture-tray:empty {
  display: none;
}

/* Each pile grows from the same edge as the card of the player it belongs to. */
.capture-tray--top {
  left: 0;
  top: 2px;
}

.capture-tray--bottom {
  right: 0;
  bottom: 2px;
}

.capture {
  font-family:
    "Segoe UI Symbol", "Apple Symbols", "Noto Sans Symbols 2",
    "DejaVu Sans", "FreeSerif", sans-serif;
  font-variant-emoji: text;
  flex: none;
  /*
    The glyphs overlap vertically. A full pile is fifteen pieces and the column
    is as tall as the board — about 340px on a phone, which fifteen glyphs at
    their natural line height do not fit. Tucking each one a quarter behind the
    last does, and still leaves every piece identifiable: the same trick a
    stack of poker chips uses.
  */
  margin-bottom: -0.25em;
  font-size: 0.95rem;
  line-height: 1;
}

.capture:last-child {
  margin-bottom: 0;
}

/*
   A drawn piece rather than a glyph.

   Square, because the portrait is framed to the tallest piece in the set — so
   a pawn genuinely occupies less of its tile than a king does, exactly as it
   does on the board. The overlap is a percentage because these are images with
   no font size to hang an em off; percentage margins resolve against the
   column's width, which is the same number as the tile's height here.
*/
.capture--piece {
  display: block;
  width: 100%;
  height: auto;
  margin-bottom: -30%;
}

.capture--piece:last-child {
  margin-bottom: 0;
}

/*
  These are tinted to be READ, not to match the pieces exactly.

  Both sides use the same solid glyphs, so colour is the only thing telling
  them apart — and a captured black piece painted its true near-black sits on
  a near-black card and disappears, which is what the first cut of this did.
  A muted slate keeps it obviously the darker side while staying legible
  against the panel, and the two tints are far enough apart to tell at a
  glance which pile you are looking at.
*/
.capture[data-color="w"] {
  color: #f4f4f0;
}

.capture[data-color="b"] {
  color: #94a1bd;
}

/* Only ever shown on the side that is actually ahead. */
.player-card__edge {
  flex: none;
  color: var(--accent);
  font-size: 0.78rem;
  font-weight: 700;
  font-variant-numeric: tabular-nums;
}

.player-card__turn {
  flex: none;
  padding: 0.25rem 0.6rem;
  border-radius: 999px;
  background: var(--accent-soft);
  color: var(--accent);
  font-size: 0.72rem;
  font-weight: 700;
  letter-spacing: 0.06em;
  text-transform: uppercase;
}

/* ==========================================================================
   Status
   ========================================================================== */

.status {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 0.6rem;
  min-height: 40px;
  padding: 0.4rem 0.75rem;
  border-radius: var(--radius);
  text-align: center;
}

.status__text {
  font-size: 1rem;
  font-weight: 700;
  letter-spacing: 0.08em;
  text-transform: uppercase;
}

.status.is-over .status__text {
  color: var(--accent);
}

.status__badge {
  padding: 0.2rem 0.6rem;
  border-radius: 999px;
  font-size: 0.72rem;
  font-weight: 700;
  letter-spacing: 0.08em;
  text-transform: uppercase;
}

.status__badge[data-tone="check"] {
  background: var(--danger-soft);
  color: #ff8b81;
  animation: pulse 1.4s ease-in-out infinite;
}

.status__badge[data-tone="win"] {
  background: rgba(79, 201, 138, 0.14);
  color: var(--success);
}

.status__badge[data-tone="draw"] {
  background: var(--panel-soft);
  color: var(--text-muted);
}

@keyframes pulse {
  0%, 100% { opacity: 1; }
  50% { opacity: 0.55; }
}

/* ==========================================================================
   Controls
   ========================================================================== */

.controls {
  display: grid;
  /* One column per control that is actually shown, rather than a fixed count.
     Zoom is hidden on the flat board, and a fixed template would leave its
     column standing there empty.

     minmax(0, 1fr) rather than 1fr: without the zero floor, each button's
     min-content width becomes a hard minimum and the row overflows on narrow
     screens instead of shrinking. */
  grid-auto-flow: column;
  grid-auto-columns: minmax(0, 1fr);
  gap: 0.5rem;
}

.btn--control {
  flex-direction: column;
  gap: 0.15rem;
  padding: 0.5rem 0.3rem;
  border-color: var(--border);
  background: var(--panel);
  font-size: 0.74rem;
  font-weight: 600;
  letter-spacing: 0.02em;
}

.btn--control span {
  font-size: 1.05rem;
  line-height: 1.1;
}

/* --------------------------------------------------------------------------
   Locked control: present, plainly unavailable, and not pressable
   -------------------------------------------------------------------------- */

/*
   Not the `disabled` attribute — see #buildDynamic in ui.js. These use
   `aria-disabled`, so the browser still treats them as live buttons and would
   otherwise give them the full hover and press affordance of a working
   control. Everything below is what takes that affordance away.
*/
.btn--locked {
  position: relative;
  cursor: not-allowed;
  border-color: var(--border);
  background: var(--panel);
  color: var(--text-faint);
  opacity: 0.55;
}

.btn--locked:hover,
.btn--locked:active,
.btn--locked:focus {
  /* A locked control must not move or light up under the finger. */
  border-color: var(--border);
  background: var(--panel);
  transform: none;
}

/*
   Sits in the corner rather than in the flow, so a locked control keeps
   exactly the same height and label position as the working ones beside it
   and the row does not shift when a control is locked or unlocked.

   Two classes, because `.btn--control span` sets a 1.05rem icon size and
   would otherwise win on specificity and blow the padlock up to icon size.
*/
.btn--locked .btn__lock {
  position: absolute;
  top: 3px;
  right: 4px;
  font-size: 0.62rem;
  line-height: 1;
  opacity: 0.9;
}

/* ==========================================================================
   Move history
   ========================================================================== */

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

.history__toggle {
  display: flex;
  align-items: center;
  gap: 0.6rem;
  width: 100%;
  min-height: var(--tap);
  padding: 0.65rem 0.9rem;
  border: 0;
  background: transparent;
  color: var(--text);
  font-family: inherit;
  font-size: 0.85rem;
  font-weight: 600;
  letter-spacing: 0.04em;
  text-align: left;
  cursor: pointer;
}

.history__count {
  margin-left: auto;
  color: var(--text-faint);
  font-weight: 500;
}

.history__chevron {
  color: var(--text-faint);
  transition: transform 0.2s ease;
}

.history.is-expanded .history__chevron {
  transform: rotate(180deg);
}

.history__list {
  max-height: 0;
  margin: 0;
  padding: 0;
  overflow-y: auto;
  list-style: none;
  transition: max-height 0.25s ease;
}

.history.is-expanded .history__list {
  max-height: 200px;
  padding: 0 0.5rem 0.5rem;
}

.history__row {
  display: grid;
  grid-template-columns: 2.4rem 1fr 1fr;
  gap: 0.5rem;
  padding: 0.3rem 0.4rem;
  border-radius: var(--radius-sm);
  font-variant-numeric: tabular-nums;
  font-size: 0.9rem;
}

.history__row:nth-child(odd) {
  background: rgba(255, 255, 255, 0.025);
}

.history__num {
  color: var(--text-faint);
}

.history__san {
  font-weight: 600;
}

/* ==========================================================================
   Modals
   ========================================================================== */

.modal {
  position: fixed;
  inset: 0;
  z-index: 100;
  display: grid;
  place-items: center;
  padding: 1rem;
}

.modal__backdrop {
  position: absolute;
  inset: 0;
  background: rgba(6, 8, 12, 0.72);
  backdrop-filter: blur(3px);
  opacity: 0;
  transition: opacity 0.2s ease;
}

.modal.is-open .modal__backdrop {
  opacity: 1;
}

.modal__panel {
  position: relative;
  width: 100%;
  max-width: 380px;
  max-height: calc(100dvh - 2rem);
  overflow-y: auto;
  padding: 1.5rem;
  border: 1px solid var(--border-strong);
  border-radius: var(--radius-lg);
  background: var(--bg-elevated);
  box-shadow: var(--shadow-lg);
  text-align: center;
  opacity: 0;
  transform: translateY(12px) scale(0.98);
  transition: opacity 0.2s ease, transform 0.2s ease;
}

.modal.is-open .modal__panel {
  opacity: 1;
  transform: translateY(0) scale(1);
}

.modal__panel--compact {
  max-width: 340px;
}

.modal__title {
  margin: 0 0 0.5rem;
  font-size: 1.25rem;
  font-weight: 750;
  letter-spacing: 0.02em;
}

.modal__text {
  margin: 0 0 1.25rem;
  color: var(--text-muted);
  font-size: 0.92rem;
}

.modal__actions {
  display: flex;
  gap: 0.6rem;
}

.modal__actions .btn {
  flex: 1;
}

.modal__actions--stack {
  flex-direction: column;
}

/* Bottom-sheet presentation on small screens */
.modal--sheet .modal__panel {
  max-width: 420px;
}

/* Game over */
.gameover__icon {
  font-family:
    "Segoe UI Symbol", "Apple Symbols", "Noto Sans Symbols 2",
    "DejaVu Sans", "FreeSerif", sans-serif;
  font-variant-emoji: text;
  display: grid;
  place-items: center;
  width: 68px;
  height: 68px;
  margin: 0 auto 0.9rem;
  border: 1px solid var(--border-strong);
  border-radius: 50%;
  background: var(--panel);
  color: var(--accent);
  font-size: 2.2rem;
  line-height: 1;
}

.gameover__result {
  margin: 0 0 0.35rem;
  color: var(--accent);
  font-size: 1.5rem;
  font-weight: 800;
  letter-spacing: 0.04em;
}

/* Promotion picker */
.promotion {
  display: grid;
  grid-template-columns: repeat(4, 1fr);
  gap: 0.5rem;
  margin-bottom: 1rem;
}

.promotion__choice {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 0.25rem;
  min-height: 76px;
  padding: 0.6rem 0.25rem;
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: var(--panel);
  color: var(--text);
  font-family: inherit;
  cursor: pointer;
  transition: border-color 0.15s ease, background 0.15s ease, transform 0.08s ease;
}

.promotion__choice:hover {
  border-color: var(--accent);
  background: var(--accent-soft);
}

.promotion__choice:active {
  transform: translateY(1px);
}

.promotion__glyph {
  font-family:
    "Segoe UI Symbol", "Apple Symbols", "Noto Sans Symbols 2",
    "DejaVu Sans", "FreeSerif", sans-serif;
  font-variant-emoji: text;
  font-size: 2rem;
  line-height: 1;
}

.promotion__glyph[data-color="w"] {
  color: #f7f7f4;
  text-shadow: 0 0 1px #10121a, 0 1px 0 #10121a, 1px 0 0 #10121a,
               0 -1px 0 #10121a, -1px 0 0 #10121a;
}

.promotion__glyph[data-color="b"] {
  color: #1d1f27;
  text-shadow: 0 0 1px #c9cedb, 0 1px 0 #c9cedb, 1px 0 0 #c9cedb,
               0 -1px 0 #c9cedb, -1px 0 0 #c9cedb;
}

.promotion__name {
  color: var(--text-faint);
  font-size: 0.7rem;
  letter-spacing: 0.04em;
}

/* ==========================================================================
   Settings
   ========================================================================== */

.setting {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 1rem;
  min-height: var(--tap);
  padding: 0.6rem 0;
  border-bottom: 1px solid var(--border);
  text-align: left;
}

.setting--stack {
  flex-direction: column;
  align-items: stretch;
  gap: 0.7rem;
  border-bottom: 0;
  padding-bottom: 1rem;
}

.setting__label {
  font-size: 0.92rem;
  font-weight: 600;
}

/* Accessible toggle built from a native checkbox */
.switch {
  position: relative;
  width: 46px;
  height: 26px;
  flex: none;
  margin: 0;
  border-radius: 999px;
  background: var(--panel-soft);
  border: 1px solid var(--border-strong);
  appearance: none;
  cursor: pointer;
  transition: background 0.18s ease, border-color 0.18s ease;
}

.switch::after {
  content: "";
  position: absolute;
  top: 2px;
  left: 2px;
  width: 18px;
  height: 18px;
  border-radius: 50%;
  background: var(--text-faint);
  transition: transform 0.18s ease, background 0.18s ease;
}

.switch:checked {
  background: var(--accent-soft);
  border-color: var(--accent);
}

.switch:checked::after {
  transform: translateX(20px);
  background: var(--accent);
}

/* --------------------------------------------------------------------------
   Look & feel picker (shown in both skins)
   -------------------------------------------------------------------------- */

.style-picker {
  display: grid;
  grid-template-columns: repeat(2, 1fr);
  gap: 0.5rem;
}

.style-option {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 0.3rem;
  min-height: var(--tap);
  padding: 0.6rem 0.4rem;
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: var(--panel);
  color: var(--text-muted);
  font-family: inherit;
  cursor: pointer;
  transition: border-color 0.15s ease, color 0.15s ease;
}

.style-option.is-active {
  border-color: var(--accent);
  color: var(--text);
}

.style-option__preview {
  position: relative;
  width: 100%;
  height: 34px;
  border-radius: var(--radius-sm);
  border: 1px solid var(--border);
  overflow: hidden;
}

/* Flat checkerboard for Classic. */
.style-option__preview[data-style="classic"] {
  background:
    linear-gradient(135deg, #ecd8b6 0 25%, #b07d4f 25% 50%, #ecd8b6 50% 75%, #b07d4f 75% 100%)
      0 0 / 14px 14px,
    #1b1f2a;
}


.style-option__name {
  font-size: 0.82rem;
  font-weight: 700;
}

.style-option__hint {
  color: var(--text-faint);
  font-size: 0.68rem;
  text-align: center;
}

.theme-picker {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 0.5rem;
}

.theme-swatch {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 0.4rem;
  min-height: var(--tap);
  padding: 0.5rem 0.3rem;
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: var(--panel);
  color: var(--text-muted);
  font-family: inherit;
  font-size: 0.76rem;
  cursor: pointer;
  transition: border-color 0.15s ease, color 0.15s ease;
}

.theme-swatch.is-active {
  border-color: var(--accent);
  color: var(--text);
}

.theme-swatch__preview {
  width: 100%;
  height: 26px;
  border-radius: var(--radius-sm);
  border: 1px solid var(--border);
}

/* --------------------------------------------------------------------------
   Background picker

   Two columns rather than the board picker's three: these labels are words
   rather than one-word colours, and four of them in a row on a phone would
   wrap "Mahogany" onto two lines.
   -------------------------------------------------------------------------- */

.bg-picker {
  display: grid;
  grid-template-columns: repeat(2, 1fr);
  gap: 0.5rem;
}

.bg-swatch {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 0.35rem;
  min-height: var(--tap);
  padding: 0.5rem 0.4rem;
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: var(--panel);
  color: var(--text-muted);
  font-family: inherit;
  cursor: pointer;
  transition: border-color 0.15s ease, color 0.15s ease;
}

.bg-swatch.is-active {
  border-color: var(--accent);
  color: var(--text);
}

/*
  The swatch is the game screen in miniature — ground, a player card, the gold
  pip — rather than a paint chip.

  The paint chip was the obvious thing and it was wrong. Every one of these
  grounds is within a few points of black, so a 30px rectangle of pure ground
  is four black boxes: honest about the colour, useless at telling anyone what
  they are choosing. What separates these backgrounds on a real screen is never
  the ground alone, it is the ground SEEN AGAINST the cards and the accent
  sitting on it — so the swatch shows that instead, and the hue arrives as
  contrast rather than as a wash nobody can see.

  The gradient is in percentages rather than the body's pixel radii, which is
  what lets one rule serve a 44px preview and a phone screen alike.
*/
.bg-swatch__preview {
  position: relative;
  width: 100%;
  height: 44px;
  border-radius: var(--radius-sm);
  border: 1px solid var(--border);
  overflow: hidden;
  background:
    radial-gradient(120% 150% at 50% -20%, var(--bg-glow) 0%, transparent 70%),
    var(--bg);
}

/* The player card: the surface that carries this background's hue at a size
   the eye can actually read it at. */
.bg-swatch__preview::before {
  content: "";
  position: absolute;
  left: 12%;
  right: 12%;
  bottom: 20%;
  height: 38%;
  border: 1px solid var(--border-strong);
  border-radius: 3px;
  background: var(--panel-soft);
}

/* The "to move" pip, so the swatch also says what does NOT change: the accent
   is the same gold on every ground, and seeing it here is how you know. */
.bg-swatch__preview::after {
  content: "";
  position: absolute;
  right: 18%;
  bottom: 30%;
  width: 6px;
  height: 6px;
  border-radius: 50%;
  background: var(--accent);
}

.bg-swatch__label {
  font-size: 0.78rem;
  font-weight: 700;
}

.bg-swatch__hint {
  color: var(--text-faint);
  font-size: 0.66rem;
  text-align: center;
}

/* ==========================================================================
   Toasts
   ========================================================================== */

.toasts {
  position: fixed;
  left: 50%;
  bottom: calc(1.25rem + env(safe-area-inset-bottom));
  z-index: 200;
  display: flex;
  flex-direction: column;
  gap: 0.5rem;
  align-items: center;
  transform: translateX(-50%);
  pointer-events: none;
}

.toast {
  max-width: min(88vw, 340px);
  padding: 0.6rem 1rem;
  border: 1px solid var(--border-strong);
  border-radius: 999px;
  background: rgba(28, 32, 43, 0.96);
  box-shadow: var(--shadow);
  color: var(--text);
  font-size: 0.86rem;
  font-weight: 550;
  text-align: center;
  opacity: 0;
  transform: translateY(10px);
  transition: opacity 0.22s ease, transform 0.22s ease;
}

.toast.is-visible {
  opacity: 1;
  transform: translateY(0);
}

.toast[data-tone="error"] {
  border-color: var(--danger);
  color: #ffb0a8;
}

.toast[data-tone="warn"] {
  border-color: #7a6633;
  color: #f0cd85;
}

/* ==========================================================================
   The clock

   One readout on each player card, shown only in Speed Chess.
   ========================================================================== */

.player-card__clock {
  flex: none;
  min-width: 4.2rem;
  padding: 0.3rem 0.55rem;
  border: 1px solid var(--border-strong);
  border-radius: var(--radius-sm);
  background: var(--bg);
  color: var(--text-muted);
  font-size: 1.02rem;
  font-weight: 700;
  /* Tabular figures, or the whole box twitches every tenth of a second as a 1
     is replaced by a 4. A clock that jitters is a clock you stop reading. */
  font-variant-numeric: tabular-nums;
  letter-spacing: 0.02em;
  text-align: center;
  transition: color 0.2s ease, border-color 0.2s ease, background 0.2s ease;
}

/* Whose time is actually being spent. Not the same as whose turn it is:
   before White's first move neither clock runs, and neither should look like
   it is bleeding. */
.player-card__clock.is-running {
  border-color: var(--accent);
  color: var(--text);
}

.player-card__clock.is-urgent {
  border-color: var(--danger);
  background: var(--danger-soft);
  color: #ff9d94;
}

/* Down, not out — the flag has fallen and the number has stopped moving. */
.player-card__clock.is-flagged {
  opacity: 0.7;
}

/* --------------------------------------------------------------------------
   Time-control picker
   -------------------------------------------------------------------------- */

.time-picker {
  display: grid;
  grid-template-columns: repeat(2, 1fr);
  gap: 0.5rem;
}

.time-option {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 0.1rem;
  min-height: var(--tap);
  padding: 0.55rem 0.4rem;
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: var(--panel);
  color: var(--text-muted);
  font-family: inherit;
  cursor: pointer;
  transition: border-color 0.15s ease, color 0.15s ease;
}

.time-option.is-active {
  border-color: var(--accent);
  background: var(--accent-soft);
  color: var(--text);
}

.time-option__label {
  font-size: 1rem;
  font-weight: 700;
  font-variant-numeric: tabular-nums;
}

.time-option__name {
  color: var(--text-faint);
  font-size: 0.68rem;
  letter-spacing: 0.06em;
  text-transform: uppercase;
}

.time-option.is-active .time-option__name {
  color: var(--accent);
}

/* ==========================================================================
   The tournament ladder

   Five rows on the New Game form, one per rung. Read top to bottom as a
   climb, so the row you are on is the loud one and the rest are quiet in two
   different ways: what is behind you is dimmed but ticked, what is ahead is
   dimmed and unmarked.
   ========================================================================== */

.ladder {
  display: flex;
  flex-direction: column;
  gap: 0.4rem;
  margin: 0;
  padding: 0;
  list-style: none;
}

.ladder__rung {
  display: grid;
  /* The mark column is fixed so the names line up whatever is in it — a tick
     and a digit are not the same width, and a ladder whose rungs shift by a
     pixel as you climb reads as broken. */
  grid-template-columns: 1.6rem 1fr auto;
  align-items: center;
  gap: 0.1rem 0.6rem;
  padding: 0.55rem 0.7rem;
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  background: var(--panel);
}

.ladder__mark {
  grid-row: span 2;
  display: flex;
  align-items: center;
  justify-content: center;
  width: 1.6rem;
  height: 1.6rem;
  border: 1px solid var(--border-strong);
  border-radius: 50%;
  color: var(--text-faint);
  font-size: 0.76rem;
  font-weight: 700;
}

.ladder__name {
  color: var(--text-muted);
  font-size: 0.92rem;
  font-weight: 650;
}

.ladder__hint {
  grid-column: 2;
  color: var(--text-faint);
  font-size: 0.72rem;
}

.ladder__state {
  grid-row: span 2;
  color: var(--text-faint);
  font-size: 0.68rem;
  letter-spacing: 0.06em;
  text-transform: uppercase;
}

/* Behind you: ticked, and stated plainly. Dimmed rather than hidden — the
   rungs you have beaten are the best part of the run to look at. */
.ladder__rung.is-beaten .ladder__mark {
  border-color: var(--success);
  color: var(--success);
}

.ladder__rung.is-beaten .ladder__name {
  color: var(--text);
}

/* The one you are on. */
.ladder__rung.is-current {
  border-color: var(--accent);
  background: var(--accent-soft);
}

.ladder__rung.is-current .ladder__mark {
  border-color: var(--accent);
  color: var(--accent);
}

.ladder__rung.is-current .ladder__name {
  color: var(--text);
}

.ladder__rung.is-current .ladder__state {
  color: var(--accent);
}

/* Ahead of you. */
.ladder__rung.is-locked {
  opacity: 0.55;
}

.ladder__note {
  margin: 0.6rem 0 0;
  color: var(--text-faint);
  font-size: 0.76rem;
  line-height: 1.45;
}

/* ==========================================================================
   Hover motion

   How every control answers a pointer, in one place rather than each
   component inventing its own. Three things happen together: the control
   lifts, a shadow opens under it, and on the two solid buttons a sheen
   crosses the face — the button's own gradient seen through a moving
   highlight, rather than another colour change.

   LAST in the file on purpose. These selectors are no more specific than the
   component rules they extend — `.theme-swatch:hover` against
   `.theme-swatch:hover` — so source order is what makes them win, and it is
   also what keeps this readable as one behaviour instead of a transform
   scattered through nine components.
   ========================================================================== */

.btn,
.icon-btn,
.avatar-picker__btn,
.promotion__choice,
.style-option,
.theme-swatch,
.bg-swatch {
  transition:
    background 0.18s ease,
    border-color 0.18s ease,
    color 0.18s ease,
    box-shadow 0.24s ease,
    /* A little overshoot on the way up. Not much: a control that springs past
       its own resting place reads as loose rather than responsive. */
    transform 0.2s cubic-bezier(0.34, 1.26, 0.64, 1);
}

/* The two solid buttons carry the sheen, so they have to contain it. Outside
   the hover query below, because a rule that applied only on pointer devices
   would leave a phone with differently-shaped buttons. */
.btn--primary,
.btn[data-tone="danger"] {
  position: relative;
  overflow: hidden;
  isolation: isolate;
}

/*
   The lift is behind `hover: hover` because a touch screen reports a tap as a
   hover and then KEEPS reporting it until you tap something else. Unguarded, a
   phone would leave the last button you pressed floating and lit. The press
   and focus states further down are deliberately outside the guard: both are
   intentional, and neither sticks to a control the way a tap's phantom hover
   does.
*/
@media (hover: hover) and (pointer: fine) {
  .btn:hover:not(:disabled):not(.btn--locked),
  .icon-btn:hover,
  .avatar-picker__btn:hover,
  .promotion__choice:hover,
  .style-option:hover,
  .theme-swatch:hover,
  .bg-swatch:hover,
  .time-option:hover {
    transform: translateY(-2px);
    box-shadow: var(--shadow);
  }

  /*
     The accent buttons glow in their own colour. A gold button casting a grey
     shadow looks stuck to the page rather than lifted off it.

     `.btn` is repeated only to win: the generic rule above carries two :not()
     clauses and so outranks a plain `.btn--primary:hover`, which left the gold
     buttons wearing the grey shadow. Matching its weight and coming later is
     what settles it.
  */
  .btn.btn--primary:hover:not(:disabled) {
    box-shadow: 0 10px 24px rgba(232, 180, 76, 0.3);
  }

  .btn.btn[data-tone="danger"]:hover:not(:disabled) {
    box-shadow: 0 10px 24px rgba(229, 89, 77, 0.32);
  }

  /* A picker that is the chosen one wears a gold outline. Hovering it must not
     take away the mark that says so, hence the :not. */
  .style-option:hover:not(.is-active),
  .theme-swatch:hover:not(.is-active),
  .bg-swatch:hover:not(.is-active),
  .time-option:hover:not(.is-active) {
    border-color: var(--border-strong);
    color: var(--text);
  }

  /*
     The sheen: one pass across the face, on the solid buttons only — on a
     panel button there is nothing for a highlight to catch.

     z-index: -1 puts it behind the label and in front of the fill. That is
     not a trick: within a stacking context a negative-z child paints after
     the element's own background and before its inline content, which is
     exactly the layer a highlight belongs in. `isolation` above is what keeps
     it from slipping behind an ancestor instead.
  */
  .btn--primary::after,
  .btn[data-tone="danger"]::after {
    content: "";
    position: absolute;
    inset: 0;
    z-index: -1;
    background: linear-gradient(
      105deg,
      transparent 38%,
      rgba(255, 255, 255, 0.42) 50%,
      transparent 62%
    );
    transform: translateX(-130%);
  }

  .btn--primary:hover:not(:disabled)::after,
  .btn[data-tone="danger"]:hover:not(:disabled)::after {
    transform: translateX(130%);
    /* Only on the way in. Returning, it snaps back off-screen unseen rather
       than sweeping backwards, which would read as the button undoing itself. */
    transition: transform 0.62s ease;
  }
}

/*
   The press has to beat the lift, so it comes after it and goes the other way.
   A hovered control that merely returned to its resting place under the finger
   would read as not having been pressed at all.
*/
.btn:active:not(:disabled):not(.btn--locked),
.icon-btn:active,
.avatar-picker__btn:active,
.promotion__choice:active,
.style-option:active,
.theme-swatch:active,
.bg-swatch:active,
.time-option:active {
  transform: translateY(1px);
  box-shadow: none;
  transition-duration: 0.07s;
}

/* Keyboard parity: the same answer to the same intent. A control that lights
   up for a mouse and does nothing for Tab is telling half the room it is not
   for them. */
.btn:focus-visible:not(:disabled):not(.btn--locked),
.icon-btn:focus-visible,
.avatar-picker__btn:focus-visible,
.promotion__choice:focus-visible,
.style-option:focus-visible,
.theme-swatch:focus-visible,
.bg-swatch:focus-visible,
.time-option:focus-visible {
  transform: translateY(-2px);
}

/* ==========================================================================
   Chat, emotes, friends
   ========================================================================== */

/*
  A count riding on a button: the Friends button on the menu, and the chat
  button in the room bar. Absolute rather than in the flow so that adding a
  number never changes the size of the thing it is attached to — a menu
  button that grew when a request arrived would move everything under it.
*/
.btn__badge,
.room-bar__unread {
  position: absolute;
  top: -6px;
  right: -6px;
  display: grid;
  place-items: center;
  min-width: 20px;
  height: 20px;
  padding: 0 5px;
  border-radius: 999px;
  background: var(--accent);
  color: var(--accent-text);
  font-size: 0.7rem;
  font-weight: 800;
  line-height: 1;
  font-variant-numeric: tabular-nums;
}

/* The badge is positioned against the button, so the button has to be the
   thing it is positioned against. */
#btn-menu-friends,
.room-bar__chat {
  position: relative;
}

.room-bar__chat {
  display: inline-grid;
  place-items: center;
  width: 34px;
  height: 34px;
  flex: none;
  border: 1px solid var(--border);
  border-radius: 50%;
  background: var(--panel);
  color: var(--text-muted);
  font-size: 1rem;
  cursor: pointer;
  transition: background 0.15s ease, border-color 0.15s ease, transform 0.08s ease;
}

.room-bar__chat:hover {
  background: var(--panel-soft);
  border-color: var(--border-strong);
}

.room-bar__chat:active {
  transform: translateY(1px);
}

.room-bar__chat-glyph {
  line-height: 1;
}

/* --------------------------------------------------------- emote bubble -- */

/*
  The bubble is positioned against the card it belongs to. Set here rather
  than on the card rule itself so the whole feature stays in one block — the
  cards were laid out before there was anything to float above them.
*/
.player-card {
  position: relative;
}

.player-card__emote {
  position: absolute;
  left: 0.6rem;
  /* Sits over the card's own top edge, so it reads as coming OUT of the
     card rather than hovering unattached above it. */
  bottom: calc(100% - 0.9rem);
  z-index: 3;
  display: grid;
  place-items: center;
  min-width: 2.3rem;
  height: 2.3rem;
  padding: 0 0.45rem;
  border: 1px solid var(--border-strong);
  border-radius: 999px;
  background: var(--panel-soft);
  box-shadow: var(--shadow);
  font-size: 1.3rem;
  line-height: 1;
  /* Decoration on top of a card that is not interactive anyway — but the
     board is right beside it, and a bubble that swallowed a tap aimed at a
     square would be maddening. */
  pointer-events: none;
}

/*
  2.6s mirrors EMOTE_BUBBLE_MS in js/config.js, which is what actually takes
  the bubble off screen. If they disagree, the shorter one wins and looks
  broken — either a bubble that vanishes mid-animation or one that sits
  finished and faded until the timer catches up.
*/
.player-card__emote.is-popping {
  animation: emote-pop 2.6s cubic-bezier(0.2, 0.9, 0.3, 1) forwards;
}

@keyframes emote-pop {
  0%   { opacity: 0; transform: translateY(6px) scale(0.6); }
  12%  { opacity: 1; transform: translateY(0) scale(1.12); }
  22%  { transform: translateY(0) scale(1); }
  82%  { opacity: 1; transform: translateY(0) scale(1); }
  100% { opacity: 0; transform: translateY(-8px) scale(0.9); }
}

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

.modal--chat .modal__panel,
.modal--friends .modal__panel {
  max-width: 460px;
  /* The panels above are centred announcements; these two are documents you
     read and type into, and centred prose is neither. */
  text-align: left;
  display: flex;
  flex-direction: column;
  gap: 0.8rem;
}

.chat__head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 0.5rem;
}

.chat__head .modal__title {
  margin: 0;
}

.chat__log {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: 0.4rem;
  /* Capped in viewport units so the emote row and the box stay on screen on
     a short phone, where the panel would otherwise scroll them off the
     bottom and hide the only way to reply. */
  max-height: min(44dvh, 320px);
  overflow-y: auto;
  overscroll-behavior: contain;
}

.chat__log:empty {
  display: none;
}

.chat__row {
  display: flex;
  flex-direction: column;
  gap: 0.1rem;
  align-self: flex-start;
  max-width: 84%;
  padding: 0.4rem 0.6rem;
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  background: var(--panel);
}

.chat__row[data-who="me"] {
  align-self: flex-end;
  border-color: rgba(232, 180, 76, 0.3);
  background: var(--accent-soft);
}

.chat__who {
  font-size: 0.66rem;
  font-weight: 700;
  letter-spacing: 0.07em;
  text-transform: uppercase;
  color: var(--text-faint);
}

.chat__body {
  font-size: 0.92rem;
  line-height: 1.35;
  /* A message can legally be 160 characters with no space in it. Without
     this, one of those widens the panel until the sheet scrolls sideways. */
  overflow-wrap: anywhere;
}

.chat__emote {
  font-size: 1.9rem;
  line-height: 1.15;
}

.chat__empty,
.friends__empty {
  margin: 0;
  color: var(--text-faint);
  font-size: 0.85rem;
}

.friends__status {
  margin: 0;
  font-size: 0.82rem;
  color: var(--text-muted);
}

.friends__status[data-tone="error"] {
  color: var(--danger);
}

/* ------------------------------------------------------------- emote row -- */

.emote-bar {
  display: grid;
  /* Four across, so eight emotes are two tidy rows at any width — and each
     cell stays over the 44px tap target on the narrowest phone. */
  grid-template-columns: repeat(4, 1fr);
  gap: 0.4rem;
}

.emote {
  display: grid;
  place-items: center;
  min-height: var(--tap);
  padding: 0.3rem;
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  background: var(--panel);
  cursor: pointer;
  transition: background 0.15s ease, border-color 0.15s ease, transform 0.08s ease;
}

.emote:hover {
  background: var(--panel-soft);
  border-color: var(--border-strong);
  transform: translateY(-2px);
}

.emote:active {
  transform: translateY(0);
}

.emote__glyph {
  font-size: 1.4rem;
  line-height: 1;
}

.chat__compose {
  display: flex;
  gap: 0.5rem;
}

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

/* --------------------------------------------------------- friends panel -- */

.me-card {
  display: flex;
  align-items: flex-start;
  gap: 0.75rem;
  padding: 0.85rem;
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: var(--panel);
}

.me-card__avatar {
  font-family:
    "Segoe UI Symbol", "Apple Symbols", "Noto Sans Symbols 2",
    "DejaVu Sans", "FreeSerif", sans-serif;
  font-variant-emoji: text;
  display: grid;
  place-items: center;
  width: 48px;
  height: 48px;
  flex: none;
  overflow: hidden;
  border: 1px solid var(--border-strong);
  border-radius: 50%;
  background: #23262f;
  color: #e9ecf2;
  font-size: 1.6rem;
  line-height: 1;
}

.me-card__photo {
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
}

.me-card__body {
  flex: 1;
  min-width: 0;
}

.me-card__code {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 0.35rem;
  margin-top: 0.6rem;
}

.me-card__code-label {
  width: 100%;
  font-size: 0.68rem;
  font-weight: 700;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--text-faint);
}

/* Smaller than the room code on the waiting screen: that one is read across
   a table, this one is read off your own phone. */
.me-card__code .room-code__value {
  font-size: clamp(1.05rem, 6vw, 1.35rem);
  letter-spacing: 0.2em;
}

.me-card__code .icon-btn {
  width: 36px;
  height: 36px;
  font-size: 1rem;
}

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

.friends__heading {
  margin: 0;
  font-size: 0.72rem;
  font-weight: 700;
  letter-spacing: 0.09em;
  text-transform: uppercase;
  color: var(--text-faint);
}

.friends__list {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: 0.45rem;
}

.friend {
  display: flex;
  align-items: center;
  gap: 0.6rem;
  padding: 0.5rem 0.6rem;
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  background: var(--panel);
}

.friend__avatar {
  font-family:
    "Segoe UI Symbol", "Apple Symbols", "Noto Sans Symbols 2",
    "DejaVu Sans", "FreeSerif", sans-serif;
  font-variant-emoji: text;
  display: grid;
  place-items: center;
  width: 36px;
  height: 36px;
  flex: none;
  overflow: hidden;
  border: 1px solid var(--border-strong);
  border-radius: 50%;
  background: #23262f;
  color: #e9ecf2;
  font-size: 1.2rem;
  line-height: 1;
}

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

.friend__body {
  flex: 1;
  min-width: 0;
  display: flex;
  flex-direction: column;
  gap: 0.1rem;
}

.friend__name {
  font-weight: 650;
  /* A name is capped at 20 characters, which still overflows a narrow row
     next to two buttons. */
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.friend__status {
  display: inline-flex;
  align-items: center;
  gap: 0.35rem;
  font-size: 0.74rem;
  color: var(--text-faint);
}

/* The dot carries the state as well as the words do, which is what makes a
   list scannable without reading it. */
.friend__status::before {
  content: "";
  width: 7px;
  height: 7px;
  flex: none;
  border-radius: 50%;
  background: var(--text-faint);
}

/* Not yet heard from. Deliberately dimmer than offline rather than a
   different colour: it is an absence of news, not news. */
.friend__status[data-state="unknown"] {
  opacity: 0.7;
}

.friend__status[data-state="online"] {
  color: var(--success);
}

.friend__status[data-state="online"]::before {
  background: var(--success);
}

.friend__status[data-state="playing"] {
  color: var(--accent);
}

.friend__status[data-state="playing"]::before {
  background: var(--accent);
}

/* A request carries no presence, so it gets the line without the dot — and
   it shares its row with Accept and Decline, so it must not wrap. */
.friend__note {
  font-size: 0.74rem;
  color: var(--text-faint);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.friend__pending {
  font-size: 0.7rem;
  color: var(--text-faint);
  font-style: italic;
}

.friend__remove {
  width: 34px;
  height: 34px;
  flex: none;
  font-size: 1.1rem;
}

/* The only control on a friend row with a word in it, so it is the only
   one that can be squeezed into two lines. It is not allowed to be. */
.friend__invite {
  flex: none;
  white-space: nowrap;
}

/* Already asked, or not there to ask. Dimmed rather than removed, so the
   row keeps its shape and the reason stays readable. */
.friend__invite:disabled {
  opacity: 0.45;
  cursor: default;
}

/* An invitation is the one row here that is worth interrupting for, and
   the only one that will not still be true in ten minutes. */
.friend--invite {
  border-color: var(--accent);
  background: var(--accent-soft);
}

.friend--invite .friend__note {
  color: var(--accent);
}

/* Accept and Decline sit in a row that also holds a name and a face, so they
   trade the full tap height for fitting. Still 34px, still comfortably over
   the minimum for a control with a label. */
.btn--tiny {
  min-height: 34px;
  padding: 0.3rem 0.7rem;
  border-radius: var(--radius-sm);
  font-size: 0.78rem;
}

/* Respect users who prefer less motion */
@media (prefers-reduced-motion: reduce) {
  *,
  *::before,
  *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
  }

  /*
     Movement is the whole of the hover lift and the sheen, so they go
     altogether rather than snapping to their finished position with the
     transition removed — which is what the rule above would otherwise leave:
     the jump without the motion, the worst of both.

     The colour and shadow changes stay. A control still has to answer the
     pointer; it just does it by lighting up rather than by moving.

     `!important` for the same reason the durations above carry it: these are
     one-class selectors deliberately overruling the hover rules, which are
     four deep. An accessibility preference should not have to win a
     specificity argument it can only lose.
  */
  .btn:hover,
  .btn:active,
  .btn:focus-visible,
  .icon-btn:hover,
  .icon-btn:active,
  .icon-btn:focus-visible,
  .avatar-picker__btn:hover,
  .avatar-picker__btn:active,
  .avatar-picker__btn:focus-visible,
  .promotion__choice:hover,
  .promotion__choice:active,
  .promotion__choice:focus-visible,
  .style-option:hover,
  .style-option:active,
  .style-option:focus-visible,
  .theme-swatch:hover,
  .theme-swatch:active,
  .theme-swatch:focus-visible,
  .bg-swatch:hover,
  .bg-swatch:active,
  .bg-swatch:focus-visible,
  .emote:hover,
  .emote:active,
  .emote:focus-visible,
  .room-bar__chat:hover,
  .room-bar__chat:active,
  .room-bar__chat:focus-visible,
.time-option:focus-visible {
    transform: none !important;
  }

  .btn--primary::after,
  .btn[data-tone="danger"]::after {
    display: none;
  }
}
