/* tilt-party — all styling, including the gameplay css-rotated fallback.
 *
 * Phone-first and dark by default: this is a game played in a living room with
 * the lights down, and a white screen on a forehead is a lamp pointed at the
 * other players. Tablets must not break; nothing here targets them (Non-goals).
 *
 * Three rules carry contract weight and are marked in place:
 *   - `.screen[hidden]` — `hidden` is the ONE visibility mechanism
 *     (contract/screens.md). Every `.screen` sets a display value, so the
 *     attribute needs an explicit override or it would lose to it.
 *   - `[data-orientation-mode="css-rotated"]` — the D5 fallback, scoped to
 *     `#screen-countdown` and `#screen-round` and to nothing else.
 *   - `#gesture-flash[data-outcome]` — colour AND glyph together, never colour
 *     alone (D15, colourblind-safe by construction).
 */

/* ───────────────────────────────────────────────────────────────── tokens */

:root {
  --bg: #12131a;
  --bg-raised: #1c1e29;
  --bg-sunken: #0d0e14;
  --line: #2c2f3d;
  --text: #eceef4;
  --text-dim: #9aa0b4;
  --accent: #6e8cff;
  --danger: #e5484d;
  --correct: #30a46c;
  --pass: #ffb224;

  --radius: 14px;
  --gap: 12px;
  --pad: 16px;

  --font: system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;

  color-scheme: dark;
}

* {
  box-sizing: border-box;
}

html,
body {
  height: 100%;
}

body {
  margin: 0;
  background: var(--bg);
  color: var(--text);
  font-family: var(--font);
  font-size: 16px;
  line-height: 1.4;
  /* The phone is passed hand to hand and held to a forehead: no accidental
     text selection, no long-press callouts, no double-tap zoom. */
  -webkit-user-select: none;
  user-select: none;
  -webkit-tap-highlight-color: transparent;
  touch-action: manipulation;
  overscroll-behavior: none;
}

/* ───────────────────────────────────────────────────────────────── screens */

.screen {
  position: fixed;
  inset: 0;
  display: flex;
  flex-direction: column;
  gap: var(--gap);
  padding: max(var(--pad), env(safe-area-inset-top)) var(--pad)
           max(var(--pad), env(safe-area-inset-bottom));
  overflow: hidden;
}

/* CONTRACT: `hidden` is the only visibility mechanism. Without this the
 * `display: flex` above would keep every screen on screen at once. */
.screen[hidden] {
  display: none;
}

.screen--portrait {
  overflow-y: auto;
}

/* Pass Phone is a tap-anywhere screen (screens.md): the whole section is the
 * target, so it centres its content and takes the pointer itself. */
.screen--tap {
  align-items: center;
  justify-content: center;
  text-align: center;
  cursor: pointer;
}

.screen__back {
  position: absolute;
  top: max(var(--pad), env(safe-area-inset-top));
  left: var(--pad);
}

/* ──────────────────────────────────────────────────────────────── app bar */

.app-bar {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--gap);
  flex: 0 0 auto;
}

.app-bar__title {
  margin: 0;
  font-size: 1.35rem;
  font-weight: 700;
  letter-spacing: 0.01em;
}

/* ──────────────────────────────────────────────────────────────── buttons */

.btn {
  appearance: none;
  min-height: 44px; /* thumb target on a phone being passed around */
  padding: 10px 16px;
  border: 1px solid var(--line);
  border-radius: var(--radius);
  background: var(--bg-raised);
  color: var(--text);
  font: inherit;
  font-weight: 600;
  cursor: pointer;
}

.btn:disabled {
  opacity: 0.5;
  cursor: default;
}

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

.btn--primary {
  border-color: transparent;
  background: var(--accent);
  color: #0b0d16;
}

.btn--danger {
  border-color: transparent;
  background: var(--danger);
  color: #fff;
}

.btn--quiet {
  background: transparent;
  color: var(--text-dim);
}

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

.btn--icon {
  min-width: 44px;
  padding: 10px;
  font-size: 1.2rem;
  line-height: 1;
}

/* ─────────────────────────────────────────────────────────── score strip */

.score-strip {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
  flex: 0 0 auto;
}

.score-chip {
  display: inline-flex;
  align-items: baseline;
  gap: 8px;
  padding: 6px 12px;
  border: 1px solid color-mix(in srgb, var(--player-color, var(--line)) 55%, transparent);
  border-radius: 999px;
  background: color-mix(in srgb, var(--player-color, var(--bg-raised)) 16%, var(--bg-raised));
}

.score-chip__name {
  color: var(--player-color, var(--text));
  font-weight: 650;
}

.score-chip__score {
  font-variant-numeric: tabular-nums;
  font-weight: 700;
}

/* ───────────────────────────────────────────────────────────── deck grid */

.deck-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(140px, 1fr));
  gap: var(--gap);
  align-content: start;
  flex: 1 1 auto;
  min-height: 0;
  overflow-y: auto;
  padding-bottom: 4px;
}

.deck-grid__message {
  grid-column: 1 / -1;
  margin: 0;
  padding: var(--pad) 0;
  color: var(--text-dim);
  text-align: center;
}

.deck-tile {
  appearance: none;
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 6px;
  min-height: 112px;
  padding: 14px;
  border: 1px solid color-mix(in srgb, var(--deck-color, var(--line)) 60%, transparent);
  border-radius: var(--radius);
  /* The deck's own colour, as a wash rather than a fill: eight saturated tiles
     side by side is a swatch book, not a menu. */
  background: linear-gradient(
    160deg,
    color-mix(in srgb, var(--deck-color, var(--bg-raised)) 26%, var(--bg-raised)),
    var(--bg-raised)
  );
  color: var(--text);
  font: inherit;
  text-align: left;
  cursor: pointer;
}

.deck-tile:focus-visible {
  outline: 3px solid var(--deck-color, var(--accent));
  outline-offset: 2px;
}

.deck-tile__icon {
  font-size: 1.9rem;
  line-height: 1;
}

.deck-tile__name {
  font-weight: 700;
  line-height: 1.2;
}

.deck-tile__count {
  color: var(--text-dim);
  font-size: 0.82rem;
  font-variant-numeric: tabular-nums;
}

/* Difficulty mix (tilt-party-15). Three proportional segments pinned to the
   bottom of the tile — `margin-top: auto` inside the tile's column flex, so
   every bar in the grid lines up whatever the name wrapped to.

   Deliberately small and low-contrast: contract README invariant 4 makes most
   cards untagged by design, so this is a hint about a deck's flavour, not a
   headline. Colour is not the only channel — `difficultyLabel` puts the same
   counts in the tile's accessible name and title. */
.deck-tile__difficulty {
  display: flex;
  gap: 2px;
  width: 100%;
  height: 4px;
  margin-top: auto;
  border-radius: 999px;
  overflow: hidden;
}

.deck-tile__difficulty-seg {
  flex: 1 1 0;
  border-radius: 999px;
}

.deck-tile__difficulty-seg[data-level="easy"] {
  background: color-mix(in srgb, var(--correct) 70%, transparent);
}

.deck-tile__difficulty-seg[data-level="medium"] {
  background: color-mix(in srgb, var(--text-dim) 45%, transparent);
}

.deck-tile__difficulty-seg[data-level="hard"] {
  background: color-mix(in srgb, var(--danger) 70%, transparent);
}

/* ────────────────────────────────────────────── notices, prompts, confirm */

.notice {
  flex: 0 0 auto;
  margin: 0;
  padding: var(--pad);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  background: var(--bg-raised);
}

.notice--error {
  border-color: color-mix(in srgb, var(--danger) 55%, transparent);
}

.notice--prompt {
  border-color: color-mix(in srgb, var(--accent) 55%, transparent);
}

.notice--info {
  color: var(--text-dim);
}

.notice__title {
  margin: 0 0 6px;
  font-size: 1.05rem;
}

.notice__body {
  margin: 0 0 12px;
  color: var(--text-dim);
}

/* Written only by a refresh that failed (review pass 1, S2), so it collapses
   to nothing the rest of the time rather than holding a reserved line the way
   `.settings-status` does — this one sits inside a prompt whose height the
   player is looking at. */
.notice__status:not(:empty) {
  margin: 8px 0 0;
  color: var(--danger);
  font-size: 0.85rem;
}

.notice__actions,
.confirm__actions {
  display: flex;
  justify-content: flex-end;
  gap: 8px;
  margin-top: 12px;
}

.update-changes__label {
  margin: 10px 0 4px;
  font-size: 0.8rem;
  font-weight: 700;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--text-dim);
}

.update-changes__list {
  margin: 0;
  padding-left: 1.2em;
}

.deck-select__footer {
  flex: 0 0 auto;
  display: flex;
  flex-direction: column;
  gap: 8px;
  align-items: stretch;
}

.confirm {
  padding: 12px;
  border: 1px solid color-mix(in srgb, var(--danger) 50%, transparent);
  border-radius: var(--radius);
  background: var(--bg-raised);
}

.confirm__text {
  margin: 0;
}

/* ──────────────────────────────────────────────────── pass phone (portrait) */

.pass-phone__lead {
  margin: 0;
  color: var(--text-dim);
}

.pass-phone__name {
  margin: 0;
  font-size: clamp(2.5rem, 14vw, 4.5rem);
  font-weight: 800;
  line-height: 1.05;
  color: var(--player-color, var(--text));
}

.pass-phone__hint {
  margin: 0;
  max-width: 22rem;
  color: var(--text-dim);
}

/* Skip sits inside a tap-anywhere screen, so it is deliberately QUIET: the
   whole surface starts a round, and a prominent button here would compete with
   that for the eye of somebody about to put the phone on their forehead. It is
   still a full-size touch target — the min-height is the point, not the
   decoration. */
.pass-phone__skip {
  margin-top: 0.5rem;
  min-height: 2.75rem;
  padding-inline: 1.25rem;
  background: transparent;
  border: 1px solid var(--line);
  color: var(--text-dim);
  font-size: 0.9rem;
}

/* ──────────────────────────────────────────── gameplay (countdown + round) */

.screen--gameplay {
  align-items: center;
  justify-content: center;
  text-align: center;
  background: var(--bg-sunken);
}

.countdown__beat {
  margin: 0;
  font-size: clamp(5rem, 30vmin, 14rem);
  font-weight: 800;
  line-height: 1;
  font-variant-numeric: tabular-nums;
}

.rotate-nudge {
  margin: 0;
  color: var(--text-dim);
  font-size: 1.05rem;
}

.rotate-nudge[hidden] {
  display: none;
}

.card-text {
  margin: 0;
  max-width: 92%;
  font-weight: 800;
  line-height: 1.08;
  /* Auto-fit is measured in JS (tilt-party-13); this is the pre-measurement
     size so the first paint is never tiny. */
  font-size: clamp(2rem, 11vmin, 8rem);
  overflow-wrap: break-word;
}

.round-timer {
  position: absolute;
  bottom: max(var(--pad), env(safe-area-inset-bottom));
  margin: 0;
  /* "visible but not dominant" (screens.md): readable at a glance from the
     other side of the room, never competing with the card. */
  color: var(--text-dim);
  font-size: 1.1rem;
  font-variant-numeric: tabular-nums;
}

/* CONTRACT (D15): the flash is never colour alone — the glyph below is part of
 * the same rule, and a pitch-distinct cue lands with it. */
.gesture-flash {
  position: absolute;
  inset: 0;
  display: none;
  align-items: center;
  justify-content: center;
  font-size: clamp(6rem, 40vmin, 16rem);
  line-height: 1;
  pointer-events: none;
}

.gesture-flash[data-outcome] {
  display: flex;
}

.gesture-flash[data-outcome="correct"] {
  background: color-mix(in srgb, var(--correct) 82%, transparent);
  color: #08210f;
}

.gesture-flash[data-outcome="correct"]::after {
  content: "✓";
}

.gesture-flash[data-outcome="pass"] {
  background: color-mix(in srgb, var(--pass) 82%, transparent);
  color: #2a1a00;
}

.gesture-flash[data-outcome="pass"]::after {
  content: "⤴";
}

/* CONTRACT (design D5): the css-rotated fallback, scoped to the two gameplay
 * screens and nothing else. Used only when the orientation lock was
 * unavailable or denied AND the viewport is still portrait — the container is
 * turned a quarter turn and given the viewport's dimensions swapped, so the
 * card reads across the long edge of a phone held sideways.
 *
 * DECLARATION ORDER IS LOAD-BEARING. `.screen` sets `inset: 0`, and this rule
 * has to undo that BEFORE it centres the box, because `inset` is the shorthand
 * for top/right/bottom/left: written after `top`/`left` it reset them both to
 * `auto`, the box fell to its static position at the viewport's top-left
 * corner, and translate(-50%, -50%) then centred it ON that corner — three
 * quarters of every card off screen (owner device pass, 2026-09-02, installed
 * PWA; tilt-party-fix-4). `data-orientation-mode` read `css-rotated` the whole
 * time, which is why harness/smoke.test.js step B now measures the rendered
 * box rather than trusting the attribute. */
[data-orientation-mode="css-rotated"] #screen-countdown,
[data-orientation-mode="css-rotated"] #screen-round {
  inset: auto;
  top: 50%;
  left: 50%;
  /* The viewport's dimensions, swapped. `dvh`/`dvw` where the browser has
     them, so a tab that refused fullscreen fits the visible area rather than
     the URL-bar-hidden one; the `vh`/`vw` pair stays as the fallback. */
  width: 100vh;
  height: 100vw;
  width: 100dvh;
  height: 100dvw;
  transform: translate(-50%, -50%) rotate(90deg);
  transform-origin: center center;
}

/* ───────────────────────────────────────────────────── summary (portrait) */

.round-score {
  margin: 0;
  font-size: 1.6rem;
  font-weight: 800;
  font-variant-numeric: tabular-nums;
}

.summary-list {
  flex: 1 1 auto;
  min-height: 0;
  overflow-y: auto;
  margin: 0;
  padding: 0;
  list-style: none;
  display: flex;
  flex-direction: column;
  gap: 6px;
}

.summary-item {
  display: block;
}

/* The zero-card round (tilt-party-15) — a deck that could not be opened. Not a
   `.summary-row`: nothing here is tappable, because there is no outcome to
   flip. Centred and dim so it reads as an explanation rather than as a card
   the player somehow got wrong. */
.summary-empty {
  display: block;
  padding: var(--pad);
  color: var(--text-dim);
  text-align: center;
  text-wrap: balance;
}

/* CONTRACT (D15, same rule as #gesture-flash): a row's outcome is carried by a
 * GLYPH and a colour together, never colour alone. The glyph is real text in
 * the button — it is part of the accessible name too — so the three channels
 * (shape, colour, words) all say the same thing. */
.summary-row {
  display: flex;
  align-items: center;
  gap: 10px;
  width: 100%;
  padding: 10px 12px;
  border: 1px solid var(--line);
  border-left: 4px solid var(--row-color, var(--line));
  border-radius: var(--radius);
  background: var(--bg-raised);
  color: var(--text);
  font: inherit;
  text-align: left;
  cursor: pointer;
}

.summary-row[data-outcome="correct"] {
  --row-color: var(--correct);
}

.summary-row[data-outcome="pass"] {
  --row-color: var(--pass);
  color: var(--text-dim);
}

.summary-row__glyph {
  flex: 0 0 auto;
  width: 1.25em;
  color: var(--row-color);
  font-size: 1.15rem;
  font-weight: 800;
  text-align: center;
}

.summary-row__text {
  flex: 1 1 auto;
  min-width: 0;
  overflow-wrap: break-word;
}

.summary__footer {
  flex: 0 0 auto;
  display: flex;
  flex-direction: column;
  gap: 8px;
}

.next-player {
  margin: 0;
  color: var(--player-color, var(--text-dim));
  font-weight: 650;
}

/* ──────────────────────────────────────────────────── settings (portrait) */

.settings__body {
  flex: 1 1 auto;
  min-height: 0;
  overflow-y: auto;
  display: flex;
  flex-direction: column;
  gap: var(--gap);
}

.settings-group {
  padding: var(--pad);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  background: var(--bg-raised);
  display: flex;
  flex-direction: column;
  gap: 10px;
}

.settings-group__title {
  margin: 0;
  font-size: 0.8rem;
  font-weight: 700;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--text-dim);
}

/* tilt-party-feat-7. The Advanced group's title IS its trigger, so the <h3>
   stops being the padded thing and the button inside it takes over — a title
   you can tap has to have a tap target, and 0.8rem of uppercase text is not
   one. Negative margins pull the button back out to the group's own padding
   edge so the row reads as the heading it is rather than as a button sitting
   under one. */
.settings-group__title--disclosure {
  /* Bottom margin stays 0: the group is a flex column with `gap: 10px`, so the
     gap alone separates the header from the panel and keeps the same vertical
     rhythm as every other group. A negative bottom margin here would eat into
     that gap instead. */
  margin: calc(var(--pad) * -1) calc(var(--pad) * -1) 0;
}

.settings-disclosure {
  /* Stretched by the flex column, NOT `width: 100%` — that resolves against
     the group's content box, so with the negative side margins above the
     button would sit 16px left and finish 32px short of the right edge. */
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 12px var(--pad);
  border: 0;
  border-radius: var(--radius);
  background: none;
  color: inherit;
  font: inherit;
  letter-spacing: inherit;
  text-transform: inherit;
  text-align: left;
  cursor: pointer;
}

/* The caret is the only thing on screen that says "there is more here", so it
   turns rather than swapping glyphs — a rotation survives a font that renders
   ▸ and ▾ at different widths, which would otherwise shift the label sideways
   on every toggle. */
.settings-disclosure__caret {
  display: inline-block;
  transition: transform 120ms ease;
}

.settings-disclosure[aria-expanded='true'] .settings-disclosure__caret {
  transform: rotate(90deg);
}

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

/* The panel re-creates the group's own column, because `hidden` on a flex item
   would otherwise be overridden by `display: flex` and the section would show
   its "collapsed" contents in full. `[hidden]` is declared alongside so the
   two rules cannot be read apart. */
.settings-advanced {
  display: flex;
  flex-direction: column;
  gap: 10px;
  /* The group's bottom padding is moved onto the PANEL (see the rule below),
     so a collapsed Advanced is exactly its header rather than a header with an
     empty 16px shelf under it that reads as something failing to render. Done
     this way rather than with `:has()` on the group so it needs no selector
     the app does not already rely on. */
  padding-bottom: var(--pad);
}

.settings-advanced[hidden] {
  display: none;
}

/* Paired with `.settings-advanced`'s own `padding-bottom` above — the two are
   one decision and must not be edited apart. */
#settings-advanced {
  padding-bottom: 0;
}

.field {
  display: flex;
  flex-direction: column;
  gap: 6px;
}

.field--toggle {
  flex-direction: row;
  align-items: center;
  justify-content: space-between;
}

.field__label {
  color: var(--text-dim);
  font-size: 0.9rem;
}

.field input[type="number"],
.field input[type="text"] {
  min-height: 44px;
  padding: 8px 12px;
  border: 1px solid var(--line);
  border-radius: 10px;
  background: var(--bg-sunken);
  color: var(--text);
  font: inherit;
}

.field input[type="range"] {
  width: 100%;
  accent-color: var(--accent);
}

.field input[type="checkbox"] {
  width: 28px;
  height: 28px;
  accent-color: var(--accent);
}

.debug-readout {
  display: grid;
  grid-template-columns: auto 1fr auto 1fr auto 1fr;
  gap: 4px 8px;
  margin: 0;
  padding: 10px;
  border-radius: 10px;
  background: var(--bg-sunken);
  font-size: 0.85rem;
  font-variant-numeric: tabular-nums;
}

.debug-readout dt {
  color: var(--text-dim);
}

.debug-readout dd {
  margin: 0;
  font-weight: 650;
}

/* The About group's build marker (tilt-party-15). Same sunken card as the
   debug readout — both are diagnostics rather than settings — but two columns
   instead of six, and the value is allowed to wrap: after deploy.yml stamps
   it, this is a commit sha, not a two-character label. */
.about-readout {
  display: grid;
  grid-template-columns: auto 1fr;
  gap: 4px 8px;
  margin: 0;
  padding: 10px;
  border-radius: 10px;
  background: var(--bg-sunken);
  font-size: 0.85rem;
}

.about-readout dt {
  color: var(--text-dim);
}

.about-readout dd {
  margin: 0;
  font-weight: 650;
  overflow-wrap: anywhere;
}

/* The live value beside a slider's label. The thresholds are in m/s² and the
   whole reason a player is on this screen is that the default did not suit
   their phone — a slider with no number is a slider they cannot describe,
   report, or return to. */
.field__value {
  float: right;
  color: var(--text);
  font-variant-numeric: tabular-nums;
  font-weight: 650;
}

/* ── roster editor ─────────────────────────────────────────────────────── */

.settings-players {
  display: flex;
  flex-direction: column;
  gap: 8px;
}

.player-row {
  display: grid;
  /* handle | up | down | next | name (elastic) | colour | remove. The name is
     the only column that gives, so a long name never squeezes the touch
     targets. The three controls added by tilt-party-feat-2 sit BEFORE the name
     because they are all about position in the order, and the drag handle they
     duplicate is already there. */
  grid-template-columns: auto auto auto auto 1fr auto auto;
  align-items: center;
  gap: 8px;
  padding: 4px;
  border-radius: 10px;
  background: var(--bg-sunken);
  /* `touch-action: none` on the handle is what lets the pointer-drag work at
     all on Android: without it the browser claims the gesture for scrolling
     and never delivers the pointermove/pointerup. */
  touch-action: pan-y;
}

.player-row--dragging {
  outline: 2px solid var(--accent);
  opacity: 0.85;
}

.player-drag,
.player-move-up,
.player-move-down,
.player-remove {
  min-width: 44px;
  min-height: 44px;
  border: 0;
  border-radius: 8px;
  background: transparent;
  color: var(--text-dim);
  font-size: 1.1rem;
  cursor: pointer;
}

.player-drag {
  cursor: grab;
  touch-action: none;
}

.player-remove:hover:not(:disabled),
.player-remove:focus-visible {
  color: var(--danger);
}

.player-drag:focus-visible,
.player-move-up:focus-visible,
.player-move-down:focus-visible,
.player-next:focus-visible,
.player-remove:focus-visible,
.player-color:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}

.player-remove:disabled,
.player-move-up:disabled,
.player-move-down:disabled {
  opacity: 0.35;
  cursor: not-allowed;
}

/* The "next up" radio. Scaled up rather than restyled: a native radio already
   carries the exclusivity and the arrow-key navigation this control exists
   for, and the default rendering is the one every assistive technology
   already understands. The accent tint is what makes it read as "whose turn"
   rather than as a generic form field. */
.player-next {
  width: 22px;
  height: 22px;
  margin: 0 2px;
  accent-color: var(--accent);
  cursor: pointer;
}

.player-name {
  min-width: 0;
  min-height: 44px;
  padding: 8px 12px;
  border: 1px solid var(--line);
  border-radius: 10px;
  background: var(--bg-raised);
  color: var(--text);
  font: inherit;
}

.player-color {
  width: 44px;
  height: 44px;
  padding: 2px;
  border: 1px solid var(--line);
  border-radius: 10px;
  background: var(--bg-raised);
  cursor: pointer;
}

/* ── status lines ──────────────────────────────────────────────────────── */

/* Calibration and force-refresh feedback. `min-height` reserves the line
   whether or not there is text in it, so a message appearing does not shove
   the button the player just pressed out from under their thumb. */
.settings-status {
  margin: 0;
  min-height: 1.2em;
  color: var(--text-dim);
  font-size: 0.85rem;
}

/* The haptics probe's reading (tilt-party-feat-6). Unlike `.settings-status`
   this one does NOT reserve a line: it is empty until the button is pressed,
   it is several lines long once it is not, and a permanently reserved gap
   below a button nobody has pressed is just a hole in the Feedback group.
   `word-break` because the reading names pattern lengths and a narrow phone
   would otherwise push the group sideways. */
.probe-readout:not(:empty) {
  margin: 8px 0 0;
  padding: 10px;
  border-radius: 10px;
  background: var(--bg-sunken);
  color: var(--text-dim);
  font-size: 0.85rem;
  line-height: 1.45;
  overflow-wrap: anywhere;
}

/* ─────────────────────────────────────────────── deck editor (portrait) */
/* tilt-party-feat-3. Two views in one section — the picker and the card list —
   so both live here and `hidden` decides which is on. */

.deck-edit-picker,
.deck-edit-cards {
  display: flex;
  flex-direction: column;
  gap: var(--gap);
  flex: 1 1 auto;
  min-height: 0;
  overflow-y: auto;
}

.deck-edit-picker[hidden],
.deck-edit-cards[hidden] {
  display: none;
}

.deck-edit-empty {
  margin: 0;
  padding: var(--pad) 0;
  color: var(--text-dim);
  text-align: center;
}

/* A ROW, not a tile. Deliberately unlike `.deck-tile`: this list is a
   destination for changing a deck, and making it look like the play grid would
   invite a tap meant for a round. No deck colour here for the same reason. */
.deck-edit-pick {
  appearance: none;
  display: flex;
  align-items: center;
  gap: 10px;
  width: 100%;
  padding: 12px 14px;
  border: 1px solid var(--line);
  border-radius: var(--radius);
  background: var(--bg-raised);
  color: var(--text);
  font: inherit;
  text-align: left;
  cursor: pointer;
}

.deck-edit-pick:focus-visible {
  outline: 3px solid var(--accent);
  outline-offset: 2px;
}

.deck-edit-pick__icon {
  font-size: 1.5rem;
  line-height: 1;
}

.deck-edit-pick__name {
  flex: 1 1 auto;
  font-weight: 700;
}

.deck-edit-pick__count {
  color: var(--text-dim);
  font-size: 0.82rem;
  font-variant-numeric: tabular-nums;
}

/* "Changed on this device", on the picker row and on the deck tile. One rule
   for both: they are the same claim and they should not read differently
   depending on which screen you are on. Not colour alone — it is a word. */
.deck-edit-pick__badge,
.deck-tile__badge {
  padding: 2px 7px;
  border-radius: 999px;
  background: color-mix(in srgb, var(--accent) 26%, transparent);
  color: var(--text);
  font-size: 0.7rem;
  font-weight: 700;
  letter-spacing: 0.02em;
  text-transform: uppercase;
  white-space: nowrap;
}

.deck-tile__badge {
  align-self: flex-start;
}

.deck-edit-add {
  display: flex;
  align-items: flex-end;
  gap: 8px;
  flex: 0 0 auto;
}

.deck-edit-add .field {
  flex: 1 1 auto;
  min-width: 0;
}

/* The two export routes, side by side (tilt-party-feat-4). Both present at
   once rather than one with the other behind a "having trouble?" — on an
   installed iOS PWA the download can do nothing at all, silently, and a
   fallback nobody can see is not a fallback. */
.deck-edit__export {
  display: flex;
  gap: 8px;
}

.deck-edit__export .btn {
  flex: 1 1 0;
  min-width: 0;
}

/* Reserves its line whether or not there is text, for the reason
   `.settings-status` does: a refusal appearing must not shove the row the
   player is aiming at out from under their thumb. */
.deck-edit-status {
  margin: 0;
  min-height: 1.2em;
  flex: 0 0 auto;
  color: var(--text-dim);
  font-size: 0.85rem;
}

.deck-edit-status--error {
  color: var(--danger);
}

.deck-edit-list {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: 6px;
  flex: 1 1 auto;
  min-height: 0;
  overflow-y: auto;
}

.card-row {
  display: flex;
  align-items: center;
  gap: 6px;
}

.card-row__text {
  flex: 1 1 auto;
  min-width: 0;
  padding: 10px 12px;
  border: 1px solid var(--line);
  border-radius: 10px;
  background: var(--bg-sunken);
  color: var(--text);
  font: inherit;
}

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

/* Sized to the 44px touch target the rest of the app uses, so a remove sitting
   next to a text field is not the control a thumb hits by accident. */
.card-row__remove {
  flex: 0 0 auto;
  min-width: 44px;
  min-height: 44px;
  color: var(--text-dim);
}

.card-row__remove:hover,
.card-row__remove:focus-visible {
  color: var(--danger);
}

.deck-edit__footer {
  flex: 0 0 auto;
  display: flex;
  flex-direction: column;
  gap: 8px;
  align-items: stretch;
}

/* ─────────────────────────────────────────────────────────── preferences */

@media (prefers-reduced-motion: reduce) {
  *,
  *::before,
  *::after {
    animation-duration: 0.001ms !important;
    transition-duration: 0.001ms !important;
  }
}
